public void SendAssetWithChange(TransactionBuilder builder, TransactionBuildContext context, List <ColoredCoin> coins, IDestination destination, AssetMoney amount, IDestination changeDestination) { if (amount.Quantity <= 0) { throw new BackendException("Amount can't be less or equal to zero", ErrorCode.BadInputParameter); } Action throwError = () => { throw new BackendException($"The sum of total applicable outputs is less than the required: {amount.Quantity} {amount.Id}.", ErrorCode.NotEnoughAssetAvailable); }; var selectedCoins = OpenAssetsHelper.CoinSelect(coins, amount); if (selectedCoins == null) { throwError(); } var orderedCounts = selectedCoins.Cast <ColoredCoin>().OrderBy(o => o.Amount).ToList(); var sendAmount = new AssetMoney(amount.Id); var cnt = 0; while (sendAmount < amount && cnt < orderedCounts.Count) { sendAmount += orderedCounts[cnt].Amount; cnt++; } if (sendAmount < amount) { throwError(); } builder.AddCoins(orderedCounts.Take(cnt)); context.AddCoins(orderedCounts.Take(cnt)); builder.SendAsset(destination, amount); if ((sendAmount - amount).Quantity > 0) { builder.SendAsset(changeDestination, sendAmount - amount); } }
public async Task <decimal> SendWithChange(TransactionBuilder builder, TransactionBuildContext context, List <ICoin> coins, IDestination destination, Money amount, IDestination changeDestination, bool addDust = true) { if (amount.Satoshi <= 0) { throw new BackendException("Amount can't be less or equal to zero", ErrorCode.BadInputParameter); } Action throwError = () => { throw new BackendException($"The sum of total applicable outputs is less than the required: {amount.Satoshi} satoshis.", ErrorCode.NotEnoughBitcoinAvailable); }; var selectedCoins = OpenAssetsHelper.CoinSelect(coins, amount); if (selectedCoins == null) { throwError(); } var orderedCoins = selectedCoins.OrderBy(o => o.Amount).ToList(); var sendAmount = Money.Zero; var cnt = 0; while (sendAmount < amount && cnt < orderedCoins.Count) { sendAmount += orderedCoins[cnt].TxOut.Value; cnt++; } if (sendAmount < amount) { throwError(); } context.AddCoins(orderedCoins.Take(cnt)); builder.AddCoins(orderedCoins.Take(cnt)); var sent = await Send(builder, context, destination, amount, addDust); if (sendAmount - amount > 0) { await Send(builder, context, changeDestination, sendAmount - amount, addDust); } return(sent); }
public Task <CreateMultiCashoutTransactionResult> GetMultipleCashoutTransaction(List <ICashoutRequest> cashoutRequests, Guid transactionId) { return(Retry.Try(async() => { var context = _transactionBuildContextFactory.Create(_connectionParams.Network); var assetSetting = await _assetSettingCache.GetItemAsync("BTC"); var hotWallet = OpenAssetsHelper.ParseAddress(assetSetting.HotWallet); var changeWallet = OpenAssetsHelper.ParseAddress(string.IsNullOrWhiteSpace(assetSetting.ChangeWallet) ? assetSetting.HotWallet : assetSetting.ChangeWallet); return await context.Build(async() => { var builder = new TransactionBuilder(); var hotWalletOutputs = (await _bitcoinOutputsService.GetUncoloredUnspentOutputs(hotWallet.ToString())).OfType <Coin>().ToList(); var hotWalletBalance = new Money(hotWalletOutputs.Select(o => o.Amount).DefaultIfEmpty().Sum(o => o?.Satoshi ?? 0)); var maxFeeForTransaction = Money.FromUnit(0.099M, MoneyUnit.BTC); var selectedCoins = new List <Coin>(); var maxInputsCount = maxFeeForTransaction.Satoshi / (await _feeProvider.GetFeeRate()).GetFee(Constants.InputSize).Satoshi; var tryCount = 100; do { if (selectedCoins.Count > maxInputsCount && cashoutRequests.Count > 1) { cashoutRequests = cashoutRequests.Take(cashoutRequests.Count - 1).ToList(); selectedCoins.Clear(); } else if (selectedCoins.Count > 0) { break; } var totalAmount = Money.FromUnit(cashoutRequests.Select(o => o.Amount).Sum(), MoneyUnit.BTC); if (hotWalletBalance < totalAmount + maxFeeForTransaction) { var changeBalance = Money.Zero; List <Coin> changeWalletOutputs = new List <Coin>(); if (hotWallet != changeWallet) { changeWalletOutputs = (await _bitcoinOutputsService.GetUncoloredUnspentOutputs(changeWallet.ToString())) .OfType <Coin>().ToList(); changeBalance = new Money(changeWalletOutputs.Select(o => o.Amount).DefaultIfEmpty().Sum(o => o?.Satoshi ?? 0)); } if (changeBalance + hotWalletBalance >= totalAmount + maxFeeForTransaction) { selectedCoins.AddRange(hotWalletOutputs); selectedCoins.AddRange(OpenAssetsHelper .CoinSelect(changeWalletOutputs, totalAmount + maxFeeForTransaction - hotWalletBalance).OfType <Coin>()); } else { selectedCoins.AddRange(hotWalletOutputs); selectedCoins.AddRange(changeWalletOutputs); int cashoutsUsedCount = 0; var cashoutsAmount = await _transactionBuildHelper.CalcFee(selectedCoins.Count, cashoutRequests.Count + 1); foreach (var cashoutRequest in cashoutRequests) { cashoutsAmount += Money.FromUnit(cashoutRequest.Amount, MoneyUnit.BTC); if (cashoutsAmount > hotWalletBalance + changeBalance) { break; } cashoutsUsedCount++; } if (cashoutsUsedCount == 0) { throw new BackendException("Not enough bitcoin available", ErrorCode.NotEnoughBitcoinAvailable); } cashoutRequests = cashoutRequests.Take(cashoutsUsedCount).ToList(); } if (changeWallet != hotWallet) { if (assetSetting.Asset == Constants.DefaultAssetSetting) { assetSetting = await CreateAssetSetting("BTC", assetSetting); } if (assetSetting.Asset != Constants.DefaultAssetSetting) { await _assetSettingRepository.UpdateHotWallet(assetSetting.Asset, assetSetting.ChangeWallet); } } } else { selectedCoins.AddRange(OpenAssetsHelper.CoinSelect(hotWalletOutputs, totalAmount + maxFeeForTransaction).OfType <Coin>()); } } while (tryCount-- > 0); var selectedCoinsAmount = new Money(selectedCoins.Sum(o => o.Amount)); var sendAmount = new Money(cashoutRequests.Sum(o => o.Amount), MoneyUnit.BTC); builder.AddCoins(selectedCoins); foreach (var cashout in cashoutRequests) { var amount = Money.FromUnit(cashout.Amount, MoneyUnit.BTC); builder.Send(OpenAssetsHelper.ParseAddress(cashout.DestinationAddress), amount); } builder.Send(changeWallet, selectedCoinsAmount - sendAmount); builder.SubtractFees(); builder.SendEstimatedFees(await _feeProvider.GetFeeRate()); builder.SetChange(changeWallet); var tx = builder.BuildTransaction(true); _transactionBuildHelper.AggregateOutputs(tx); await _broadcastedOutputRepository.InsertOutputs( tx.Outputs.AsCoins().Where(o => o.ScriptPubKey == changeWallet.ScriptPubKey).Select(o => new BroadcastedOutput(o, transactionId, _connectionParams.Network)).ToList()); await _spentOutputService.SaveSpentOutputs(transactionId, tx); return new CreateMultiCashoutTransactionResult { Transaction = tx, UsedRequests = cashoutRequests }; }); }, exception => (exception as BackendException)?.Code == ErrorCode.TransactionConcurrentInputsProblem, 3, _log)); }
public async Task Test() { var hotWallet = OpenAssetsHelper.ParseAddress("mj5FEqrC2P4FjFNfX8q3eZ4UABWUcRNy9r"); var changeWallet = OpenAssetsHelper.ParseAddress("minog49vnNVuWK5kSs5Ut1iPyNZcR1i7he"); var hotWalletOutputs = GenerateOutputs(5); var hotWalletBalance = new Money(hotWalletOutputs.Select(o => o.Amount).DefaultIfEmpty().Sum(o => o?.Satoshi ?? 0)); var maxFeeForTransaction = Money.FromUnit(0.099M, MoneyUnit.BTC); var selectedCoins = new List <Coin>(); var _feeProvider = Config.Services.GetService <IFeeProvider>(); var _transactionBuildHelper = Config.Services.GetService <ITransactionBuildHelper>(); var cashoutRequests = (GenerateCashoutRequest(200)).ToList(); var maxInputsCount = maxFeeForTransaction.Satoshi / (await _feeProvider.GetFeeRate()).GetFee(Constants.InputSize).Satoshi; do { if (selectedCoins.Count > maxInputsCount && cashoutRequests.Count > 1) { cashoutRequests = cashoutRequests.Take(cashoutRequests.Count - 1).ToList(); selectedCoins.Clear(); } else if (selectedCoins.Count > 0) { break; } var totalAmount = Money.FromUnit(cashoutRequests.Select(o => o.Amount).Sum(), MoneyUnit.BTC); if (hotWalletBalance < totalAmount + maxFeeForTransaction) { var changeBalance = Money.Zero; List <Coin> changeWalletOutputs = new List <Coin>(); if (hotWallet != changeWallet) { changeWalletOutputs = GenerateOutputs(1).ToList(); changeBalance = new Money(changeWalletOutputs.Select(o => o.Amount).DefaultIfEmpty().Sum(o => o?.Satoshi ?? 0)); } if (changeBalance + hotWalletBalance >= totalAmount + maxFeeForTransaction) { selectedCoins.AddRange(hotWalletOutputs); selectedCoins.AddRange(OpenAssetsHelper .CoinSelect(changeWalletOutputs, totalAmount + maxFeeForTransaction - hotWalletBalance).OfType <Coin>()); } else { selectedCoins.AddRange(hotWalletOutputs); selectedCoins.AddRange(changeWalletOutputs); int cashoutsUsedCount = 0; var cashoutsAmount = await _transactionBuildHelper.CalcFee(selectedCoins.Count, cashoutRequests.Count + 1); foreach (var cashoutRequest in cashoutRequests) { cashoutsAmount += Money.FromUnit(cashoutRequest.Amount, MoneyUnit.BTC); if (cashoutsAmount > hotWalletBalance + changeBalance) { break; } cashoutsUsedCount++; } if (cashoutsUsedCount == 0) { throw new BackendException("Not enough bitcoin available", ErrorCode.NotEnoughBitcoinAvailable); } cashoutRequests = cashoutRequests.Take(cashoutsUsedCount).ToList(); } } else { selectedCoins.AddRange(OpenAssetsHelper.CoinSelect(hotWalletOutputs, totalAmount + maxFeeForTransaction).OfType <Coin>()); } } while (true); var selectedCoinsAmount = new Money(selectedCoins.Sum(o => o.Amount)); var sendAmount = new Money(cashoutRequests.Sum(o => o.Amount), MoneyUnit.BTC); var builder = new TransactionBuilder(); builder.AddCoins(selectedCoins); foreach (var cashout in cashoutRequests) { var amount = Money.FromUnit(cashout.Amount, MoneyUnit.BTC); builder.Send(OpenAssetsHelper.ParseAddress(cashout.DestinationAddress), amount); } builder.Send(changeWallet, selectedCoinsAmount - sendAmount); builder.SubtractFees(); builder.SendEstimatedFees(await _feeProvider.GetFeeRate()); builder.SetChange(changeWallet); var tx = builder.BuildTransaction(true); _transactionBuildHelper.AggregateOutputs(tx); }