From d633db54166497685b80a12c51db6772982e01fe Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Thu, 15 May 2025 11:32:04 -0400 Subject: [PATCH 1/3] rpc: add "ischange: true" in wallet gettransaction decoded tx output --- src/core_io.h | 3 ++- src/core_write.cpp | 7 ++++++- src/rpc/rawtransaction.cpp | 1 + src/wallet/rpc/transactions.cpp | 11 ++++++++++- test/functional/wallet_basic.py | 9 ++++++--- 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/core_io.h b/src/core_io.h index ce2e8f67128..1874c93a05c 100644 --- a/src/core_io.h +++ b/src/core_io.h @@ -21,6 +21,7 @@ class SigningProvider; class uint256; class UniValue; class CTxUndo; +class CTxOut; /** * Verbose level for block's transaction @@ -46,6 +47,6 @@ std::string FormatScript(const CScript& script); std::string EncodeHexTx(const CTransaction& tx); std::string SighashToStr(unsigned char sighash_type); void ScriptToUniv(const CScript& script, UniValue& out, bool include_hex = true, bool include_address = false, const SigningProvider* provider = nullptr); -void TxToUniv(const CTransaction& tx, const uint256& block_hash, UniValue& entry, bool include_hex = true, const CTxUndo* txundo = nullptr, TxVerbosity verbosity = TxVerbosity::SHOW_DETAILS); +void TxToUniv(const CTransaction& tx, const uint256& block_hash, UniValue& entry, bool include_hex = true, const CTxUndo* txundo = nullptr, TxVerbosity verbosity = TxVerbosity::SHOW_DETAILS, std::function is_change_func = {}); #endif // BITCOIN_CORE_IO_H diff --git a/src/core_write.cpp b/src/core_write.cpp index 253dfde1006..d7baf4ccd13 100644 --- a/src/core_write.cpp +++ b/src/core_write.cpp @@ -168,7 +168,7 @@ void ScriptToUniv(const CScript& script, UniValue& out, bool include_hex, bool i out.pushKV("type", GetTxnOutputType(type)); } -void TxToUniv(const CTransaction& tx, const uint256& block_hash, UniValue& entry, bool include_hex, const CTxUndo* txundo, TxVerbosity verbosity) +void TxToUniv(const CTransaction& tx, const uint256& block_hash, UniValue& entry, bool include_hex, const CTxUndo* txundo, TxVerbosity verbosity, std::function is_change_func) { CHECK_NONFATAL(verbosity >= TxVerbosity::SHOW_DETAILS); @@ -243,6 +243,11 @@ void TxToUniv(const CTransaction& tx, const uint256& block_hash, UniValue& entry UniValue o(UniValue::VOBJ); ScriptToUniv(txout.scriptPubKey, /*out=*/o, /*include_hex=*/true, /*include_address=*/true); out.pushKV("scriptPubKey", std::move(o)); + + if (is_change_func && is_change_func(txout)) { + out.pushKV("ischange", true); + } + vout.push_back(std::move(out)); if (have_undo) { diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index de0ee601584..2f1bd98b38c 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -118,6 +118,7 @@ static std::vector DecodeTxDoc(const std::string& txid_field_doc) {RPCResult::Type::STR_AMOUNT, "value", "The value in " + CURRENCY_UNIT}, {RPCResult::Type::NUM, "n", "index"}, {RPCResult::Type::OBJ, "scriptPubKey", "", ScriptPubKeyDoc()}, + {RPCResult::Type::BOOL, "ischange", /*optional=*/true, "Output script is change (only if wallet transaction and true for selected rpcwallet)"}, }}, }}, }; diff --git a/src/wallet/rpc/transactions.cpp b/src/wallet/rpc/transactions.cpp index f31596fce8d..36044afd3fe 100644 --- a/src/wallet/rpc/transactions.cpp +++ b/src/wallet/rpc/transactions.cpp @@ -796,7 +796,16 @@ RPCHelpMan gettransaction() if (verbose) { UniValue decoded(UniValue::VOBJ); - TxToUniv(*wtx.tx, /*block_hash=*/uint256(), /*entry=*/decoded, /*include_hex=*/false); + TxToUniv(*wtx.tx, + /*block_hash=*/uint256(), + /*entry=*/decoded, + /*include_hex=*/false, + /*txundo=*/nullptr, + /*verbosity=*/TxVerbosity::SHOW_DETAILS, + /*is_change_func=*/[&pwallet](const CTxOut& txout) EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet) { + AssertLockHeld(pwallet->cs_wallet); + return OutputIsChange(*pwallet, txout); + }); entry.pushKV("decoded", std::move(decoded)); } diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py index 4c2e90e08ef..324b48b22fd 100755 --- a/test/functional/wallet_basic.py +++ b/test/functional/wallet_basic.py @@ -542,13 +542,16 @@ class WalletTest(BitcoinTestFramework): destination = self.nodes[1].getnewaddress() txid = self.nodes[0].sendtoaddress(destination, 0.123) tx = self.nodes[0].gettransaction(txid=txid, verbose=True)['decoded'] - output_addresses = [vout['scriptPubKey']['address'] for vout in tx["vout"]] - assert len(output_addresses) > 1 - for address in output_addresses: + assert len(tx["vout"]) > 1 + for vout in tx["vout"]: + address = vout['scriptPubKey']['address'] ischange = self.nodes[0].getaddressinfo(address)['ischange'] assert_equal(ischange, address != destination) if ischange: change = address + assert vout["ischange"] + else: + assert "ischange" not in vout self.nodes[0].setlabel(change, 'foobar') assert_equal(self.nodes[0].getaddressinfo(change)['ischange'], False) From ad1c3bdba547685ca4163316017ab78e965c7ad1 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Thu, 15 May 2025 14:05:06 -0400 Subject: [PATCH 2/3] [move only] move DecodeTxDoc() to a common util file for sharing --- src/rpc/rawtransaction.cpp | 42 --------------------------------- src/rpc/rawtransaction_util.cpp | 42 +++++++++++++++++++++++++++++++++ src/rpc/rawtransaction_util.h | 4 ++++ 3 files changed, 46 insertions(+), 42 deletions(-) diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 2f1bd98b38c..28329000647 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -82,48 +82,6 @@ static void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue& } } -static std::vector DecodeTxDoc(const std::string& txid_field_doc) -{ - return { - {RPCResult::Type::STR_HEX, "txid", txid_field_doc}, - {RPCResult::Type::STR_HEX, "hash", "The transaction hash (differs from txid for witness transactions)"}, - {RPCResult::Type::NUM, "size", "The serialized transaction size"}, - {RPCResult::Type::NUM, "vsize", "The virtual transaction size (differs from size for witness transactions)"}, - {RPCResult::Type::NUM, "weight", "The transaction's weight (between vsize*4-3 and vsize*4)"}, - {RPCResult::Type::NUM, "version", "The version"}, - {RPCResult::Type::NUM_TIME, "locktime", "The lock time"}, - {RPCResult::Type::ARR, "vin", "", - { - {RPCResult::Type::OBJ, "", "", - { - {RPCResult::Type::STR_HEX, "coinbase", /*optional=*/true, "The coinbase value (only if coinbase transaction)"}, - {RPCResult::Type::STR_HEX, "txid", /*optional=*/true, "The transaction id (if not coinbase transaction)"}, - {RPCResult::Type::NUM, "vout", /*optional=*/true, "The output number (if not coinbase transaction)"}, - {RPCResult::Type::OBJ, "scriptSig", /*optional=*/true, "The script (if not coinbase transaction)", - { - {RPCResult::Type::STR, "asm", "Disassembly of the signature script"}, - {RPCResult::Type::STR_HEX, "hex", "The raw signature script bytes, hex-encoded"}, - }}, - {RPCResult::Type::ARR, "txinwitness", /*optional=*/true, "", - { - {RPCResult::Type::STR_HEX, "hex", "hex-encoded witness data (if any)"}, - }}, - {RPCResult::Type::NUM, "sequence", "The script sequence number"}, - }}, - }}, - {RPCResult::Type::ARR, "vout", "", - { - {RPCResult::Type::OBJ, "", "", - { - {RPCResult::Type::STR_AMOUNT, "value", "The value in " + CURRENCY_UNIT}, - {RPCResult::Type::NUM, "n", "index"}, - {RPCResult::Type::OBJ, "scriptPubKey", "", ScriptPubKeyDoc()}, - {RPCResult::Type::BOOL, "ischange", /*optional=*/true, "Output script is change (only if wallet transaction and true for selected rpcwallet)"}, - }}, - }}, - }; -} - static std::vector CreateTxDoc() { return { diff --git a/src/rpc/rawtransaction_util.cpp b/src/rpc/rawtransaction_util.cpp index 53f943bb9e0..075d8f31c59 100644 --- a/src/rpc/rawtransaction_util.cpp +++ b/src/rpc/rawtransaction_util.cpp @@ -334,3 +334,45 @@ void SignTransactionResultToJSON(CMutableTransaction& mtx, bool complete, const result.pushKV("errors", std::move(vErrors)); } } + +std::vector DecodeTxDoc(const std::string& txid_field_doc) +{ + return { + {RPCResult::Type::STR_HEX, "txid", txid_field_doc}, + {RPCResult::Type::STR_HEX, "hash", "The transaction hash (differs from txid for witness transactions)"}, + {RPCResult::Type::NUM, "size", "The serialized transaction size"}, + {RPCResult::Type::NUM, "vsize", "The virtual transaction size (differs from size for witness transactions)"}, + {RPCResult::Type::NUM, "weight", "The transaction's weight (between vsize*4-3 and vsize*4)"}, + {RPCResult::Type::NUM, "version", "The version"}, + {RPCResult::Type::NUM_TIME, "locktime", "The lock time"}, + {RPCResult::Type::ARR, "vin", "", + { + {RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR_HEX, "coinbase", /*optional=*/true, "The coinbase value (only if coinbase transaction)"}, + {RPCResult::Type::STR_HEX, "txid", /*optional=*/true, "The transaction id (if not coinbase transaction)"}, + {RPCResult::Type::NUM, "vout", /*optional=*/true, "The output number (if not coinbase transaction)"}, + {RPCResult::Type::OBJ, "scriptSig", /*optional=*/true, "The script (if not coinbase transaction)", + { + {RPCResult::Type::STR, "asm", "Disassembly of the signature script"}, + {RPCResult::Type::STR_HEX, "hex", "The raw signature script bytes, hex-encoded"}, + }}, + {RPCResult::Type::ARR, "txinwitness", /*optional=*/true, "", + { + {RPCResult::Type::STR_HEX, "hex", "hex-encoded witness data (if any)"}, + }}, + {RPCResult::Type::NUM, "sequence", "The script sequence number"}, + }}, + }}, + {RPCResult::Type::ARR, "vout", "", + { + {RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR_AMOUNT, "value", "The value in " + CURRENCY_UNIT}, + {RPCResult::Type::NUM, "n", "index"}, + {RPCResult::Type::OBJ, "scriptPubKey", "", ScriptPubKeyDoc()}, + {RPCResult::Type::BOOL, "ischange", /*optional=*/true, "Output script is change (only if wallet transaction and true for selected rpcwallet)"}, + }}, + }}, + }; +} diff --git a/src/rpc/rawtransaction_util.h b/src/rpc/rawtransaction_util.h index 40d6bbba873..1a673f98941 100644 --- a/src/rpc/rawtransaction_util.h +++ b/src/rpc/rawtransaction_util.h @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -55,4 +56,7 @@ void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in); /** Create a transaction from univalue parameters */ CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional rbf); +/** Explain the UniValue "decoded" transaction object **/ +std::vector DecodeTxDoc(const std::string& txid_field_doc); + #endif // BITCOIN_RPC_RAWTRANSACTION_UTIL_H From 060bb55508245776bb6a39c8b7849769ee588d69 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Thu, 15 May 2025 14:18:11 -0400 Subject: [PATCH 3/3] rpc: add decoded tx details to gettransaction with extra wallet fields --- src/rpc/rawtransaction.cpp | 4 ++-- src/rpc/rawtransaction_util.cpp | 20 ++++++++++++-------- src/rpc/rawtransaction_util.h | 4 ++-- src/wallet/rpc/transactions.cpp | 3 ++- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 28329000647..c82c9b6a7bb 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -242,7 +242,7 @@ static RPCHelpMan getrawtransaction() {RPCResult::Type::NUM, "time", /*optional=*/true, "Same as \"blocktime\""}, {RPCResult::Type::STR_HEX, "hex", "The serialized, hex-encoded data for 'txid'"}, }, - DecodeTxDoc(/*txid_field_doc=*/"The transaction id (same as provided)")), + DecodeTxDoc(/*txid_field_doc=*/"The transaction id (same as provided)", /*wallet=*/false)), }, RPCResult{"for verbosity = 2", RPCResult::Type::OBJ, "", "", @@ -415,7 +415,7 @@ static RPCHelpMan decoderawtransaction() }, RPCResult{ RPCResult::Type::OBJ, "", "", - DecodeTxDoc(/*txid_field_doc=*/"The transaction id"), + DecodeTxDoc(/*txid_field_doc=*/"The transaction id", /*wallet=*/false), }, RPCExamples{ HelpExampleCli("decoderawtransaction", "\"hexstring\"") diff --git a/src/rpc/rawtransaction_util.cpp b/src/rpc/rawtransaction_util.cpp index 075d8f31c59..a746c72cd47 100644 --- a/src/rpc/rawtransaction_util.cpp +++ b/src/rpc/rawtransaction_util.cpp @@ -335,7 +335,7 @@ void SignTransactionResultToJSON(CMutableTransaction& mtx, bool complete, const } } -std::vector DecodeTxDoc(const std::string& txid_field_doc) +std::vector DecodeTxDoc(const std::string& txid_field_doc, bool wallet) { return { {RPCResult::Type::STR_HEX, "txid", txid_field_doc}, @@ -366,13 +366,17 @@ std::vector DecodeTxDoc(const std::string& txid_field_doc) }}, {RPCResult::Type::ARR, "vout", "", { - {RPCResult::Type::OBJ, "", "", - { - {RPCResult::Type::STR_AMOUNT, "value", "The value in " + CURRENCY_UNIT}, - {RPCResult::Type::NUM, "n", "index"}, - {RPCResult::Type::OBJ, "scriptPubKey", "", ScriptPubKeyDoc()}, - {RPCResult::Type::BOOL, "ischange", /*optional=*/true, "Output script is change (only if wallet transaction and true for selected rpcwallet)"}, - }}, + {RPCResult::Type::OBJ, "", "", Cat( + { + {RPCResult::Type::STR_AMOUNT, "value", "The value in " + CURRENCY_UNIT}, + {RPCResult::Type::NUM, "n", "index"}, + {RPCResult::Type::OBJ, "scriptPubKey", "", ScriptPubKeyDoc()}, + }, + wallet ? + std::vector{{RPCResult::Type::BOOL, "ischange", /*optional=*/true, "Output script is change (only present if true)"}} : + std::vector{} + ) + }, }}, }; } diff --git a/src/rpc/rawtransaction_util.h b/src/rpc/rawtransaction_util.h index 1a673f98941..3d917489d67 100644 --- a/src/rpc/rawtransaction_util.h +++ b/src/rpc/rawtransaction_util.h @@ -56,7 +56,7 @@ void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in); /** Create a transaction from univalue parameters */ CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional rbf); -/** Explain the UniValue "decoded" transaction object **/ -std::vector DecodeTxDoc(const std::string& txid_field_doc); +/** Explain the UniValue "decoded" transaction object, may include extra fields if processed by wallet **/ +std::vector DecodeTxDoc(const std::string& txid_field_doc, bool wallet); #endif // BITCOIN_RPC_RAWTRANSACTION_UTIL_H diff --git a/src/wallet/rpc/transactions.cpp b/src/wallet/rpc/transactions.cpp index 36044afd3fe..4c2451b11bf 100644 --- a/src/wallet/rpc/transactions.cpp +++ b/src/wallet/rpc/transactions.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -738,7 +739,7 @@ RPCHelpMan gettransaction() {RPCResult::Type::STR_HEX, "hex", "Raw data for transaction"}, {RPCResult::Type::OBJ, "decoded", /*optional=*/true, "The decoded transaction (only present when `verbose` is passed)", { - {RPCResult::Type::ELISION, "", "Equivalent to the RPC decoderawtransaction method, or the RPC getrawtransaction method when `verbose` is passed."}, + DecodeTxDoc(/*txid_field_doc=*/"The transaction id", /*wallet=*/true), }}, RESULT_LAST_PROCESSED_BLOCK, })