Skip to content

Commit 16203d5

Browse files
committed
Add descriptors to listunspent and getaddressinfo + tests
1 parent 9b2a25b commit 16203d5

File tree

2 files changed

+69
-0
lines changed

2 files changed

+69
-0
lines changed

src/wallet/rpcwallet.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <rpc/rawtransaction.h>
2121
#include <rpc/server.h>
2222
#include <rpc/util.h>
23+
#include <script/descriptor.h>
2324
#include <script/sign.h>
2425
#include <shutdown.h>
2526
#include <timedata.h>
@@ -2623,6 +2624,7 @@ static UniValue listunspent(const JSONRPCRequest& request)
26232624
" \"redeemScript\" : n (string) The redeemScript if scriptPubKey is P2SH\n"
26242625
" \"spendable\" : xxx, (bool) Whether we have the private keys to spend this output\n"
26252626
" \"solvable\" : xxx, (bool) Whether we know how to spend this output, ignoring the lack of keys\n"
2627+
" \"desc\" : xxx, (string, only when solvable) A descriptor for spending this output\n"
26262628
" \"safe\" : xxx (bool) Whether this output is considered safe to spend. Unconfirmed transactions\n"
26272629
" from outside keys and unconfirmed replacement transactions are considered unsafe\n"
26282630
" and are not eligible for spending by fundrawtransaction and sendtoaddress.\n"
@@ -2740,6 +2742,10 @@ static UniValue listunspent(const JSONRPCRequest& request)
27402742
entry.pushKV("confirmations", out.nDepth);
27412743
entry.pushKV("spendable", out.fSpendable);
27422744
entry.pushKV("solvable", out.fSolvable);
2745+
if (out.fSolvable) {
2746+
auto descriptor = InferDescriptor(scriptPubKey, *pwallet);
2747+
entry.pushKV("desc", descriptor->ToString());
2748+
}
27432749
entry.pushKV("safe", out.fSafe);
27442750
results.push_back(entry);
27452751
}
@@ -3456,6 +3462,8 @@ UniValue getaddressinfo(const JSONRPCRequest& request)
34563462
" \"scriptPubKey\" : \"hex\", (string) The hex-encoded scriptPubKey generated by the address\n"
34573463
" \"ismine\" : true|false, (boolean) If the address is yours or not\n"
34583464
" \"iswatchonly\" : true|false, (boolean) If the address is watchonly\n"
3465+
" \"solvable\" : true|false, (boolean) Whether we know how to spend coins sent to this address, ignoring the possible lack of private keys\n"
3466+
" \"desc\" : \"desc\", (string, optional) A descriptor for spending coins sent to this address (only when solvable)\n"
34593467
" \"isscript\" : true|false, (boolean) If the key is a script\n"
34603468
" \"iswitness\" : true|false, (boolean) If the address is a witness address\n"
34613469
" \"witness_version\" : version (numeric, optional) The version number of the witness program\n"
@@ -3508,6 +3516,11 @@ UniValue getaddressinfo(const JSONRPCRequest& request)
35083516

35093517
isminetype mine = IsMine(*pwallet, dest);
35103518
ret.pushKV("ismine", bool(mine & ISMINE_SPENDABLE));
3519+
bool solvable = IsSolvable(*pwallet, scriptPubKey);
3520+
ret.pushKV("solvable", solvable);
3521+
if (solvable) {
3522+
ret.pushKV("desc", InferDescriptor(scriptPubKey, *pwallet)->ToString());
3523+
}
35113524
ret.pushKV("iswatchonly", bool(mine & ISMINE_WATCH_ONLY));
35123525
UniValue detail = DescribeWalletAddress(pwallet, dest);
35133526
ret.pushKVs(detail);

test/functional/wallet_address_types.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ def test_address(self, node, address, multisig, typ):
9999
"""Run sanity checks on an address."""
100100
info = self.nodes[node].getaddressinfo(address)
101101
assert(self.nodes[node].validateaddress(address)['isvalid'])
102+
assert_equal(info.get('solvable'), True)
103+
102104
if not multisig and typ == 'legacy':
103105
# P2PKH
104106
assert(not info['isscript'])
@@ -146,6 +148,47 @@ def test_address(self, node, address, multisig, typ):
146148
# Unknown type
147149
assert(False)
148150

