mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-01-09 11:27:28 -03:00
rpc: add getdescriptoractivity
This commit is contained in:
parent
25fe087de5
commit
811f76f3a5
7 changed files with 441 additions and 0 deletions
6
doc/release-notes-30708.md
Normal file
6
doc/release-notes-30708.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
New RPCs
|
||||||
|
--------
|
||||||
|
|
||||||
|
- `getdescriptoractivity` can be used to find all spend/receive activity relevant to
|
||||||
|
a given set of descriptors within a set of specified blocks. This call can be used with
|
||||||
|
`scanblocks` to lessen the need for additional indexing programs.
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
class CBlock;
|
class CBlock;
|
||||||
class CBlockHeader;
|
class CBlockHeader;
|
||||||
|
|
|
@ -55,9 +55,11 @@
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
|
#include <iterator>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
using kernel::CCoinsStats;
|
using kernel::CCoinsStats;
|
||||||
using kernel::CoinStatsHashType;
|
using kernel::CoinStatsHashType;
|
||||||
|
@ -2585,6 +2587,235 @@ static RPCHelpMan scanblocks()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static RPCHelpMan getdescriptoractivity()
|
||||||
|
{
|
||||||
|
return RPCHelpMan{"getdescriptoractivity",
|
||||||
|
"\nGet spend and receive activity associated with a set of descriptors for a set of blocks. "
|
||||||
|
"This command pairs well with the `relevant_blocks` output of `scanblocks()`.\n"
|
||||||
|
"This call may take several minutes. If you encounter timeouts, try specifying no RPC timeout (bitcoin-cli -rpcclienttimeout=0)",
|
||||||
|
{
|
||||||
|
RPCArg{"blockhashes", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "The list of blockhashes to examine for activity. Order doesn't matter. Must be along main chain or an error is thrown.\n", {
|
||||||
|
{"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "A valid blockhash"},
|
||||||
|
}},
|
||||||
|
scan_objects_arg_desc,
|
||||||
|
{"include_mempool", RPCArg::Type::BOOL, RPCArg::Default{true}, "Whether to include unconfirmed activity"},
|
||||||
|
},
|
||||||
|
RPCResult{
|
||||||
|
RPCResult::Type::OBJ, "", "", {
|
||||||
|
{RPCResult::Type::ARR, "activity", "events", {
|
||||||
|
{RPCResult::Type::OBJ, "", "", {
|
||||||
|
{RPCResult::Type::STR, "type", "always 'spend'"},
|
||||||
|
{RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " of the spent output"},
|
||||||
|
{RPCResult::Type::STR_HEX, "blockhash", /*optional=*/true, "The blockhash this spend appears in (omitted if unconfirmed)"},
|
||||||
|
{RPCResult::Type::NUM, "height", /*optional=*/true, "Height of the spend (omitted if unconfirmed)"},
|
||||||
|
{RPCResult::Type::STR_HEX, "spend_txid", "The txid of the spending transaction"},
|
||||||
|
{RPCResult::Type::NUM, "spend_vout", "The vout of the spend"},
|
||||||
|
{RPCResult::Type::STR_HEX, "prevout_txid", "The txid of the prevout"},
|
||||||
|
{RPCResult::Type::NUM, "prevout_vout", "The vout of the prevout"},
|
||||||
|
{RPCResult::Type::OBJ, "prevout_spk", "", ScriptPubKeyDoc()},
|
||||||
|
}},
|
||||||
|
{RPCResult::Type::OBJ, "", "", {
|
||||||
|
{RPCResult::Type::STR, "type", "always 'receive'"},
|
||||||
|
{RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " of the new output"},
|
||||||
|
{RPCResult::Type::STR_HEX, "blockhash", /*optional=*/true, "The block that this receive is in (omitted if unconfirmed)"},
|
||||||
|
{RPCResult::Type::NUM, "height", /*optional=*/true, "The height of the receive (omitted if unconfirmed)"},
|
||||||
|
{RPCResult::Type::STR_HEX, "txid", "The txid of the receiving transaction"},
|
||||||
|
{RPCResult::Type::NUM, "vout", "The vout of the receiving output"},
|
||||||
|
{RPCResult::Type::OBJ, "output_spk", "", ScriptPubKeyDoc()},
|
||||||
|
}},
|
||||||
|
// TODO is the skip_type_check avoidable with a heterogeneous ARR?
|
||||||
|
}, /*skip_type_check=*/true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RPCExamples{
|
||||||
|
HelpExampleCli("getdescriptoractivity", "'[\"000000000000000000001347062c12fded7c528943c8ce133987e2e2f5a840ee\"]' '[\"addr(bc1qzl6nsgqzu89a66l50cvwapnkw5shh23zarqkw9)\"]'")
|
||||||
|
},
|
||||||
|
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
||||||
|
{
|
||||||
|
UniValue ret(UniValue::VOBJ);
|
||||||
|
UniValue activity(UniValue::VARR);
|
||||||
|
NodeContext& node = EnsureAnyNodeContext(request.context);
|
||||||
|
ChainstateManager& chainman = EnsureChainman(node);
|
||||||
|
|
||||||
|
struct CompareByHeightAscending {
|
||||||
|
bool operator()(const CBlockIndex* a, const CBlockIndex* b) const {
|
||||||
|
return a->nHeight < b->nHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::set<const CBlockIndex*, CompareByHeightAscending> blockindexes_sorted;
|
||||||
|
|
||||||
|
{
|
||||||
|
// Validate all given blockhashes, and ensure blocks are along a single chain.
|
||||||
|
LOCK(::cs_main);
|
||||||
|
for (const UniValue& blockhash : request.params[0].get_array().getValues()) {
|
||||||
|
uint256 bhash = ParseHashV(blockhash, "blockhash");
|
||||||
|
CBlockIndex* pindex = chainman.m_blockman.LookupBlockIndex(bhash);
|
||||||
|
if (!pindex) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
|
||||||
|
}
|
||||||
|
if (!chainman.ActiveChain().Contains(pindex)) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Block is not in main chain");
|
||||||
|
}
|
||||||
|
blockindexes_sorted.insert(pindex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<CScript> scripts_to_watch;
|
||||||
|
|
||||||
|
// Determine scripts to watch.
|
||||||
|
for (const UniValue& scanobject : request.params[1].get_array().getValues()) {
|
||||||
|
FlatSigningProvider provider;
|
||||||
|
std::vector<CScript> scripts = EvalDescriptorStringOrObject(scanobject, provider);
|
||||||
|
|
||||||
|
for (const CScript& script : scripts) {
|
||||||
|
scripts_to_watch.insert(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto AddSpend = [&](
|
||||||
|
const CScript& spk,
|
||||||
|
const CAmount val,
|
||||||
|
const CTransactionRef& tx,
|
||||||
|
int vin,
|
||||||
|
const CTxIn& txin,
|
||||||
|
const CBlockIndex* index
|
||||||
|
) {
|
||||||
|
UniValue event(UniValue::VOBJ);
|
||||||
|
UniValue spkUv(UniValue::VOBJ);
|
||||||
|
ScriptToUniv(spk, /*out=*/spkUv, /*include_hex=*/true, /*include_address=*/true);
|
||||||
|
|
||||||
|
event.pushKV("type", "spend");
|
||||||
|
event.pushKV("amount", ValueFromAmount(val));
|
||||||
|
if (index) {
|
||||||
|
event.pushKV("blockhash", index->GetBlockHash().ToString());
|
||||||
|
event.pushKV("height", index->nHeight);
|
||||||
|
}
|
||||||
|
event.pushKV("spend_txid", tx->GetHash().ToString());
|
||||||
|
event.pushKV("spend_vin", vin);
|
||||||
|
event.pushKV("prevout_txid", txin.prevout.hash.ToString());
|
||||||
|
event.pushKV("prevout_vout", txin.prevout.n);
|
||||||
|
event.pushKV("prevout_spk", spkUv);
|
||||||
|
|
||||||
|
return event;
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto AddReceive = [&](const CTxOut& txout, const CBlockIndex* index, int vout, const CTransactionRef& tx) {
|
||||||
|
UniValue event(UniValue::VOBJ);
|
||||||
|
UniValue spkUv(UniValue::VOBJ);
|
||||||
|
ScriptToUniv(txout.scriptPubKey, /*out=*/spkUv, /*include_hex=*/true, /*include_address=*/true);
|
||||||
|
|
||||||
|
event.pushKV("type", "receive");
|
||||||
|
event.pushKV("amount", ValueFromAmount(txout.nValue));
|
||||||
|
if (index) {
|
||||||
|
event.pushKV("blockhash", index->GetBlockHash().ToString());
|
||||||
|
event.pushKV("height", index->nHeight);
|
||||||
|
}
|
||||||
|
event.pushKV("txid", tx->GetHash().ToString());
|
||||||
|
event.pushKV("vout", vout);
|
||||||
|
event.pushKV("output_spk", spkUv);
|
||||||
|
|
||||||
|
return event;
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockManager* blockman;
|
||||||
|
Chainstate& active_chainstate = chainman.ActiveChainstate();
|
||||||
|
{
|
||||||
|
LOCK(::cs_main);
|
||||||
|
blockman = CHECK_NONFATAL(&active_chainstate.m_blockman);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const CBlockIndex* blockindex : blockindexes_sorted) {
|
||||||
|
const CBlock block{GetBlockChecked(chainman.m_blockman, *blockindex)};
|
||||||
|
const CBlockUndo block_undo{GetUndoChecked(*blockman, *blockindex)};
|
||||||
|
|
||||||
|
for (size_t i = 0; i < block.vtx.size(); ++i) {
|
||||||
|
const auto& tx = block.vtx.at(i);
|
||||||
|
|
||||||
|
if (!tx->IsCoinBase()) {
|
||||||
|
// skip coinbase; spends can't happen there.
|
||||||
|
const auto& txundo = block_undo.vtxundo.at(i - 1);
|
||||||
|
|
||||||
|
for (size_t vin_idx = 0; vin_idx < tx->vin.size(); ++vin_idx) {
|
||||||
|
const auto& coin = txundo.vprevout.at(vin_idx);
|
||||||
|
const auto& txin = tx->vin.at(vin_idx);
|
||||||
|
if (scripts_to_watch.contains(coin.out.scriptPubKey)) {
|
||||||
|
activity.push_back(AddSpend(
|
||||||
|
coin.out.scriptPubKey, coin.out.nValue, tx, vin_idx, txin, blockindex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t vout_idx = 0; vout_idx < tx->vout.size(); ++vout_idx) {
|
||||||
|
const auto& vout = tx->vout.at(vout_idx);
|
||||||
|
if (scripts_to_watch.contains(vout.scriptPubKey)) {
|
||||||
|
activity.push_back(AddReceive(vout, blockindex, vout_idx, tx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool search_mempool = true;
|
||||||
|
if (!request.params[2].isNull()) {
|
||||||
|
search_mempool = request.params[2].get_bool();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search_mempool) {
|
||||||
|
const CTxMemPool& mempool = EnsureMemPool(node);
|
||||||
|
LOCK(::cs_main);
|
||||||
|
LOCK(mempool.cs);
|
||||||
|
const CCoinsViewCache& coins_view = &active_chainstate.CoinsTip();
|
||||||
|
|
||||||
|
for (const CTxMemPoolEntry& e : mempool.entryAll()) {
|
||||||
|
const auto& tx = e.GetSharedTx();
|
||||||
|
|
||||||
|
for (size_t vin_idx = 0; vin_idx < tx->vin.size(); ++vin_idx) {
|
||||||
|
CScript scriptPubKey;
|
||||||
|
CAmount value;
|
||||||
|
const auto& txin = tx->vin.at(vin_idx);
|
||||||
|
std::optional<Coin> coin = coins_view.GetCoin(txin.prevout);
|
||||||
|
|
||||||
|
// Check if the previous output is in the chain
|
||||||
|
if (!coin) {
|
||||||
|
// If not found in the chain, check the mempool. Likely, this is a
|
||||||
|
// child transaction of another transaction in the mempool.
|
||||||
|
CTransactionRef prev_tx = CHECK_NONFATAL(mempool.get(txin.prevout.hash));
|
||||||
|
|
||||||
|
if (txin.prevout.n >= prev_tx->vout.size()) {
|
||||||
|
throw std::runtime_error("Invalid output index");
|
||||||
|
}
|
||||||
|
const CTxOut& out = prev_tx->vout[txin.prevout.n];
|
||||||
|
scriptPubKey = out.scriptPubKey;
|
||||||
|
value = out.nValue;
|
||||||
|
} else {
|
||||||
|
// Coin found in the chain
|
||||||
|
const CTxOut& out = coin->out;
|
||||||
|
scriptPubKey = out.scriptPubKey;
|
||||||
|
value = out.nValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scripts_to_watch.contains(scriptPubKey)) {
|
||||||
|
UniValue event(UniValue::VOBJ);
|
||||||
|
activity.push_back(AddSpend(
|
||||||
|
scriptPubKey, value, tx, vin_idx, txin, nullptr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t vout_idx = 0; vout_idx < tx->vout.size(); ++vout_idx) {
|
||||||
|
const auto& vout = tx->vout.at(vout_idx);
|
||||||
|
if (scripts_to_watch.contains(vout.scriptPubKey)) {
|
||||||
|
activity.push_back(AddReceive(vout, nullptr, vout_idx, tx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.pushKV("activity", activity);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
static RPCHelpMan getblockfilter()
|
static RPCHelpMan getblockfilter()
|
||||||
{
|
{
|
||||||
return RPCHelpMan{"getblockfilter",
|
return RPCHelpMan{"getblockfilter",
|
||||||
|
@ -3152,6 +3383,7 @@ void RegisterBlockchainRPCCommands(CRPCTable& t)
|
||||||
{"blockchain", &preciousblock},
|
{"blockchain", &preciousblock},
|
||||||
{"blockchain", &scantxoutset},
|
{"blockchain", &scantxoutset},
|
||||||
{"blockchain", &scanblocks},
|
{"blockchain", &scanblocks},
|
||||||
|
{"blockchain", &getdescriptoractivity},
|
||||||
{"blockchain", &getblockfilter},
|
{"blockchain", &getblockfilter},
|
||||||
{"blockchain", &dumptxoutset},
|
{"blockchain", &dumptxoutset},
|
||||||
{"blockchain", &loadtxoutset},
|
{"blockchain", &loadtxoutset},
|
||||||
|
|
|
@ -92,6 +92,9 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
||||||
{ "scanblocks", 3, "stop_height" },
|
{ "scanblocks", 3, "stop_height" },
|
||||||
{ "scanblocks", 5, "options" },
|
{ "scanblocks", 5, "options" },
|
||||||
{ "scanblocks", 5, "filter_false_positives" },
|
{ "scanblocks", 5, "filter_false_positives" },
|
||||||
|
{ "getdescriptoractivity", 0, "blockhashes" },
|
||||||
|
{ "getdescriptoractivity", 1, "scanobjects" },
|
||||||
|
{ "getdescriptoractivity", 2, "include_mempool" },
|
||||||
{ "scantxoutset", 1, "scanobjects" },
|
{ "scantxoutset", 1, "scanobjects" },
|
||||||
{ "addmultisigaddress", 0, "nrequired" },
|
{ "addmultisigaddress", 0, "nrequired" },
|
||||||
{ "addmultisigaddress", 1, "keys" },
|
{ "addmultisigaddress", 1, "keys" },
|
||||||
|
|
|
@ -130,6 +130,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
|
||||||
"getchaintxstats",
|
"getchaintxstats",
|
||||||
"getconnectioncount",
|
"getconnectioncount",
|
||||||
"getdeploymentinfo",
|
"getdeploymentinfo",
|
||||||
|
"getdescriptoractivity",
|
||||||
"getdescriptorinfo",
|
"getdescriptorinfo",
|
||||||
"getdifficulty",
|
"getdifficulty",
|
||||||
"getindexinfo",
|
"getindexinfo",
|
||||||
|
|
197
test/functional/rpc_getdescriptoractivity.py
Executable file
197
test/functional/rpc_getdescriptoractivity.py
Executable file
|
@ -0,0 +1,197 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) 2024-present The Bitcoin Core developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from test_framework.test_framework import BitcoinTestFramework
|
||||||
|
from test_framework.util import assert_equal, assert_raises_rpc_error
|
||||||
|
from test_framework.messages import COIN
|
||||||
|
from test_framework.wallet import MiniWallet, getnewdestination
|
||||||
|
|
||||||
|
|
||||||
|
class GetBlocksActivityTest(BitcoinTestFramework):
|
||||||
|
def set_test_params(self):
|
||||||
|
self.num_nodes = 1
|
||||||
|
self.setup_clean_chain = True
|
||||||
|
|
||||||
|
def run_test(self):
|
||||||
|
node = self.nodes[0]
|
||||||
|
wallet = MiniWallet(node)
|
||||||
|
node.setmocktime(node.getblockheader(node.getbestblockhash())['time'])
|
||||||
|
wallet.generate(200, invalid_call=False)
|
||||||
|
|
||||||
|
self.test_no_activity(node)
|
||||||
|
self.test_activity_in_block(node, wallet)
|
||||||
|
self.test_no_mempool_inclusion(node, wallet)
|
||||||
|
self.test_multiple_addresses(node, wallet)
|
||||||
|
self.test_invalid_blockhash(node, wallet)
|
||||||
|
self.test_invalid_descriptor(node, wallet)
|
||||||
|
self.test_confirmed_and_unconfirmed(node, wallet)
|
||||||
|
self.test_receive_then_spend(node, wallet)
|
||||||
|
|
||||||
|
def test_no_activity(self, node):
|
||||||
|
_, _, addr_1 = getnewdestination()
|
||||||
|
result = node.getdescriptoractivity([], [f"addr({addr_1})"], True)
|
||||||
|
assert_equal(len(result['activity']), 0)
|
||||||
|
|
||||||
|
def test_activity_in_block(self, node, wallet):
|
||||||
|
_, spk_1, addr_1 = getnewdestination(address_type='bech32m')
|
||||||
|
txid = wallet.send_to(from_node=node, scriptPubKey=spk_1, amount=1 * COIN)['txid']
|
||||||
|
blockhash = self.generate(node, 1)[0]
|
||||||
|
|
||||||
|
# Test getdescriptoractivity with the specific blockhash
|
||||||
|
result = node.getdescriptoractivity([blockhash], [f"addr({addr_1})"], True)
|
||||||
|
assert_equal(list(result.keys()), ['activity'])
|
||||||
|
[activity] = result['activity']
|
||||||
|
|
||||||
|
for k, v in {
|
||||||
|
'amount': Decimal('1.00000000'),
|
||||||
|
'blockhash': blockhash,
|
||||||
|
'height': 201,
|
||||||
|
'txid': txid,
|
||||||
|
'type': 'receive',
|
||||||
|
'vout': 1,
|
||||||
|
}.items():
|
||||||
|
assert_equal(activity[k], v)
|
||||||
|
|
||||||
|
outspk = activity['output_spk']
|
||||||
|
|
||||||
|
assert_equal(outspk['asm'][:2], '1 ')
|
||||||
|
assert_equal(outspk['desc'].split('(')[0], 'rawtr')
|
||||||
|
assert_equal(outspk['hex'], spk_1.hex())
|
||||||
|
assert_equal(outspk['address'], addr_1)
|
||||||
|
assert_equal(outspk['type'], 'witness_v1_taproot')
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_mempool_inclusion(self, node, wallet):
|
||||||
|
_, spk_1, addr_1 = getnewdestination()
|
||||||
|
wallet.send_to(from_node=node, scriptPubKey=spk_1, amount=1 * COIN)
|
||||||
|
|
||||||
|
_, spk_2, addr_2 = getnewdestination()
|
||||||
|
wallet.send_to(
|
||||||
|
from_node=node, scriptPubKey=spk_2, amount=1 * COIN)
|
||||||
|
|
||||||
|
# Do not generate a block to keep the transaction in the mempool
|
||||||
|
|
||||||
|
result = node.getdescriptoractivity([], [f"addr({addr_1})", f"addr({addr_2})"], False)
|
||||||
|
|
||||||
|
assert_equal(len(result['activity']), 0)
|
||||||
|
|
||||||
|
def test_multiple_addresses(self, node, wallet):
|
||||||
|
_, spk_1, addr_1 = getnewdestination()
|
||||||
|
_, spk_2, addr_2 = getnewdestination()
|
||||||
|
wallet.send_to(from_node=node, scriptPubKey=spk_1, amount=1 * COIN)
|
||||||
|
wallet.send_to(from_node=node, scriptPubKey=spk_2, amount=2 * COIN)
|
||||||
|
|
||||||
|
blockhash = self.generate(node, 1)[0]
|
||||||
|
|
||||||
|
result = node.getdescriptoractivity([blockhash], [f"addr({addr_1})", f"addr({addr_2})"], True)
|
||||||
|
|
||||||
|
assert_equal(len(result['activity']), 2)
|
||||||
|
|
||||||
|
# Duplicate address specification is fine.
|
||||||
|
assert_equal(
|
||||||
|
result,
|
||||||
|
node.getdescriptoractivity([blockhash], [
|
||||||
|
f"addr({addr_1})", f"addr({addr_1})", f"addr({addr_2})"], True))
|
||||||
|
|
||||||
|
# Flipping descriptor order doesn't affect results.
|
||||||
|
result_flipped = node.getdescriptoractivity(
|
||||||
|
[blockhash], [f"addr({addr_2})", f"addr({addr_1})"], True)
|
||||||
|
assert_equal(result, result_flipped)
|
||||||
|
|
||||||
|
[a1] = [a for a in result['activity'] if a['output_spk']['address'] == addr_1]
|
||||||
|
[a2] = [a for a in result['activity'] if a['output_spk']['address'] == addr_2]
|
||||||
|
|
||||||
|
assert a1['blockhash'] == blockhash
|
||||||
|
assert a1['amount'] == 1.0
|
||||||
|
|
||||||
|
assert a2['blockhash'] == blockhash
|
||||||
|
assert a2['amount'] == 2.0
|
||||||
|
|
||||||
|
def test_invalid_blockhash(self, node, wallet):
|
||||||
|
self.generate(node, 20) # Generate to get more fees
|
||||||
|
|
||||||
|
_, spk_1, addr_1 = getnewdestination()
|
||||||
|
wallet.send_to(from_node=node, scriptPubKey=spk_1, amount=1 * COIN)
|
||||||
|
|
||||||
|
invalid_blockhash = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
|
|
||||||
|
assert_raises_rpc_error(
|
||||||
|
-5, "Block not found",
|
||||||
|
node.getdescriptoractivity, [invalid_blockhash], [f"addr({addr_1})"], True)
|
||||||
|
|
||||||
|
def test_invalid_descriptor(self, node, wallet):
|
||||||
|
blockhash = self.generate(node, 1)[0]
|
||||||
|
_, _, addr_1 = getnewdestination()
|
||||||
|
|
||||||
|
assert_raises_rpc_error(
|
||||||
|
-5, "is not a valid descriptor",
|
||||||
|
node.getdescriptoractivity, [blockhash], [f"addrx({addr_1})"], True)
|
||||||
|
|
||||||
|
def test_confirmed_and_unconfirmed(self, node, wallet):
|
||||||
|
self.generate(node, 20) # Generate to get more fees
|
||||||
|
|
||||||
|
_, spk_1, addr_1 = getnewdestination()
|
||||||
|
txid_1 = wallet.send_to(
|
||||||
|
from_node=node, scriptPubKey=spk_1, amount=1 * COIN)['txid']
|
||||||
|
blockhash = self.generate(node, 1)[0]
|
||||||
|
|
||||||
|
_, spk_2, to_addr = getnewdestination()
|
||||||
|
txid_2 = wallet.send_to(
|
||||||
|
from_node=node, scriptPubKey=spk_2, amount=1 * COIN)['txid']
|
||||||
|
|
||||||
|
result = node.getdescriptoractivity(
|
||||||
|
[blockhash], [f"addr({addr_1})", f"addr({to_addr})"], True)
|
||||||
|
|
||||||
|
activity = result['activity']
|
||||||
|
assert_equal(len(activity), 2)
|
||||||
|
|
||||||
|
[confirmed] = [a for a in activity if a.get('blockhash') == blockhash]
|
||||||
|
assert confirmed['txid'] == txid_1
|
||||||
|
assert confirmed['height'] == node.getblockchaininfo()['blocks']
|
||||||
|
|
||||||
|
[unconfirmed] = [a for a in activity if not a.get('blockhash')]
|
||||||
|
assert 'blockhash' not in unconfirmed
|
||||||
|
assert 'height' not in unconfirmed
|
||||||
|
|
||||||
|
assert any(a['txid'] == txid_2 for a in activity if not a.get('blockhash'))
|
||||||
|
|
||||||
|
def test_receive_then_spend(self, node, wallet):
|
||||||
|
"""Also important because this tests multiple blockhashes."""
|
||||||
|
self.generate(node, 20) # Generate to get more fees
|
||||||
|
|
||||||
|
sent1 = wallet.send_self_transfer(from_node=node)
|
||||||
|
utxo = sent1['new_utxo']
|
||||||
|
blockhash_1 = self.generate(node, 1)[0]
|
||||||
|
|
||||||
|
sent2 = wallet.send_self_transfer(from_node=node, utxo_to_spend=utxo)
|
||||||
|
blockhash_2 = self.generate(node, 1)[0]
|
||||||
|
|
||||||
|
result = node.getdescriptoractivity(
|
||||||
|
[blockhash_1, blockhash_2], [wallet.get_descriptor()], True)
|
||||||
|
|
||||||
|
assert_equal(len(result['activity']), 4)
|
||||||
|
|
||||||
|
assert result['activity'][1]['type'] == 'receive'
|
||||||
|
assert result['activity'][1]['txid'] == sent1['txid']
|
||||||
|
assert result['activity'][1]['blockhash'] == blockhash_1
|
||||||
|
|
||||||
|
assert result['activity'][2]['type'] == 'spend'
|
||||||
|
assert result['activity'][2]['spend_txid'] == sent2['txid']
|
||||||
|
assert result['activity'][2]['prevout_txid'] == sent1['txid']
|
||||||
|
assert result['activity'][2]['blockhash'] == blockhash_2
|
||||||
|
|
||||||
|
# Test that reversing the blockorder yields the same result.
|
||||||
|
assert_equal(result, node.getdescriptoractivity(
|
||||||
|
[blockhash_1, blockhash_2], [wallet.get_descriptor()], True))
|
||||||
|
|
||||||
|
# Test that duplicating a blockhash yields the same result.
|
||||||
|
assert_equal(result, node.getdescriptoractivity(
|
||||||
|
[blockhash_1, blockhash_2, blockhash_2], [wallet.get_descriptor()], True))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
GetBlocksActivityTest(__file__).main()
|
|
@ -378,6 +378,7 @@ BASE_SCRIPTS = [
|
||||||
'rpc_deriveaddresses.py --usecli',
|
'rpc_deriveaddresses.py --usecli',
|
||||||
'p2p_ping.py',
|
'p2p_ping.py',
|
||||||
'p2p_tx_privacy.py',
|
'p2p_tx_privacy.py',
|
||||||
|
'rpc_getdescriptoractivity.py',
|
||||||
'rpc_scanblocks.py',
|
'rpc_scanblocks.py',
|
||||||
'p2p_sendtxrcncl.py',
|
'p2p_sendtxrcncl.py',
|
||||||
'rpc_scantxoutset.py',
|
'rpc_scantxoutset.py',
|
||||||
|
|
Loading…
Reference in a new issue