/// <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);
        }
예제 #12
0
        /// <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}");
        }
예제 #13
0
        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);
            }
        }