Skip to content

Commit b724476

Browse files
committed
rpc: output wallet descriptors for received entries in listsinceblock
The descriptor wallets allow an application to track coins of multiple descriptors in a single wallet. However, such an application would not previously be able to (easily) tell what received coin "belongs" to what descriptor. This commit tackles this issues by adding a "wallet_desc" entry to the entries for received coins in 'listsinceblock'.
1 parent 55a82ea commit b724476

File tree

4 files changed

+63
-1
lines changed

4 files changed

+63
-1
lines changed

src/wallet/rpc/transactions.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nM
367367
entry.pushKV("involvesWatchonly", true);
368368
}
369369
MaybePushAddress(entry, r.destination);
370+
PushParentDescriptors(wallet, wtx.tx->vout.at(r.vout).scriptPubKey, entry);
370371
if (wtx.IsCoinBase())
371372
{
372373
if (wallet.GetTxDepthInMainChain(wtx) < 1)
@@ -418,7 +419,11 @@ static const std::vector<RPCResult> TransactionDescriptionString()
418419
{RPCResult::Type::NUM_TIME, "timereceived", "The time received expressed in " + UNIX_EPOCH_TIME + "."},
419420
{RPCResult::Type::STR, "comment", /*optional=*/true, "If a comment is associated with the transaction, only present if not empty."},
420421
{RPCResult::Type::STR, "bip125-replaceable", "(\"yes|no|unknown\") Whether this transaction could be replaced due to BIP125 (replace-by-fee);\n"
421-
"may be unknown for unconfirmed transactions not in the mempool."}};
422+
"may be unknown for unconfirmed transactions not in the mempool."},
423+
{RPCResult::Type::ARR, "parent_descs", /*optional=*/true, "Only if 'category' is 'received'. List of parent descriptors for the scriptPubKey of this coin.", {
424+
{RPCResult::Type::STR, "desc", "The descriptor string."},
425+
}},
426+
};
422427
}
423428

424429
RPCHelpMan listtransactions()
@@ -709,6 +714,9 @@ RPCHelpMan gettransaction()
709714
"'send' category of transactions."},
710715
{RPCResult::Type::BOOL, "abandoned", /*optional=*/true, "'true' if the transaction has been abandoned (inputs are respendable). Only available for the \n"
711716
"'send' category of transactions."},
717+
{RPCResult::Type::ARR, "parent_descs", /*optional=*/true, "Only if 'category' is 'received'. List of parent descriptors for the scriptPubKey of this coin.", {
718+
{RPCResult::Type::STR, "desc", "The descriptor string."},
719+
}},
712720
}},
713721
}},
714722
{RPCResult::Type::STR_HEX, "hex", "Raw data for transaction"},

src/wallet/rpc/util.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,15 @@ std::string LabelFromValue(const UniValue& value)
123123
return label;
124124
}
125125

