Skip to content

Commit 9deba2d

Browse files
committed
Merge #20226: wallet, rpc: add listdescriptors command
647b81b wallet, rpc: add listdescriptors command (Ivan Metlushko) Pull request description: Looking for concept ACKs **Rationale**: allow users to inspect the contents of their newly created descriptor wallets. Currently the command only returns xpubs which is not very useful in itself, but there are multiples ways to extend it: * add an option to export xprv * with #19136 it'll be possible to return normalised descriptors suitable for a watch-only purposes The output is compatible with `importdescriptors` command so it could be easily used for backup/recover purposes. **Output example:** ```json [ { "desc": "wpkh(tpubD6NzVbkrYhZ4WW6E2ZETFyNfq2hfF23SKxqSGFvUpPAY58jmmuBybwqwFihAyQPk9KnwTt5516NDZRJ7k5QPeKjy7wuVd5WvXNxwwAs5tUD/*)#nhavpr5h", "timestamp": 1296688602, "active": false, "range": [ 0, 999 ], "next": 0 } ] ``` ACKs for top commit: jonatack: re-ACK 647b81b rebased to master, debug builds cleanly, reviewed diff since last review, tested with a descriptor wallet (and with a legacy wallet) achow101: re-ACK 647b81b Tree-SHA512: 51a3620bb17c836c52cecb066d4fa9d5ff418af56809046eaee0528c4dc240a4e90fff5711ba96e399c6664e00b9ee8194e33852b1b9e75af18061296e19a8a7
2 parents d1ddead + 647b81b commit 9deba2d

File tree

4 files changed

+139
-0
lines changed

4 files changed

+139
-0
lines changed

src/wallet/rpcdump.cpp

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1740,3 +1740,72 @@ RPCHelpMan importdescriptors()
17401740
},
17411741
};
17421742
}
1743+
1744+
RPCHelpMan listdescriptors()
1745+
{
1746+
return RPCHelpMan{
1747+
"listdescriptors",
1748+
"\nList descriptors imported into a descriptor-enabled wallet.",
1749+
{},
1750+
RPCResult{
1751+
RPCResult::Type::ARR, "", "Response is an array of descriptor objects",
1752+
{
1753+
{RPCResult::Type::OBJ, "", "", {
1754+
{RPCResult::Type::STR, "desc", "Descriptor string representation"},
1755+
{RPCResult::Type::NUM, "timestamp", "The creation time of the descriptor"},
1756+
{RPCResult::Type::BOOL, "active", "Activeness flag"},
1757+
{RPCResult::Type::BOOL, "internal", true, "Whether this is internal or external descriptor; defined only for active descriptors"},
1758+
{RPCResult::Type::ARR_FIXED, "range", true, "Defined only for ranged descriptors", {
1759+
{RPCResult::Type::NUM, "", "Range start inclusive"},
1760+
{RPCResult::Type::NUM, "", "Range end inclusive"},
1761+
}},
1762+
{RPCResult::Type::NUM, "next", true, "The next index to generate addresses from; defined only for ranged descriptors"},
1763+
}},
1764+
}
1765+
},
1766+
RPCExamples{
1767+
HelpExampleCli("listdescriptors", "") + HelpExampleRpc("listdescriptors", "")
1768+
},
1769+
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
1770+
{
1771+
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
1772+
if (!wallet) return NullUniValue;
1773+
1774+
if (!wallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
1775+
throw JSONRPCError(RPC_WALLET_ERROR, "listdescriptors is not available for non-descriptor wallets");
1776+
}
1777+
1778+
LOCK(wallet->cs_wallet);
1779+
1780+
UniValue response(UniValue::VARR);
1781+
const auto active_spk_mans = wallet->GetActiveScriptPubKeyMans();
1782+
for (const auto& spk_man : wallet->GetAllScriptPubKeyMans()) {
1783+
const auto desc_spk_man = dynamic_cast<DescriptorScriptPubKeyMan*>(spk_man);
1784+
if (!desc_spk_man) {
1785+
throw JSONRPCError(RPC_WALLET_ERROR, "Unexpected ScriptPubKey manager type.");
1786+
}
1787+
UniValue spk(UniValue::VOBJ);
1788+
LOCK(desc_spk_man->cs_desc_man);
1789+
const auto& wallet_descriptor = desc_spk_man->GetWalletDescriptor();
1790+
spk.pushKV("desc", wallet_descriptor.descriptor->ToString());
1791+
spk.pushKV("timestamp", wallet_descriptor.creation_time);
1792+
const bool active = active_spk_mans.count(desc_spk_man) != 0;
1793+
spk.pushKV("active", active);
1794+
const auto& type = wallet_descriptor.descriptor->GetOutputType();
1795+
if (active && type != nullopt) {
1796+
spk.pushKV("internal", wallet->GetScriptPubKeyMan(*type, true) == desc_spk_man);
1797+
}
1798+
if (wallet_descriptor.descriptor->IsRange()) {
1799+
UniValue range(UniValue::VARR);
1800+
range.push_back(wallet_descriptor.range_start);
1801+
range.push_back(wallet_descriptor.range_end - 1);
1802+
spk.pushKV("range", range);
1803+
spk.pushKV("next", wallet_descriptor.next_index);
1804+
}
1805+
response.push_back(spk);
1806+
}
1807+
1808+
return response;
1809+
},
1810+
};
1811+
}

