Skip to content

Commit

Permalink
Merge bitcoin#20226: wallet, rpc: add listdescriptors command
Browse files Browse the repository at this point in the history
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 bitcoin#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
  • Loading branch information
meshcollider authored and knst committed Mar 8, 2024
1 parent 59c30e6 commit 56d9fe9
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 0 deletions.
69 changes: 69 additions & 0 deletions src/wallet/rpcdump.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1908,3 +1908,72 @@ UniValue importdescriptors(const JSONRPCRequest& main_request) {

return response;
}

RPCHelpMan listdescriptors()
{
return RPCHelpMan{
"listdescriptors",
"\nList descriptors imported into a descriptor-enabled wallet.",
{},
RPCResult{
RPCResult::Type::ARR, "", "Response is an array of descriptor objects",
{
{RPCResult::Type::OBJ, "", "", {
{RPCResult::Type::STR, "desc", "Descriptor string representation"},
{RPCResult::Type::NUM, "timestamp", "The creation time of the descriptor"},
{RPCResult::Type::BOOL, "active", "Activeness flag"},
{RPCResult::Type::BOOL, "internal", true, "Whether this is internal or external descriptor; defined only for active descriptors"},
{RPCResult::Type::ARR_FIXED, "range", true, "Defined only for ranged descriptors", {
{RPCResult::Type::NUM, "", "Range start inclusive"},
{RPCResult::Type::NUM, "", "Range end inclusive"},
}},
{RPCResult::Type::NUM, "next", true, "The next index to generate addresses from; defined only for ranged descriptors"},
}},
}
},
RPCExamples{
HelpExampleCli("listdescriptors", "") + HelpExampleRpc("listdescriptors", "")
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
if (!wallet) return NullUniValue;

if (!wallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
throw JSONRPCError(RPC_WALLET_ERROR, "listdescriptors is not available for non-descriptor wallets");
}

LOCK(wallet->cs_wallet);

UniValue response(UniValue::VARR);
const auto active_spk_mans = wallet->GetActiveScriptPubKeyMans();
for (const auto& spk_man : wallet->GetAllScriptPubKeyMans()) {
const auto desc_spk_man = dynamic_cast<DescriptorScriptPubKeyMan*>(spk_man);
if (!desc_spk_man) {
throw JSONRPCError(RPC_WALLET_ERROR, "Unexpected ScriptPubKey manager type.");
}
UniValue spk(UniValue::VOBJ);
LOCK(desc_spk_man->cs_desc_man);
const auto& wallet_descriptor = desc_spk_man->GetWalletDescriptor();
spk.pushKV("desc", wallet_descriptor.descriptor->ToString());
spk.pushKV("timestamp", wallet_descriptor.creation_time);
const bool active = active_spk_mans.count(desc_spk_man) != 0;
spk.pushKV("active", active);
const auto& type = wallet_descriptor.descriptor->GetOutputType();
if (active && type != std::nullopt) {
spk.pushKV("internal", wallet->GetScriptPubKeyMan(true) == desc_spk_man);
}
if (wallet_descriptor.descriptor->IsRange()) {
UniValue range(UniValue::VARR);
range.push_back(wallet_descriptor.range_start);
range.push_back(wallet_descriptor.range_end - 1);
spk.pushKV("range", range);
spk.pushKV("next", wallet_descriptor.next_index);
}
response.push_back(spk);
}

return response;
},
};
}
2 changes: 2 additions & 0 deletions src/wallet/rpcwallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4183,6 +4183,7 @@ UniValue importprunedfunds(const JSONRPCRequest& request);
UniValue removeprunedfunds(const JSONRPCRequest& request);
UniValue importmulti(const JSONRPCRequest& request);
UniValue importdescriptors(const JSONRPCRequest& request);
RPCHelpMan listdescriptors();
UniValue dumphdinfo(const JSONRPCRequest& request);
UniValue importelectrumwallet(const JSONRPCRequest& request);

Expand Down Expand Up @@ -4443,6 +4444,7 @@ static const CRPCCommand commands[] =
{ "wallet", "keypoolrefill", &keypoolrefill, {"newsize"} },
{ "wallet", "listaddressbalances", &listaddressbalances, {"minamount"} },
{ "wallet", "listaddressgroupings", &listaddressgroupings, {} },
{ "wallet", "listdescriptors", &listdescriptors, {} },
{ "wallet", "listlabels", &listlabels, {"purpose"} },
{ "wallet", "listlockunspent", &listlockunspent, {} },
{ "wallet", "listreceivedbyaddress", &listreceivedbyaddress, {"minconf","addlocked","include_empty","include_watchonly","address_filter"} },
Expand Down
1 change: 1 addition & 0 deletions test/functional/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
'mining_basic.py',
'rpc_named_arguments.py',
'wallet_listsinceblock.py',
'wallet_listdescriptors.py --descriptors',
'p2p_leak.py',
'p2p_compactblocks.py',
'p2p_connect_to_devnet.py',
Expand Down
67 changes: 67 additions & 0 deletions test/functional/wallet_listdescriptors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
# Copyright (c) 2014-2021 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test the listdescriptors RPC."""

from test_framework.descriptors import (
descsum_create
)
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
assert_raises_rpc_error,
)


class ListDescriptorsTest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 1

def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
self.skip_if_no_sqlite()

# do not create any wallet by default
def init_wallet(self, i):
return

def run_test(self):
node = self.nodes[0]
assert_raises_rpc_error(-18, 'No wallet is loaded.', node.listdescriptors)

self.log.info('Test that the command is not available for legacy wallets.')
node.createwallet(wallet_name='w1', descriptors=False)
assert_raises_rpc_error(-4, 'listdescriptors is not available for non-descriptor wallets', node.listdescriptors)

self.log.info('Test the command for empty descriptors wallet.')
node.createwallet(wallet_name='w2', blank=True, descriptors=True)
assert_equal(0, len(node.get_wallet_rpc('w2').listdescriptors()))

self.log.info('Test the command for a default descriptors wallet.')
node.createwallet(wallet_name='w3', descriptors=True)
result = node.get_wallet_rpc('w3').listdescriptors()
assert_equal(2, len(result))
assert_equal(2, len([d for d in result if d['active']]))
assert_equal(1, len([d for d in result if d['internal']]))
for item in result:
assert item['desc'] != ''
assert item['next'] == 0
assert item['range'] == [0, 0]
assert item['timestamp'] is not None

self.log.info('Test non-active non-range combo descriptor')
node.createwallet(wallet_name='w4', blank=True, descriptors=True)
wallet = node.get_wallet_rpc('w4')
wallet.importdescriptors([{
'desc': descsum_create('combo(' + node.get_deterministic_priv_key().key + ')'),
'timestamp': 1296688602,
}])
expected = [{'active': False,
'desc': 'combo(038af19f35924e37ad7c3c8045d1e19b9b90b7310e08b892e620c253a102fe49f0)#2j2j0825',
'timestamp': 1296688602}]
assert_equal(expected, wallet.listdescriptors())


if __name__ == '__main__':
ListDescriptorsTest().main()

0 comments on commit 56d9fe9

Please sign in to comment.