/// <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> /// 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 (Recipient recipient in context.Recipients) { context.TransactionBuilder.Send(recipient.ScriptPubKey, recipient.Amount); } }
/// <inheritdoc /> public Transaction BuildTransaction(TransactionBuildContext context) { Guard.NotNull(context, nameof(context)); Guard.NotNull(context.Recipients, nameof(context.Recipients)); Guard.NotNull(context.AccountReference, nameof(context.AccountReference)); context.TransactionBuilder = new TransactionBuilder(); this.AddRecipients(context); this.AddCoins(context); this.AddSecrets(context); this.FindChangeAddress(context); this.AddFee(context); // build transaction context.Transaction = context.TransactionBuilder.BuildTransaction(context.Sign); if (!context.TransactionBuilder.Verify(context.Transaction, out TransactionPolicyError[] errors))
/// <inheritdoc /> public Transaction BuildTransaction(TransactionBuildContext context) { this.InitializeTransactionBuilder(context); const int maxRetries = 5; int retryCount = 0; TransactionPolicyError[] errors = null; while (retryCount <= maxRetries) { if (context.Shuffle) { context.TransactionBuilder.Shuffle(); } Transaction transaction = context.TransactionBuilder.BuildTransaction(false); if (context.Sign) { ICoin[] coinsSpent = context.TransactionBuilder.FindSpentCoins(transaction); // TODO: Improve this as we already have secrets when running a retry iteration. this.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++; } string 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}"); }
/// <inheritdoc /> public Transaction BuildTransaction(TransactionBuildContext context) { this.InitializeTransactionBuilder(context); if (context.Shuffle) { context.TransactionBuilder.Shuffle(); } Transaction transaction = context.TransactionBuilder.BuildTransaction(false); if (context.Sign) { ICoin[] coinsSpent = context.TransactionBuilder.FindSpentCoins(transaction); this.AddSecrets(context, coinsSpent); context.TransactionBuilder.SignTransactionInPlace(transaction); } if (context.TransactionBuilder.Verify(transaction, out TransactionPolicyError[] errors))
/// <summary> /// Initializes the context transaction builder from information in <see cref="TransactionBuildContext"/>. /// </summary> /// <param name="context">Transaction build context.</param> protected virtual void InitializeTransactionBuilder(TransactionBuildContext context) { Guard.NotNull(context, nameof(context)); Guard.NotNull(context.Recipients, nameof(context.Recipients)); Guard.NotNull(context.AccountReference, nameof(context.AccountReference)); context.TransactionBuilder.CoinSelector = new DefaultCoinSelector { GroupByScriptPubKey = false }; context.TransactionBuilder.DustPrevention = false; // If inputs are selected by the user, we just choose them all. if (context.SelectedInputs != null && context.SelectedInputs.Any()) { context.TransactionBuilder.CoinSelector = new AllCoinsSelector(); } bool reCalculateFees = context.SubtractFeesFromRecipients && context.TransactionFee == null; this.AddRecipients(context); this.AddOpReturnOutput(context); this.AddCoins(context); this.FindChangeAddress(context); this.AddFee(context); if (reCalculateFees) { context.TransactionBuilder.ClearSendBuilders(); this.AddRecipients(context); this.AddCoins(context); } if (context.Time.HasValue) { context.TransactionBuilder.SetTimeStamp(context.Time.Value); } }
/// <summary> /// Loads all the private keys for each of the <see cref="HdAddress"/> in <see cref="TransactionBuildContext.UnspentOutputs"/> /// </summary> /// <param name="context">The context associated with the current transaction being built.</param> /// <param name="coinsSpent">The coins spent to generate the transaction.</param> protected void AddSecrets(TransactionBuildContext context, IEnumerable <ICoin> coinsSpent) { if (!context.Sign) { return; } Wallet wallet = this.walletManager.GetWallet(context.AccountReference.WalletName); ExtKey seedExtKey = this.walletManager.GetExtKey(context.AccountReference, context.WalletPassword); var signingKeys = new HashSet <ISecret>(); Dictionary <OutPoint, UnspentOutputReference> outpointLookup = context.UnspentOutputs.ToDictionary(o => o.ToOutPoint(), o => o); IEnumerable <string> uniqueHdPaths = coinsSpent.Select(s => s.Outpoint).Select(o => outpointLookup[o].Address.HdPath).Distinct(); foreach (string hdPath in uniqueHdPaths) { ExtKey addressExtKey = seedExtKey.Derive(new KeyPath(hdPath)); BitcoinExtKey addressPrivateKey = addressExtKey.GetWif(wallet.Network); signingKeys.Add(addressPrivateKey); } context.TransactionBuilder.AddKeys(signingKeys.ToArray()); }
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); } WalletAccountReference accountReference = this.GetAccount(); 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."); } Transaction transaction = this.walletTransactionHandler.BuildTransaction(context); await this.broadcasterManager.BroadcastTransactionAsync(transaction); return(transaction.GetHash()); } catch (SecurityException exception) { throw new RPCServerException(RPCErrorCode.RPC_WALLET_UNLOCK_NEEDED, exception.Message); } catch (WalletException exception) { throw new RPCServerException(RPCErrorCode.RPC_WALLET_ERROR, exception.Message); } catch (NotImplementedException exception) { throw new RPCServerException(RPCErrorCode.RPC_MISC_ERROR, exception.Message); } }
/// <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."); } int totalSubtractingRecipients = context.Recipients.Count(r => r.SubtractFeeFromAmount); // If none of them need the fee subtracted then it's simply a matter of adding the individual recipients to the builder. if (totalSubtractingRecipients == 0) { foreach (Recipient recipient in context.Recipients) { context.TransactionBuilder.Send(recipient.ScriptPubKey, recipient.Amount); } return; } // If the transaction fee has been explicitly specified, and we have any recipients that require a fee to be subtracted // from the amount to be sent, then evenly distribute the chosen fee among all recipients. Any remaining fee should be // subtracted from the first recipient. if (context.TransactionFee != null) { Money fee = context.TransactionFee; long recipientFee = fee.Satoshi / totalSubtractingRecipients; long remainingFee = fee.Satoshi % totalSubtractingRecipients; for (int i = 0; i < context.Recipients.Count; i++) { Recipient recipient = context.Recipients[i]; if (recipient.SubtractFeeFromAmount) { // First receiver pays the remainder not divisible by output count. long feeToSubtract = i == 0 ? remainingFee + recipientFee : recipientFee; long remainingAmount = recipient.Amount.Satoshi - feeToSubtract; if (remainingAmount <= 0) { throw new WalletException($"Fee {feeToSubtract} is higher than amount {recipient.Amount.Satoshi} to send."); } recipient.Amount = new Money(remainingAmount); } context.TransactionBuilder.Send(recipient.ScriptPubKey, recipient.Amount); } } else { // This is currently a limitation of the NBitcoin TransactionBuilder. // The only alternative would possibly be to recompute the output sizes after the AddFee call. if (totalSubtractingRecipients > 1) { throw new WalletException($"Cannot subtract fee from more than 1 recipient if {nameof(context.TransactionFee)} is not set."); } // If the transaction fee has not been explicitly specified yet, then the builder needs to assign it later from the wallet fee policy. // So we just need to indicate to the builder that the fees must be subtracted from the specified recipient. foreach (Recipient recipient in context.Recipients) { context.TransactionBuilder.Send(recipient.ScriptPubKey, recipient.Amount); if (recipient.SubtractFeeFromAmount) { context.TransactionBuilder.SubtractFees(); } } } }
/// <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. long balance = context.UnspentOutputs.Sum(t => t.Transaction.Amount); long totalToSend = context.Recipients.Sum(s => s.Amount) + (context.OpReturnAmount ?? Money.Zero); 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. Dictionary <OutPoint, UnspentOutputReference> 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 in the wallet."); } if (!context.AllowOtherInputs) { foreach (KeyValuePair <OutPoint, UnspentOutputReference> unspentOutputsItem in availableHashList) { if (!context.SelectedInputs.Contains(unspentOutputsItem.Key)) { context.UnspentOutputs.Remove(unspentOutputsItem.Value); } } } foreach (OutPoint outPoint in context.SelectedInputs) { UnspentOutputReference 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 (UnspentOutputReference 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) { this.InitializeTransactionBuilder(context); return(context.TransactionFee); }
/// <inheritdoc /> public Transaction BuildTransaction(TransactionBuildContext context) { this.InitializeTransactionBuilder(context); const int maxRetries = 5; int retryCount = 0; TransactionPolicyError[] errors = { }; while (retryCount <= maxRetries) { if (context.Shuffle) { context.TransactionBuilder.Shuffle(); } Transaction transaction = context.TransactionBuilder.BuildTransaction(false); // If there are cross chain deposits, try and validate them before // we continue with signing and verification. DepositValidationHelper.ValidateCrossChainDeposit(this.network, transaction); ICoin[] spentCoins = context.TransactionBuilder.FindSpentCoins(transaction); if (context.Sign) { // TODO: Improve this as we already have secrets when running a retry iteration. this.AddSecrets(context, spentCoins); context.TransactionBuilder.SignTransactionInPlace(transaction); if (context.TransactionBuilder.Verify(transaction, out errors)) { // Only reserve the UTXOs if the transaction was successfully built. this.reserveUtxoService.ReserveUtxos(spentCoins.Select(c => c.Outpoint)); return(transaction); } } else { // Only reserve the UTXOs if the transaction was successfully built. this.reserveUtxoService.ReserveUtxos(spentCoins.Select(c => c.Outpoint)); // If we aren't being asked to sign then it is not really meaningful to perform the Verify step. // TODO: Do we still need to check for FeeTooLowPolicyError in this case? return(transaction); } // Retry only if error is of type 'FeeTooLowPolicyError'. if (!errors.Any(e => e is FeeTooLowPolicyError)) { break; } retryCount++; } string 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}"); }
public async Task <FundRawTransactionResponse> FundRawTransactionAsync(string rawHex, FundRawTransactionOptions options = null, bool?isWitness = null) { try { // TODO: Bitcoin Core performs an heuristic check to determine whether or not the provided transaction should be deserialised with witness data -> core_read.cpp DecodeHexTx() Transaction rawTx = this.Network.CreateTransaction(); // This is an uncommon case where we cannot simply rely on the consensus factory to do the right thing. // We need to override the protocol version so that the RPC client workaround functions correctly. // If this was not done the transaction deserialisation would attempt to use witness deserialisation and the transaction data would get mangled. rawTx.FromBytes(Encoders.Hex.DecodeData(rawHex), this.Network.Consensus.ConsensusFactory, ProtocolVersion.WITNESS_VERSION - 1); WalletAccountReference account = this.GetWalletAccountReference(); HdAddress changeAddress = null; // TODO: Support ChangeType properly; allow both 'legacy' and 'bech32'. p2sh-segwit could be added when wallet support progresses to store p2sh redeem scripts if (options != null && !string.IsNullOrWhiteSpace(options.ChangeType) && options.ChangeType != "legacy") { throw new RPCServerException(RPCErrorCode.RPC_INVALID_PARAMETER, "The change_type option is not yet supported"); } if (options?.ChangeAddress != null) { changeAddress = this.walletManager.GetAllAccounts().SelectMany(a => a.GetCombinedAddresses()).FirstOrDefault(a => a.Address == options?.ChangeAddress); } else { changeAddress = this.walletManager.GetUnusedChangeAddress(account); } if (options?.ChangePosition != null && options.ChangePosition > rawTx.Outputs.Count) { throw new RPCServerException(RPCErrorCode.RPC_WALLET_ERROR, "Invalid change position specified!"); } var context = new TransactionBuildContext(this.Network) { AccountReference = account, ChangeAddress = changeAddress, OverrideFeeRate = options?.FeeRate, TransactionFee = (options?.FeeRate == null) ? new Money(this.Network.MinRelayTxFee) : null, MinConfirmations = 0, Shuffle = false, UseSegwitChangeAddress = changeAddress != null && (options?.ChangeAddress == changeAddress.Bech32Address), Sign = false }; context.Recipients.AddRange(rawTx.Outputs .Select(s => new Recipient { ScriptPubKey = s.ScriptPubKey, Amount = s.Value, SubtractFeeFromAmount = false // TODO: Do we properly support only subtracting the fee from particular recipients? })); context.AllowOtherInputs = true; foreach (TxIn transactionInput in rawTx.Inputs) { context.SelectedInputs.Add(transactionInput.PrevOut); } Transaction newTransaction = this.walletTransactionHandler.BuildTransaction(context); // If the change position can't be found for some reason, then -1 is the intended default. int foundChange = -1; if (context.ChangeAddress != null) { // Try to find the position of the change and copy it over to the original transaction. // The only logical reason why the change would not be found (apart from errors) is that the chosen input UTXOs were precisely the right size. // Conceivably there could be another output that shares the change address too. // TODO: Could add change position field to the transaction build context to make this check unnecessary if (newTransaction.Outputs.Select(o => o.ScriptPubKey == context.ChangeAddress.ScriptPubKey).Count() > 1) { // This should only happen if the change address was deliberately included in the recipients. So find the output that has a different amount. int index = 0; foreach (TxOut newTransactionOutput in newTransaction.Outputs) { if (newTransactionOutput.ScriptPubKey == context.ChangeAddress.ScriptPubKey) { // Set this regardless. It will be overwritten if a subsequent output is the 'correct' change output. // If all potential change outputs have identical values it won't be updated, but in that case any of them are acceptable as the 'real' change output. if (foundChange == -1) { foundChange = index; } // TODO: When SubtractFeeFromAmount is set this amount check will no longer be valid as they won't be equal // If the amount was not in the recipients list then it must be the change output. if (!context.Recipients.Any(recipient => recipient.ScriptPubKey == newTransactionOutput.ScriptPubKey && recipient.Amount == newTransactionOutput.Value)) { foundChange = index; } } index++; } } else { int index = 0; foreach (TxOut newTransactionOutput in newTransaction.Outputs) { if (newTransactionOutput.ScriptPubKey == context.ChangeAddress.ScriptPubKey) { foundChange = index; } index++; } } if (foundChange != -1) { // The position the change will be copied from in the transaction. int tempPos = foundChange; // Just overwrite this to avoid introducing yet another change position variable to the outer scope. // We need to update the foundChange value to return it in the RPC response as the final change position. foundChange = options?.ChangePosition ?? (RandomUtils.GetInt32() % rawTx.Outputs.Count); rawTx.Outputs.Insert(foundChange, newTransaction.Outputs[tempPos]); } else { // This should never happen so it is better to error out than potentially return incorrect results. throw new RPCServerException(RPCErrorCode.RPC_WALLET_ERROR, "Unable to locate change output in built transaction!"); } } // TODO: Copy any updated output amounts, which might have changed due to the subtractfee flags etc (this also includes spreading the fee over the selected outputs, if applicable) // Copy all the inputs from the built transaction into the original. // As they are unsigned this has no effect on transaction validity. foreach (TxIn newTransactionInput in newTransaction.Inputs) { if (!context.SelectedInputs.Contains(newTransactionInput.PrevOut)) { rawTx.Inputs.Add(newTransactionInput); if (options?.LockUnspents ?? false) { if (this.reserveUtxoService == null) { continue; } // Prevent the provided UTXO from being spent by another transaction until this one is signed and broadcast. this.reserveUtxoService.ReserveUtxos(new[] { newTransactionInput.PrevOut }); } } } return(new FundRawTransactionResponse() { ChangePos = foundChange, Fee = context.TransactionFee, Transaction = rawTx }); } 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); } }