src/wallet/rpcwallet.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4551,6 +4551,7 @@ RPCHelpMan importprunedfunds();
45514551
RPCHelpMan removeprunedfunds();
45524552
RPCHelpMan importmulti();
45534553
RPCHelpMan importdescriptors();
4554+
RPCHelpMan listdescriptors();
45544555

45554556
Span<const CRPCCommand> GetWalletRPCCommands()
45564557
{
@@ -4589,6 +4590,7 @@ static const CRPCCommand commands[] =
45894590
{ "wallet", "importwallet", &importwallet, {"filename"} },
45904591
{ "wallet", "keypoolrefill", &keypoolrefill, {"newsize"} },
45914592
{ "wallet", "listaddressgroupings", &listaddressgroupings, {} },
4593+
{ "wallet", "listdescriptors", &listdescriptors, {} },
45924594
{ "wallet", "listlabels", &listlabels, {"purpose"} },
45934595
{ "wallet", "listlockunspent", &listlockunspent, {} },
45944596
{ "wallet", "listreceivedbyaddress", &listreceivedbyaddress, {"minconf","include_empty","include_watchonly","address_filter"} },

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@
238238
'rpc_named_arguments.py',
239239
'wallet_listsinceblock.py',
240240
'wallet_listsinceblock.py --descriptors',
241+
'wallet_listdescriptors.py --descriptors',
241242
'p2p_leak.py',
242243
'wallet_encryption.py',
243244
'wallet_encryption.py --descriptors',
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2014-2021 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""Test the listdescriptors RPC."""
6+
7+
from test_framework.descriptors import (
8+
descsum_create
9+
)
10+
from test_framework.test_framework import BitcoinTestFramework
11+
from test_framework.util import (
12+
assert_equal,
13+
assert_raises_rpc_error,
14+
)
15+
16+
17+
class ListDescriptorsTest(BitcoinTestFramework):
18+
def set_test_params(self):
19+
self.num_nodes = 1
20+
21+
def skip_test_if_missing_module(self):
22+
self.skip_if_no_wallet()
23+
self.skip_if_no_sqlite()
24+
25+
# do not create any wallet by default
26+
def init_wallet(self, i):
27+
return
28+
29+
def run_test(self):
30+
node = self.nodes[0]
31+
assert_raises_rpc_error(-18, 'No wallet is loaded.', node.listdescriptors)
32+
33+
self.log.info('Test that the command is not available for legacy wallets.')
34+
node.createwallet(wallet_name='w1', descriptors=False)
35+
assert_raises_rpc_error(-4, 'listdescriptors is not available for non-descriptor wallets', node.listdescriptors)
36+
37+
self.log.info('Test the command for empty descriptors wallet.')
38+
node.createwallet(wallet_name='w2', blank=True, descriptors=True)
39+
assert_equal(0, len(node.get_wallet_rpc('w2').listdescriptors()))
40+
41+
self.log.info('Test the command for a default descriptors wallet.')
42+
node.createwallet(wallet_name='w3', descriptors=True)
43+
result = node.get_wallet_rpc('w3').listdescriptors()
44+
assert_equal(6, len(result))
45+
assert_equal(6, len([d for d in result if d['active']]))
46+
assert_equal(3, len([d for d in result if d['internal']]))
47+
for item in result:
48+
assert item['desc'] != ''
49+
assert item['next'] == 0
50+
assert item['range'] == [0, 0]
51+
assert item['timestamp'] is not None
52+
53+
self.log.info('Test non-active non-range combo descriptor')
54+
node.createwallet(wallet_name='w4', blank=True, descriptors=True)
55+
wallet = node.get_wallet_rpc('w4')
56+
wallet.importdescriptors([{
57+
'desc': descsum_create('combo(' + node.get_deterministic_priv_key().key + ')'),
58+
'timestamp': 1296688602,
59+
}])
60+
expected = [{'active': False,
61+
'desc': 'combo(0227d85ba011276cf25b51df6a188b75e604b38770a462b2d0e9fb2fc839ef5d3f)#np574htj',
62+
'timestamp': 1296688602}]
63+
assert_equal(expected, wallet.listdescriptors())
64+
65+
66+
if __name__ == '__main__':
67+
ListDescriptorsTest().main()

0 commit comments

Comments
 (0)