mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-01-19 05:45:05 +01:00
rpc: Return fee and prevout(s) to getrawtransaction
* Add optional fee response in BTC to getrawtransaction * Add optional prevout(s) response to getrawtransaction showing utxos being spent * Add getrawtransaction_verbosity functional test to validate fields
This commit is contained in:
parent
bfce05cc34
commit
f86697163e
@ -102,6 +102,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
||||
{ "getchaintxstats", 0, "nblocks" },
|
||||
{ "gettransaction", 1, "include_watchonly" },
|
||||
{ "gettransaction", 2, "verbose" },
|
||||
{ "getrawtransaction", 1, "verbosity" },
|
||||
{ "getrawtransaction", 1, "verbose" },
|
||||
{ "createrawtransaction", 0, "inputs" },
|
||||
{ "createrawtransaction", 1, "outputs" },
|
||||
|
@ -32,6 +32,7 @@
|
||||
#include <script/signingprovider.h>
|
||||
#include <script/standard.h>
|
||||
#include <uint256.h>
|
||||
#include <undo.h>
|
||||
#include <util/bip32.h>
|
||||
#include <util/check.h>
|
||||
#include <util/strencodings.h>
|
||||
@ -50,15 +51,17 @@ using node::FindCoins;
|
||||
using node::GetTransaction;
|
||||
using node::NodeContext;
|
||||
using node::PSBTAnalysis;
|
||||
using node::ReadBlockFromDisk;
|
||||
using node::UndoReadFromDisk;
|
||||
|
||||
static void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue& entry, Chainstate& active_chainstate)
|
||||
static void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue& entry, Chainstate& active_chainstate, const CTxUndo* txundo = nullptr, TxVerbosity verbosity = TxVerbosity::SHOW_TXID)
|
||||
{
|
||||
// Call into TxToUniv() in bitcoin-common to decode the transaction hex.
|
||||
//
|
||||
// Blockchain contextual information (confirmations and blocktime) is not
|
||||
// available to code in bitcoin-common, so we query them here and push the
|
||||
// data into the returned UniValue.
|
||||
TxToUniv(tx, /*block_hash=*/uint256(), entry, /*include_hex=*/true, RPCSerializationFlags());
|
||||
TxToUniv(tx, /*block_hash=*/uint256(), entry, /*include_hex=*/true, RPCSerializationFlags(), txundo, verbosity);
|
||||
|
||||
if (!hashBlock.IsNull()) {
|
||||
LOCK(cs_main);
|
||||
@ -166,26 +169,27 @@ static RPCHelpMan getrawtransaction()
|
||||
{
|
||||
return RPCHelpMan{
|
||||
"getrawtransaction",
|
||||
"Return the raw transaction data.\n"
|
||||
|
||||
"\nBy default, this call only returns a transaction if it is in the mempool. If -txindex is enabled\n"
|
||||
"By default, this call only returns a transaction if it is in the mempool. If -txindex is enabled\n"
|
||||
"and no blockhash argument is passed, it will return the transaction if it is in the mempool or any block.\n"
|
||||
"If a blockhash argument is passed, it will return the transaction if\n"
|
||||
"the specified block is available and the transaction is in that block.\n"
|
||||
"\nHint: Use gettransaction for wallet transactions.\n"
|
||||
"the specified block is available and the transaction is in that block.\n\n"
|
||||
"Hint: Use gettransaction for wallet transactions.\n\n"
|
||||
|
||||
"\nIf verbose is 'true', returns an Object with information about 'txid'.\n"
|
||||
"If verbose is 'false' or omitted, returns a string that is serialized, hex-encoded data for 'txid'.",
|
||||
"If verbosity is 0 or omitted, returns the serialized transaction as a hex-encoded string.\n"
|
||||
"If verbosity is 1, returns a JSON Object with information about transaction.\n"
|
||||
"If verbosity is 2, returns a JSON Object with information about transaction, including fee and prevout information.",
|
||||
{
|
||||
{"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"},
|
||||
{"verbose", RPCArg::Type::BOOL, RPCArg::Default{false}, "If false, return a string, otherwise return a json object"},
|
||||
{"verbosity|verbose", RPCArg::Type::NUM, RPCArg::Default{0}, "0 for hex-encoded data, 1 for a JSON object, and 2 for JSON object with fee and prevout"},
|
||||
{"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED_NAMED_ARG, "The block in which to look for the transaction"},
|
||||
},
|
||||
{
|
||||
RPCResult{"if verbose is not set or set to false",
|
||||
RPCResult::Type::STR, "data", "The serialized, hex-encoded data for 'txid'"
|
||||
RPCResult{"if verbosity is not set or set to 0",
|
||||
RPCResult::Type::STR, "data", "The serialized transaction as a hex-encoded string for 'txid'"
|
||||
},
|
||||
RPCResult{"if verbose is set to true",
|
||||
RPCResult{"if verbosity is set to 1",
|
||||
// When updating this documentation, update `decoderawtransaction` in the same way.
|
||||
RPCResult::Type::OBJ, "", "",
|
||||
Cat<std::vector<RPCResult>>(
|
||||
{
|
||||
@ -198,20 +202,47 @@ static RPCHelpMan getrawtransaction()
|
||||
},
|
||||
DecodeTxDoc(/*txid_field_doc=*/"The transaction id (same as provided)")),
|
||||
},
|
||||
RPCResult{"for verbosity = 2",
|
||||
RPCResult::Type::OBJ, "", "",
|
||||
{
|
||||
{RPCResult::Type::ELISION, "", "Same output as verbosity = 1"},
|
||||
{RPCResult::Type::NUM, "fee", /* optional */ true, "transaction fee in " + CURRENCY_UNIT + ", omitted if block undo data is not available"},
|
||||
{RPCResult::Type::ARR, "vin", "",
|
||||
{
|
||||
{RPCResult::Type::OBJ, "", /* optional */ true, "utxo being spent, omitted if block undo data is not available",
|
||||
{
|
||||
{RPCResult::Type::ELISION, "", "Same output as verbosity = 1"},
|
||||
{RPCResult::Type::OBJ, "prevout", "Only if undo information is available)",
|
||||
{
|
||||
{RPCResult::Type::BOOL, "generated", "Coinbase or not"},
|
||||
{RPCResult::Type::NUM, "height", "The height of the prevout"},
|
||||
{RPCResult::Type::STR_AMOUNT, "value", "The value in " + CURRENCY_UNIT},
|
||||
{RPCResult::Type::OBJ, "scriptPubKey", "",
|
||||
{
|
||||
{RPCResult::Type::STR, "asm", "Disassembly of the public key script"},
|
||||
{RPCResult::Type::STR, "desc", "Inferred descriptor for the output"},
|
||||
{RPCResult::Type::STR_HEX, "hex", "The raw public key script bytes, hex-encoded"},
|
||||
{RPCResult::Type::STR, "address", /*optional=*/true, "The Bitcoin address (only if a well-defined address exists)"},
|
||||
{RPCResult::Type::STR, "type", "The type (one of: " + GetAllOutputTypes() + ")"},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
RPCExamples{
|
||||
HelpExampleCli("getrawtransaction", "\"mytxid\"")
|
||||
+ HelpExampleCli("getrawtransaction", "\"mytxid\" true")
|
||||
+ HelpExampleRpc("getrawtransaction", "\"mytxid\", true")
|
||||
+ HelpExampleCli("getrawtransaction", "\"mytxid\" false \"myblockhash\"")
|
||||
+ HelpExampleCli("getrawtransaction", "\"mytxid\" true \"myblockhash\"")
|
||||
+ HelpExampleCli("getrawtransaction", "\"mytxid\" 1")
|
||||
+ HelpExampleRpc("getrawtransaction", "\"mytxid\", 1")
|
||||
+ HelpExampleCli("getrawtransaction", "\"mytxid\" 0 \"myblockhash\"")
|
||||
+ HelpExampleCli("getrawtransaction", "\"mytxid\" 1 \"myblockhash\"")
|
||||
+ HelpExampleCli("getrawtransaction", "\"mytxid\" 2 \"myblockhash\"")
|
||||
},
|
||||
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
||||
{
|
||||
const NodeContext& node = EnsureAnyNodeContext(request.context);
|
||||
ChainstateManager& chainman = EnsureChainman(node);
|
||||
|
||||
bool in_active_chain = true;
|
||||
uint256 hash = ParseHashV(request.params[0], "parameter 1");
|
||||
const CBlockIndex* blockindex = nullptr;
|
||||
|
||||
@ -220,10 +251,14 @@ static RPCHelpMan getrawtransaction()
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "The genesis block coinbase is not considered an ordinary transaction and cannot be retrieved");
|
||||
}
|
||||
|
||||
// Accept either a bool (true) or a num (>=1) to indicate verbose output.
|
||||
bool fVerbose = false;
|
||||
// Accept either a bool (true) or a num (>=0) to indicate verbosity.
|
||||
int verbosity{0};
|
||||
if (!request.params[1].isNull()) {
|
||||
fVerbose = request.params[1].isNum() ? (request.params[1].getInt<int>() != 0) : request.params[1].get_bool();
|
||||
if (request.params[1].isBool()) {
|
||||
verbosity = request.params[1].get_bool();
|
||||
} else {
|
||||
verbosity = request.params[1].getInt<int>();
|
||||
}
|
||||
}
|
||||
|
||||
if (!request.params[2].isNull()) {
|
||||
@ -234,7 +269,6 @@ static RPCHelpMan getrawtransaction()
|
||||
if (!blockindex) {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block hash not found");
|
||||
}
|
||||
in_active_chain = chainman.ActiveChain().Contains(blockindex);
|
||||
}
|
||||
|
||||
bool f_txindex_ready = false;
|
||||
@ -262,13 +296,43 @@ static RPCHelpMan getrawtransaction()
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, errmsg + ". Use gettransaction for wallet transactions.");
|
||||
}
|
||||
|
||||
if (!fVerbose) {
|
||||
if (verbosity <= 0) {
|
||||
return EncodeHexTx(*tx, RPCSerializationFlags());
|
||||
}
|
||||
|
||||
UniValue result(UniValue::VOBJ);
|
||||
if (blockindex) result.pushKV("in_active_chain", in_active_chain);
|
||||
TxToJSON(*tx, hash_block, result, chainman.ActiveChainstate());
|
||||
if (blockindex) {
|
||||
LOCK(cs_main);
|
||||
result.pushKV("in_active_chain", chainman.ActiveChain().Contains(blockindex));
|
||||
}
|
||||
// If request is verbosity >= 1 but no blockhash was given, then look up the blockindex
|
||||
if (request.params[2].isNull()) {
|
||||
LOCK(cs_main);
|
||||
blockindex = chainman.m_blockman.LookupBlockIndex(hash_block);
|
||||
}
|
||||
if (verbosity == 1) {
|
||||
TxToJSON(*tx, hash_block, result, chainman.ActiveChainstate());
|
||||
return result;
|
||||
}
|
||||
|
||||
CBlockUndo blockUndo;
|
||||
CBlock block;
|
||||
const bool is_block_pruned{WITH_LOCK(cs_main, return chainman.m_blockman.IsBlockPruned(blockindex))};
|
||||
|
||||
if (tx->IsCoinBase() ||
|
||||
!blockindex || is_block_pruned ||
|
||||
!(UndoReadFromDisk(blockUndo, blockindex) && ReadBlockFromDisk(block, blockindex, Params().GetConsensus()))) {
|
||||
TxToJSON(*tx, hash_block, result, chainman.ActiveChainstate());
|
||||
return result;
|
||||
}
|
||||
|
||||
CTxUndo* undoTX {nullptr};
|
||||
auto it = std::find_if(block.vtx.begin(), block.vtx.end(), [tx](CTransactionRef t){ return *t == *tx; });
|
||||
if (it != block.vtx.end()) {
|
||||
// -1 as blockundo does not have coinbase tx
|
||||
undoTX = &blockUndo.vtxundo.at(it - block.vtx.begin() - 1);
|
||||
}
|
||||
TxToJSON(*tx, hash_block, result, chainman.ActiveChainstate(), undoTX, TxVerbosity::SHOW_DETAILS_AND_PREVOUT);
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
@ -14,6 +14,7 @@ Test the following RPCs:
|
||||
|
||||
from collections import OrderedDict
|
||||
from decimal import Decimal
|
||||
from itertools import product
|
||||
|
||||
from test_framework.blocktools import COINBASE_MATURITY
|
||||
from test_framework.messages import (
|
||||
@ -80,6 +81,7 @@ class RawTransactionsTest(BitcoinTestFramework):
|
||||
self.generate(self.nodes[0], COINBASE_MATURITY + 1)
|
||||
|
||||
self.getrawtransaction_tests()
|
||||
self.getrawtransaction_verbosity_tests()
|
||||
self.createrawtransaction_tests()
|
||||
self.sendrawtransaction_tests()
|
||||
self.sendrawtransaction_testmempoolaccept_tests()
|
||||
@ -114,6 +116,7 @@ class RawTransactionsTest(BitcoinTestFramework):
|
||||
# 4. valid parameters - supply txid and 1 for verbose.
|
||||
# We only check the "hex" field of the output so we don't need to update this test every time the output format changes.
|
||||
assert_equal(self.nodes[n].getrawtransaction(txId, 1)["hex"], tx['hex'])
|
||||
assert_equal(self.nodes[n].getrawtransaction(txId, 2)["hex"], tx['hex'])
|
||||
|
||||
# 5. valid parameters - supply txid and True for non-verbose
|
||||
assert_equal(self.nodes[n].getrawtransaction(txId, True)["hex"], tx['hex'])
|
||||
@ -124,13 +127,14 @@ class RawTransactionsTest(BitcoinTestFramework):
|
||||
|
||||
# 6. invalid parameters - supply txid and invalid boolean values (strings) for verbose
|
||||
for value in ["True", "False"]:
|
||||
assert_raises_rpc_error(-3, "not of expected type bool", self.nodes[n].getrawtransaction, txid=txId, verbose=value)
|
||||
assert_raises_rpc_error(-3, "not of expected type number", self.nodes[n].getrawtransaction, txid=txId, verbose=value)
|
||||
assert_raises_rpc_error(-3, "not of expected type number", self.nodes[n].getrawtransaction, txid=txId, verbosity=value)
|
||||
|
||||
# 7. invalid parameters - supply txid and empty array
|
||||
assert_raises_rpc_error(-3, "not of expected type bool", self.nodes[n].getrawtransaction, txId, [])
|
||||
assert_raises_rpc_error(-3, "not of expected type number", self.nodes[n].getrawtransaction, txId, [])
|
||||
|
||||
# 8. invalid parameters - supply txid and empty dict
|
||||
assert_raises_rpc_error(-3, "not of expected type bool", self.nodes[n].getrawtransaction, txId, {})
|
||||
assert_raises_rpc_error(-3, "not of expected type number", self.nodes[n].getrawtransaction, txId, {})
|
||||
|
||||
# Make a tx by sending, then generate 2 blocks; block1 has the tx in it
|
||||
tx = self.wallet.send_self_transfer(from_node=self.nodes[2])['txid']
|
||||
@ -143,9 +147,10 @@ class RawTransactionsTest(BitcoinTestFramework):
|
||||
assert_equal(gottx['in_active_chain'], True)
|
||||
if n == 0:
|
||||
self.log.info("Test getrawtransaction with -txindex, without blockhash: 'in_active_chain' should be absent")
|
||||
gottx = self.nodes[n].getrawtransaction(txid=tx, verbose=True)
|
||||
assert_equal(gottx['txid'], tx)
|
||||
assert 'in_active_chain' not in gottx
|
||||
for v in [1,2]:
|
||||
gottx = self.nodes[n].getrawtransaction(txid=tx, verbosity=v)
|
||||
assert_equal(gottx['txid'], tx)
|
||||
assert 'in_active_chain' not in gottx
|
||||
else:
|
||||
self.log.info("Test getrawtransaction without -txindex, without blockhash: expect the call to raise")
|
||||
assert_raises_rpc_error(-5, err_msg, self.nodes[n].getrawtransaction, txid=tx, verbose=True)
|
||||
@ -170,6 +175,70 @@ class RawTransactionsTest(BitcoinTestFramework):
|
||||
block = self.nodes[0].getblock(self.nodes[0].getblockhash(0))
|
||||
assert_raises_rpc_error(-5, "The genesis block coinbase is not considered an ordinary transaction", self.nodes[0].getrawtransaction, block['merkleroot'])
|
||||
|
||||
def getrawtransaction_verbosity_tests(self):
|
||||
tx = self.wallet.send_self_transfer(from_node=self.nodes[1])['txid']
|
||||
[block1] = self.generate(self.nodes[1], 1)
|
||||
fields = [
|
||||
'blockhash',
|
||||
'blocktime',
|
||||
'confirmations',
|
||||
'hash',
|
||||
'hex',
|
||||
'in_active_chain',
|
||||
'locktime',
|
||||
'size',
|
||||
'time',
|
||||
'txid',
|
||||
'vin',
|
||||
'vout',
|
||||
'vsize',
|
||||
'weight',
|
||||
]
|
||||
prevout_fields = [
|
||||
'generated',
|
||||
'height',
|
||||
'value',
|
||||
'scriptPubKey',
|
||||
]
|
||||
script_pub_key_fields = [
|
||||
'address',
|
||||
'asm',
|
||||
'hex',
|
||||
'type',
|
||||
]
|
||||
# node 0 & 2 with verbosity 1 & 2
|
||||
for n, v in product([0, 2], [1, 2]):
|
||||
self.log.info(f"Test getrawtransaction_verbosity {v} {'with' if n == 0 else 'without'} -txindex, with blockhash")
|
||||
gottx = self.nodes[n].getrawtransaction(txid=tx, verbosity=v, blockhash=block1)
|
||||
missing_fields = set(fields).difference(gottx.keys())
|
||||
if missing_fields:
|
||||
raise AssertionError(f"fields {', '.join(missing_fields)} are not in transaction")
|
||||
|
||||
assert(len(gottx['vin']) > 0)
|
||||
if v == 1:
|
||||
assert('fee' not in gottx)
|
||||
assert('prevout' not in gottx['vin'][0])
|
||||
if v == 2:
|
||||
assert(isinstance(gottx['fee'], Decimal))
|
||||
assert('prevout' in gottx['vin'][0])
|
||||
prevout = gottx['vin'][0]['prevout']
|
||||
script_pub_key = prevout['scriptPubKey']
|
||||
|
||||
missing_fields = set(prevout_fields).difference(prevout.keys())
|
||||
if missing_fields:
|
||||
raise AssertionError(f"fields {', '.join(missing_fields)} are not in transaction")
|
||||
|
||||
missing_fields = set(script_pub_key_fields).difference(script_pub_key.keys())
|
||||
if missing_fields:
|
||||
raise AssertionError(f"fields {', '.join(missing_fields)} are not in transaction")
|
||||
|
||||
# check verbosity 2 without blockhash but with txindex
|
||||
assert('fee' in self.nodes[0].getrawtransaction(txid=tx, verbosity=2))
|
||||
# check that coinbase has no fee or does not throw any errors for verbosity 2
|
||||
coin_base = self.nodes[1].getblock(block1)['tx'][0]
|
||||
gottx = self.nodes[1].getrawtransaction(txid=coin_base, verbosity=2, blockhash=block1)
|
||||
assert('fee' not in gottx)
|
||||
|
||||
def createrawtransaction_tests(self):
|
||||
self.log.info("Test createrawtransaction")
|
||||
# Test `createrawtransaction` required parameters
|
||||
|
Loading…
Reference in New Issue
Block a user