126+
void PushParentDescriptors(const CWallet& wallet, const CScript& script_pubkey, UniValue& entry)
127+
{
128+
UniValue parent_descs(UniValue::VARR);
129+
for (const auto& desc: wallet.GetWalletDescriptors(script_pubkey)) {
130+
parent_descs.push_back(desc.descriptor->ToString());
131+
}
132+
entry.pushKV("parent_descs", parent_descs);
133+
}
134+
126135
void HandleWalletError(const std::shared_ptr<CWallet> wallet, DatabaseStatus& status, bilingual_str& error)
127136
{
128137
if (!wallet) {

src/wallet/rpc/util.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
#ifndef BITCOIN_WALLET_RPC_UTIL_H
66
#define BITCOIN_WALLET_RPC_UTIL_H
77

8+
#include <script/script.h>
9+
810
#include <any>
911
#include <memory>
1012
#include <string>
@@ -39,6 +41,8 @@ const LegacyScriptPubKeyMan& EnsureConstLegacyScriptPubKeyMan(const CWallet& wal
3941
bool GetAvoidReuseFlag(const CWallet& wallet, const UniValue& param);
4042
bool ParseIncludeWatchonly(const UniValue& include_watchonly, const CWallet& wallet);
4143
std::string LabelFromValue(const UniValue& value);
44+
//! Fetch parent descriptors of this scriptPubKey.
45+
void PushParentDescriptors(const CWallet& wallet, const CScript& script_pubkey, UniValue& entry);
4246

4347
void HandleWalletError(const std::shared_ptr<CWallet> wallet, DatabaseStatus& status, bilingual_str& error);
4448
} // namespace wallet

test/functional/wallet_listsinceblock.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from test_framework.address import key_to_p2wpkh
88
from test_framework.blocktools import COINBASE_MATURITY
9+
from test_framework.descriptors import descsum_create
910
from test_framework.key import ECKey
1011
from test_framework.test_framework import BitcoinTestFramework
1112
from test_framework.messages import MAX_BIP125_RBF_SEQUENCE
@@ -39,6 +40,7 @@ def run_test(self):
3940
self.test_double_send()
4041
self.double_spends_filtered()
4142
self.test_targetconfirmations()
43+
self.test_desc()
4244

4345
def test_no_blockhash(self):
4446
self.log.info("Test no blockhash")
@@ -383,5 +385,44 @@ def double_spends_filtered(self):
383385
assert_equal(original_found, False)
384386
assert_equal(double_found, False)
385387

388+
def test_desc(self):
389+
"""Make sure we can track coins by descriptor."""
390+
self.log.info("Test descriptor lookup by scriptPubKey.")
391+
392+
# Create a watchonly wallet tracking two multisig descriptors.
393+
multi_a = descsum_create("wsh(multi(1,tpubD6NzVbkrYhZ4YBNjUo96Jxd1u4XKWgnoc7LsA1jz3Yc2NiDbhtfBhaBtemB73n9V5vtJHwU6FVXwggTbeoJWQ1rzdz8ysDuQkpnaHyvnvzR/*,tpubD6NzVbkrYhZ4YHdDGMAYGaWxMSC1B6tPRTHuU5t3BcfcS3nrF523iFm5waFd1pP3ZvJt4Jr8XmCmsTBNx5suhcSgtzpGjGMASR3tau1hJz4/*))")
394+
multi_b = descsum_create("wsh(multi(1,tpubD6NzVbkrYhZ4YHdDGMAYGaWxMSC1B6tPRTHuU5t3BcfcS3nrF523iFm5waFd1pP3ZvJt4Jr8XmCmsTBNx5suhcSgtzpGjGMASR3tau1hJz4/*,tpubD6NzVbkrYhZ4Y2RLiuEzNQkntjmsLpPYDm3LTRBYynUQtDtpzeUKAcb9sYthSFL3YR74cdFgF5mW8yKxv2W2CWuZDFR2dUpE5PF9kbrVXNZ/*))")
395+
self.nodes[0].createwallet(wallet_name="wo", descriptors=True, disable_private_keys=True)
396+
wo_wallet = self.nodes[0].get_wallet_rpc("wo")
397+
wo_wallet.importdescriptors([
398+
{
399+
"desc": multi_a,
400+
"active": False,
401+
"timestamp": "now",
402+
},
403+
{
404+
"desc": multi_b,
405+
"active": False,
406+
"timestamp": "now",
407+
},
408+
])
409+
410+
# Send a coin to each descriptor.
411+
assert_equal(len(wo_wallet.listsinceblock()["transactions"]), 0)
412+
addr_a = self.nodes[0].deriveaddresses(multi_a, 0)[0]
413+
addr_b = self.nodes[0].deriveaddresses(multi_b, 0)[0]
414+
self.nodes[2].sendtoaddress(addr_a, 1)
415+
self.nodes[2].sendtoaddress(addr_b, 2)
416+
self.generate(self.nodes[2], 1)
417+
418+
# We can identify on which descriptor each coin was received.
419+
coins = wo_wallet.listsinceblock()["transactions"]
420+
assert_equal(len(coins), 2)
421+
coin_a = next(c for c in coins if c["amount"] == 1)
422+
assert_equal(coin_a["parent_descs"][0], multi_a)
423+
coin_b = next(c for c in coins if c["amount"] == 2)
424+
assert_equal(coin_b["parent_descs"][0], multi_b)
425+
426+
386427
if __name__ == '__main__':
387428
ListSinceBlockTest().main()

0 commit comments

Comments
 (0)