/// <summary> /// Add recipients to the <see cref="TransactionBuilder" />. /// </summary> /// <param name="context">The context associated with the current transaction being built.</param> /// <remarks> /// Add outputs to the <see cref="TransactionBuilder" /> based on the <see cref="Recipient" /> list. /// </remarks> protected virtual void AddRecipients(TransactionBuildContext context) { if (context.Recipients.Any(a => a.Amount == Money.Zero)) { throw new WalletException("No amount specified."); } if (context.Recipients.Any(a => a.SubtractFeeFromAmount)) { throw new NotImplementedException("Substracting the fee from the recipient is not supported yet."); } foreach (var recipient in context.Recipients) { context.TransactionBuilder.Send(recipient.ScriptPubKey, recipient.Amount); } }
/// <inheritdoc /> public Transaction BuildTransaction(TransactionBuildContext context) { InitializeTransactionBuilder(context); const int maxRetries = 5; var retryCount = 0; TransactionPolicyError[] errors = null; while (retryCount <= maxRetries) { if (context.Shuffle) { context.TransactionBuilder.Shuffle(); } var transaction = context.TransactionBuilder.BuildTransaction(false); if (context.Sign) { var coinsSpent = context.TransactionBuilder.FindSpentCoins(transaction); // TODO: Improve this as we already have secrets when running a retry iteration. AddSecrets(context, coinsSpent); context.TransactionBuilder.SignTransactionInPlace(transaction); } if (context.TransactionBuilder.Verify(transaction, out errors)) { return(transaction); } // Retry only if error is of type 'FeeTooLowPolicyError' if (!errors.Any(e => e is FeeTooLowPolicyError)) { break; } retryCount++; } var errorsMessage = string.Join(" - ", errors.Select(s => s.ToString())); this.logger.LogError($"Build transaction failed: {errorsMessage}"); throw new WalletException($"Could not build the transaction. Details: {errorsMessage}"); }
/// <summary> /// Find the next available change address. /// </summary> /// <param name="context">The context associated with the current transaction being built.</param> protected void FindChangeAddress(TransactionBuildContext context) { if (context.ChangeAddress == null) { // If no change address is supplied, get a new address to send the change to. context.ChangeAddress = this.walletManager.GetUnusedChangeAddress( new WalletAccountReference(context.AccountReference.WalletName, context.AccountReference.AccountName)); } if (context.UseSegwitChangeAddress) { context.TransactionBuilder.SetChange( new BitcoinWitPubKeyAddress(context.ChangeAddress.Bech32Address, this.network).ScriptPubKey); } else { context.TransactionBuilder.SetChange(context.ChangeAddress.ScriptPubKey); } }
/// <summary> /// Find all available outputs (UTXO's) that belong to <see cref="WalletAccountReference.AccountName" />. /// Then add them to the <see cref="TransactionBuildContext.UnspentOutputs" />. /// </summary> /// <param name="context">The context associated with the current transaction being built.</param> protected void AddCoins(TransactionBuildContext context) { context.UnspentOutputs = this.walletManager .GetSpendableTransactionsInAccount(context.AccountReference, context.MinConfirmations).ToList(); if (context.UnspentOutputs.Count == 0) { throw new WalletException("No spendable transactions found."); } // Get total spendable balance in the account. var balance = context.UnspentOutputs.Sum(t => t.Transaction.Amount); var totalToSend = context.Recipients.Sum(s => s.Amount); if (balance < totalToSend) { throw new WalletException("Not enough funds."); } Money sum = 0; var coins = new List <Coin>(); if (context.SelectedInputs != null && context.SelectedInputs.Any()) { // 'SelectedInputs' are inputs that must be included in the // current transaction. At this point we check the given // input is part of the UTXO set and filter out UTXOs that are not // in the initial list if 'context.AllowOtherInputs' is false. var availableHashList = context.UnspentOutputs.ToDictionary(item => item.ToOutPoint(), item => item); if (!context.SelectedInputs.All(input => availableHashList.ContainsKey(input))) { throw new WalletException("Not all the selected inputs were found on the wallet."); } if (!context.AllowOtherInputs) { foreach (var unspentOutputsItem in availableHashList) { if (!context.SelectedInputs.Contains(unspentOutputsItem.Key)) { context.UnspentOutputs.Remove(unspentOutputsItem.Value); } } } foreach (var outPoint in context.SelectedInputs) { var item = availableHashList[outPoint]; coins.Add(new Coin(item.Transaction.Id, (uint)item.Transaction.Index, item.Transaction.Amount, item.Transaction.ScriptPubKey)); sum += item.Transaction.Amount; } } foreach (var item in context.UnspentOutputs .OrderByDescending(a => a.Confirmations > 0) .ThenByDescending(a => a.Transaction.Amount)) { if (context.SelectedInputs?.Contains(item.ToOutPoint()) ?? false) { continue; } // If the total value is above the target // then it's safe to stop adding UTXOs to the coin list. // The primary goal is to reduce the time it takes to build a trx // when the wallet is bloated with UTXOs. // Get to our total, and then check that we're a little bit over to account for tx fees. // If it gets over totalToSend but doesn't hit this break, that's fine too. // The TransactionBuilder will have a go with what we give it, and throw NotEnoughFundsException accurately if it needs to. if (sum > totalToSend + PretendMaxFee) { break; } coins.Add(new Coin(item.Transaction.Id, (uint)item.Transaction.Index, item.Transaction.Amount, item.Transaction.ScriptPubKey)); sum += item.Transaction.Amount; } // All the UTXOs are added to the builder without filtering. // The builder then has its own coin selection mechanism // to select the best UTXO set for the corresponding amount. // To add a custom implementation of a coin selection override // the builder using builder.SetCoinSelection(). context.TransactionBuilder.AddCoins(coins); }
/// <inheritdoc /> public Money EstimateFee(TransactionBuildContext context) { InitializeTransactionBuilder(context); return(context.TransactionFee); }
public async Task <uint256> SendManyAsync(string fromAccount, string addressesJson, int minConf = 1, string comment = null, string subtractFeeFromJson = null, bool isReplaceable = false, int?confTarget = null, string estimateMode = "UNSET") { if (string.IsNullOrEmpty(addressesJson)) { throw new RPCServerException(RPCErrorCode.RPC_INVALID_PARAMETER, "No valid output addresses specified."); } var addresses = new Dictionary <string, decimal>(); try { // Outputs addresses are keyvalue pairs of address, amount. Translate to Receipient list. addresses = JsonConvert.DeserializeObject <Dictionary <string, decimal> >(addressesJson); } catch (JsonSerializationException ex) { throw new RPCServerException(RPCErrorCode.RPC_PARSE_ERROR, ex.Message); } if (addresses.Count == 0) { throw new RPCServerException(RPCErrorCode.RPC_INVALID_PARAMETER, "No valid output addresses specified."); } // Optional list of addresses to subtract fees from. IEnumerable <BitcoinAddress> subtractFeeFromAddresses = null; if (!string.IsNullOrEmpty(subtractFeeFromJson)) { try { subtractFeeFromAddresses = JsonConvert.DeserializeObject <List <string> >(subtractFeeFromJson) .Select(i => BitcoinAddress.Create(i, this.FullNode.Network)); } catch (JsonSerializationException ex) { throw new RPCServerException(RPCErrorCode.RPC_PARSE_ERROR, ex.Message); } } var recipients = new List <Recipient>(); foreach (var address in addresses) { // Check for duplicate recipients var recipientAddress = BitcoinAddress.Create(address.Key, this.FullNode.Network).ScriptPubKey; if (recipients.Any(r => r.ScriptPubKey == recipientAddress)) { throw new RPCServerException(RPCErrorCode.RPC_INVALID_PARAMETER, string.Format("Invalid parameter, duplicated address: {0}.", recipientAddress)); } var recipient = new Recipient { ScriptPubKey = recipientAddress, Amount = Money.Coins(address.Value), SubtractFeeFromAmount = subtractFeeFromAddresses == null ? false : subtractFeeFromAddresses.Contains(BitcoinAddress.Create(address.Key, this.FullNode.Network)) }; recipients.Add(recipient); } var accountReference = GetWalletAccountReference(); var context = new TransactionBuildContext(this.FullNode.Network) { AccountReference = accountReference, MinConfirmations = minConf, Shuffle = true, // We shuffle transaction outputs by default as it's better for anonymity. Recipients = recipients, CacheSecret = false }; // Set fee type for transaction build context. context.FeeType = FeeType.Medium; if (estimateMode.Equals("ECONOMICAL", StringComparison.InvariantCultureIgnoreCase)) { context.FeeType = FeeType.Low; } else if (estimateMode.Equals("CONSERVATIVE", StringComparison.InvariantCultureIgnoreCase)) { context.FeeType = FeeType.High; } try { // Log warnings for currently unsupported parameters. if (!string.IsNullOrEmpty(comment)) { this.logger.LogWarning("'comment' parameter is currently unsupported. Ignored."); } if (isReplaceable) { this.logger.LogWarning("'replaceable' parameter is currently unsupported. Ignored."); } if (confTarget != null) { this.logger.LogWarning("'conf_target' parameter is currently unsupported. Ignored."); } var transaction = this.walletTransactionHandler.BuildTransaction(context); await this.broadcasterManager.BroadcastTransactionAsync(transaction); return(transaction.GetHash()); } catch (SecurityException) { throw new RPCServerException(RPCErrorCode.RPC_WALLET_UNLOCK_NEEDED, "Wallet unlock needed"); } catch (WalletException exception) { throw new RPCServerException(RPCErrorCode.RPC_WALLET_ERROR, exception.Message); } catch (NotImplementedException exception) { throw new RPCServerException(RPCErrorCode.RPC_MISC_ERROR, exception.Message); } }