151+
def test_desc(self, node, address, multisig, typ, utxo):
152+
"""Run sanity checks on a descriptor reported by getaddressinfo."""
153+
info = self.nodes[node].getaddressinfo(address)
154+
assert('desc' in info)
155+
assert_equal(info['desc'], utxo['desc'])
156+
assert(self.nodes[node].validateaddress(address)['isvalid'])
157+
158+
# Use a ridiculously roundabout way to find the key origin info through
159+
# the PSBT logic. However, this does test consistency between the PSBT reported
160+
# fingerprints/paths and the descriptor logic.
161+
psbt = self.nodes[node].createpsbt([{'txid':utxo['txid'], 'vout':utxo['vout']}],[{address:0.00010000}])
162+
psbt = self.nodes[node].walletprocesspsbt(psbt, False, "ALL", True)
163+
decode = self.nodes[node].decodepsbt(psbt['psbt'])
164+
key_descs = {}
165+
for deriv in decode['inputs'][0]['bip32_derivs']:
166+
assert_equal(len(deriv['master_fingerprint']), 8)
167+
assert_equal(deriv['path'][0], 'm')
168+
key_descs[deriv['pubkey']] = '[' + deriv['master_fingerprint'] + deriv['path'][1:] + ']' + deriv['pubkey']
169+
170+
if not multisig and typ == 'legacy':
171+
# P2PKH
172+
assert_equal(info['desc'], "pkh(%s)" % key_descs[info['pubkey']])
173+
elif not multisig and typ == 'p2sh-segwit':
174+
# P2SH-P2WPKH
175+
assert_equal(info['desc'], "sh(wpkh(%s))" % key_descs[info['pubkey']])
176+
elif not multisig and typ == 'bech32':
177+
# P2WPKH
178+
assert_equal(info['desc'], "wpkh(%s)" % key_descs[info['pubkey']])
179+
elif typ == 'legacy':
180+
# P2SH-multisig
181+
assert_equal(info['desc'], "sh(multi(2,%s,%s))" % (key_descs[info['pubkeys'][0]], key_descs[info['pubkeys'][1]]))
182+
elif typ == 'p2sh-segwit':
183+
# P2SH-P2WSH-multisig
184+
assert_equal(info['desc'], "sh(wsh(multi(2,%s,%s)))" % (key_descs[info['embedded']['pubkeys'][0]], key_descs[info['embedded']['pubkeys'][1]]))
185+
elif typ == 'bech32':
186+
# P2WSH-multisig
187+
assert_equal(info['desc'], "wsh(multi(2,%s,%s))" % (key_descs[info['pubkeys'][0]], key_descs[info['pubkeys'][1]]))
188+
else:
189+
# Unknown type
190+
assert(False)
191+
149192
def test_change_output_type(self, node_sender, destinations, expected_type):
150193
txid = self.nodes[node_sender].sendmany(dummy="", amounts=dict.fromkeys(destinations, 0.001))
151194
raw_tx = self.nodes[node_sender].getrawtransaction(txid)
@@ -198,6 +241,7 @@ def run_test(self):
198241
self.log.debug("Old balances are {}".format(old_balances))
199242
to_send = (old_balances[from_node] / 101).quantize(Decimal("0.00000001"))
200243
sends = {}
244+
addresses = {}
201245

202246
self.log.debug("Prepare sends")
203247
for n, to_node in enumerate(range(from_node, from_node + 4)):
@@ -228,6 +272,7 @@ def run_test(self):
228272

229273
# Output entry
230274
sends[address] = to_send * 10 * (1 + n)
275+
addresses[to_node] = (address, typ)
231276

232277
self.log.debug("Sending: {}".format(sends))
233278
self.nodes[from_node].sendmany("", sends)
@@ -244,6 +289,17 @@ def run_test(self):
244289
self.nodes[5].generate(1)
245290
sync_blocks(self.nodes)
246291

292+
# Verify that the receiving wallet contains a UTXO with the expected address, and expected descriptor
293+
for n, to_node in enumerate(range(from_node, from_node + 4)):
294+
to_node %= 4
295+
found = False
296+
for utxo in self.nodes[to_node].listunspent():
297+
if utxo['address'] == addresses[to_node][0]:
298+
found = True
299+
self.test_desc(to_node, addresses[to_node][0], multisig, addresses[to_node][1], utxo)
300+
break
301+
assert found
302+
247303
new_balances = self.get_balances()
248304
self.log.debug("Check new balances: {}".format(new_balances))
249305
# We don't know what fee was set, so we can only check bounds on the balance of the sending node

0 commit comments

Comments
 (0)