private Task GenerateBtcOutputs() { return(Retry.Try(async() => { var setting = await GetAssetSetting("BTC"); var hotWallet = OpenAssetsHelper.ParseAddress(setting.HotWallet); var outputs = (await _bitcoinOutputsService.GetUncoloredUnspentOutputs(setting.HotWallet, 0, false)).Cast <Coin>().ToList(); var balance = new Money(outputs.DefaultIfEmpty().Sum(o => o?.Amount ?? Money.Zero)); var outputSize = new Money(setting.OutputSize, MoneyUnit.BTC); Money changeBalance = Money.Zero; if (setting.ChangeWallet != setting.HotWallet && !string.IsNullOrEmpty(setting.ChangeWallet)) { var changeOutputs = (await _bitcoinOutputsService.GetUncoloredUnspentOutputs(setting.ChangeWallet, 0, false)).Cast <Coin>().ToList(); changeBalance = new Money(changeOutputs.DefaultIfEmpty().Sum(o => o?.Amount ?? Money.Zero)); } if ((balance + changeBalance).ToDecimal(MoneyUnit.BTC) < setting.MinBalance) { await SendBalanceNotifications("BTC", setting.HotWallet, setting.MinBalance); } var existingCoinsCount = outputs.Count(o => o.Amount <= outputSize && o.Amount > outputSize / 2); if (existingCoinsCount > setting.MinOutputsCount) { return; } var generateCnt = setting.MaxOutputsCount - existingCoinsCount; var coins = outputs.Where(o => o.Amount > outputSize * 2).ToList(); balance = coins.DefaultIfEmpty().Sum(o => o?.Amount ?? Money.Zero); generateCnt = Math.Min(generateCnt, (int)(balance / outputSize)); if (generateCnt == 0) { return; } await GenerateOutputs(generateCnt, coins, hotWallet, outputSize, await _assetRepostory.GetItemAsync("BTC"), setting); }, exception => (exception as BackendException)?.Code == ErrorCode.TransactionConcurrentInputsProblem, 3, _logger)); }
public Task <CreateTransactionResponse> GetTransferAllTransaction(BitcoinAddress @from, BitcoinAddress to, Guid transactionId) { return(Retry.Try(async() => { var context = new TransactionBuildContext(_connectionParams.Network, _pregeneratedOutputsQueueFactory); return await context.Build(async() => { var builder = new TransactionBuilder(); var uncoloredCoins = (await _bitcoinOutputsService.GetUncoloredUnspentOutputs(from.ToString())).ToList(); var coloredCoins = (await _bitcoinOutputsService.GetColoredUnspentOutputs(from.ToString())).ToList(); if (uncoloredCoins.Count == 0 && coloredCoins.Count == 0) { throw new BackendException("Address has no unspent outputs", ErrorCode.NoCoinsFound); } if (uncoloredCoins.Count > 0) { _transactionBuildHelper.SendWithChange(builder, context, uncoloredCoins, to, uncoloredCoins.Sum(o => o.TxOut.Value), from); } foreach (var assetGroup in coloredCoins.GroupBy(o => o.AssetId)) { var sum = new AssetMoney(assetGroup.Key); foreach (var coloredCoin in assetGroup) { sum += coloredCoin.Amount; } _transactionBuildHelper.SendAssetWithChange(builder, context, assetGroup.ToList(), to, sum, from); } await _transactionBuildHelper.AddFee(builder, context); var buildedTransaction = builder.BuildTransaction(true); await SaveSpentOutputs(transactionId, buildedTransaction); await _signRequestRepository.InsertTransactionId(transactionId); await SaveNewOutputs(transactionId, buildedTransaction, context); return new CreateTransactionResponse(buildedTransaction.ToHex(), transactionId); }); }, exception => (exception as BackendException)?.Code == ErrorCode.TransactionConcurrentInputsProblem, 3, _log)); }
public async Task <PrivateTransferResponse> GetPrivateTransferTransaction(BitcoinAddress source, BitcoinAddress destinationAddress, decimal amount, decimal fee, Guid transactionId) { return(await Retry.Try(() => { var context = _transactionBuildContextFactory.Create(_connectionParams.Network); return context.Build(async() => { var coins = (await _bitcoinOutputsService.GetUncoloredUnspentOutputs(source.ToString())).OfType <Coin>().ToList(); var totalAmount = new Money(coins.Select(o => o.Amount).DefaultIfEmpty().Sum(o => o?.Satoshi ?? 0)); if (totalAmount.ToDecimal(MoneyUnit.BTC) < amount) { throw new BackendException($"The sum of total applicable outputs is less than the required: {amount} btc.", ErrorCode.NotEnoughBitcoinAvailable); } var builder = new TransactionBuilder(); builder.AddCoins(coins); builder.SetChange(source); builder.Send(destinationAddress, new Money(amount, MoneyUnit.BTC)); builder.SubtractFees(); if (fee == 0) { fee = builder.EstimateFees(await _feeProvider.GetFeeRate()).ToDecimal(MoneyUnit.BTC); } builder.SendFees(new Money(fee, MoneyUnit.BTC)); var tx = builder.BuildTransaction(true); return new PrivateTransferResponse(tx.ToHex(), transactionId, fee); }); }, exception => (exception as BackendException)?.Code == ErrorCode.TransactionConcurrentInputsProblem, 3, _log)); }
public async Task Send() { var feePerByte = (await _feeProvider.GetFeeRate()).FeePerK.Satoshi * _baseSettings.SpendChangeFeeRateMultiplier / 1000; var coins = (await _bitcoinOutputsService.GetUncoloredUnspentOutputs(_baseSettings.ChangeAddress, ConfirmationsCount)).OfType <Coin>() .Where(o => o.Amount.Satoshi > feePerByte * Constants.InputSize).ToList(); Utils.Shuffle(coins, new Random()); while (coins.Count > _baseSettings.NumberOfChangeInputsForTransaction) { var part = coins.Take(_baseSettings.NumberOfChangeInputsForTransaction).ToList(); var balance = part.Sum(o => o.Amount); var builder = new TransactionBuilder(); builder.AddCoins(part); builder.Send(new BitcoinPubKeyAddress(_baseSettings.HotWalletForPregeneratedOutputs), balance); var tr = builder.BuildTransaction(false); var fee = new Money((await _feeProvider.CalcFeeForTransaction(builder)).Satoshi * _baseSettings.SpendChangeFeeRateMultiplier, MoneyUnit.Satoshi); if (fee > balance) { await _log.WriteWarningAsync("SendFromChangeToHotWalletFunction", "Send", null, $"Calculated fee is more than balance ({fee.Satoshi}>{balance})"); continue; } tr.Outputs[0].Value = tr.Outputs[0].Value - fee; var signed = await _signatureApiProvider.SignTransaction(tr.ToHex()); var signedTr = new Transaction(signed); var transactionId = Guid.NewGuid(); await _bitcoinBroadcastService.BroadcastTransaction(transactionId, signedTr); await _spentOutputService.SaveSpentOutputs(transactionId, signedTr); coins = coins.Skip(_baseSettings.NumberOfChangeInputsForTransaction).ToList(); } }
private async Task GenerateFeeOutputs() { await _logger.WriteInfoAsync("GenerateOutputsFunction", "GenerateFeeOutputs", null, "Start process"); var queue = _pregeneratedOutputsQueueFactory.CreateFeeQueue(); try { while (await queue.Count() < _baseSettings.MinPregeneratedOutputsCount) { var uncoloredOutputs = await _bitcoinOutputsService.GetUncoloredUnspentOutputs(_baseSettings.HotWalletForPregeneratedOutputs); var outputs = uncoloredOutputs.ToList(); var totalRequiredAmount = Money.FromUnit(_baseSettings.GenerateOutputsBatchSize * _baseSettings.PregeneratedFeeAmount, MoneyUnit.BTC); // Convert to satoshi var feeAmount = new Money(_baseSettings.PregeneratedFeeAmount, MoneyUnit.BTC); if (outputs.Sum(o => o.TxOut.Value) < totalRequiredAmount) { throw new BackendException($"The sum of total applicable outputs is less than the required: {totalRequiredAmount} satoshis.", ErrorCode.NotEnoughBitcoinAvailable); } var hotWallet = new BitcoinPubKeyAddress(_baseSettings.HotWalletForPregeneratedOutputs, _connectionParams.Network); var builder = new TransactionBuilder(); builder.AddCoins(outputs); builder.SetChange(hotWallet); for (var i = 0; i < _baseSettings.GenerateOutputsBatchSize; i++) { builder.Send(new BitcoinPubKeyAddress(_baseSettings.FeeAddress, _connectionParams.Network), feeAmount); } builder.SendFees(await _feeProvider.CalcFeeForTransaction(builder)); var signedHex = await _signatureApiProvider.SignTransaction(builder.BuildTransaction(false).ToHex()); var signedTr = new Transaction(signedHex); var transactionId = Guid.NewGuid(); await _bitcoinClient.BroadcastTransaction(signedTr, transactionId); await queue.EnqueueOutputs(signedTr.Outputs.AsCoins().Where(o => o.TxOut.Value == feeAmount).ToArray()); await FinishOutputs(transactionId, signedTr, hotWallet); } } finally { if (await queue.Count() < _baseSettings.MinPregeneratedOutputsCount && (DateTime.UtcNow - _lastWarningFeeSentTime).TotalHours > 1) { var message = $"Count of fee outputs is less than {_baseSettings.MinPregeneratedOutputsCount}"; await _slackNotifier.FinanceWarningAsync(message); await _emailNotifier.WarningAsync("Bitcoin job", message); _lastWarningFeeSentTime = DateTime.UtcNow; } await _logger.WriteInfoAsync("GenerateOutputsFunction", "GenerateFeeOutputs", null, "End process"); } }