예제 #1
0
        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);
            }
        }
예제 #2
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);
            }
        }