public void CanFundRawTransactionWithChangePositionSpecified() { using (NodeBuilder builder = NodeBuilder.Create(this)) { CoreNode node = builder.CreateStratisPosNode(this.network).WithReadyBlockchainData(ReadyBlockchain.StraxRegTest150Miner).Start(); var tx = this.network.CreateTransaction(); tx.Outputs.Add(new TxOut(Money.Coins(1.1m), new Key().ScriptPubKey)); tx.Outputs.Add(new TxOut(Money.Coins(1.2m), new Key().ScriptPubKey)); tx.Outputs.Add(new TxOut(Money.Coins(1.3m), new Key().ScriptPubKey)); tx.Outputs.Add(new TxOut(Money.Coins(1.4m), new Key().ScriptPubKey)); Money totalSent = tx.TotalOut; // We specifically don't want to use the first available account as that is where the node has been mining to, and that is where the // fundrawtransaction RPC will by default get a change address from. var account = node.FullNode.WalletManager().GetUnusedAccount("mywallet", "password"); var walletAccountReference = new WalletAccountReference("mywallet", account.Name); var changeAddress = node.FullNode.WalletManager().GetUnusedChangeAddress(walletAccountReference); var options = new FundRawTransactionOptions() { ChangeAddress = BitcoinAddress.Create(changeAddress.Address, this.network).ToString(), ChangePosition = 2 }; FundRawTransactionResponse funded = node.CreateRPCClient().FundRawTransaction(tx, options); Money fee = this.CheckFunding(node, funded.Transaction); Money totalInputs = this.GetTotalInputValue(node, funded.Transaction); Assert.Equal(new Money(this.network.MinRelayTxFee), fee); Assert.Equal(2, funded.ChangePos); Assert.Equal(changeAddress.ScriptPubKey, funded.Transaction.Outputs[funded.ChangePos].ScriptPubKey); // Check that the value of the change in the specified position is the expected value. Assert.Equal(totalInputs - totalSent - fee, funded.Transaction.Outputs[funded.ChangePos].Value); } }
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); } }