Skip to content

Commit 6794105

Browse files
author
Mark Tyneway
committed
rpc: new wallet rpc getaddressinfo
Adds a new wallet RPC method getaddressinfo that corresponds to changes with bitcoind. Returns values: - address - ismine - iswatchonly - ischange - isspendable - isscript - witness_version - witness_program The ismine property previously existed on the node RPC method validateaddress which was always hardcoded to true. An additional PR will be added to update validateaddress to remove the ismine property. isspendable refers to an address with null data, of version 31. See PR in bcoin: bcoin-org/bcoin#731 See bitcoind docs: https://bitcoincore.org/en/doc/0.17.0/rpc/wallet/getaddressinfo/
1 parent bd1ba3d commit 6794105

File tree

2 files changed

+275
-0
lines changed

2 files changed

+275
-0
lines changed

lib/wallet/rpc.js

+26
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ class RPC extends RPCBase {
129129
this.add('getaccountaddress', this.getAccountAddress);
130130
this.add('getaccount', this.getAccount);
131131
this.add('getaddressesbyaccount', this.getAddressesByAccount);
132+
this.add('getaddressinfo', this.getAddressInfo);
132133
this.add('getbalance', this.getBalance);
133134
this.add('getnewaddress', this.getNewAddress);
134135
this.add('getrawchangeaddress', this.getRawChangeAddress);
@@ -449,6 +450,31 @@ class RPC extends RPCBase {
449450
return addrs;
450451
}
451452

453+
async getAddressInfo(args, help) {
454+
if (help || args.length !== 1)
455+
throw new RPCError(errs.MISC_ERROR, 'getaddressinfo "address"');
456+
457+
const valid = new Validator(args);
458+
const addr = valid.str(0, '');
459+
const address = parseAddress(addr, this.network);
460+
461+
const script = Script.decode(address.hash);
462+
const wallet = this.wallet.toJSON();
463+
464+
const path = await this.wallet.getPath(address);
465+
466+
return {
467+
address: address.toString(this.network),
468+
ismine: path != null,
469+
iswatchonly: wallet.watchOnly,
470+
ischange: path ? path.branch === 1 : false,
471+
isspendable: !address.isUnspendable(),
472+
isscript: address.isScripthash(),
473+
witness_version: address.version,
474+
witness_program: script.toHex()
475+
};
476+
}
477+
452478
async getBalance(args, help) {
453479
if (help || args.length > 3) {
454480
throw new RPCError(errs.MISC_ERROR,

test/wallet-rpc-test.js

+249
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
/* eslint-env mocha */
2+
3+
'use strict';
4+
5+
const {NodeClient,WalletClient} = require('hs-client');
6+
const assert = require('bsert');
7+
const FullNode = require('../lib/node/fullnode');
8+
const Network = require('../lib/protocol/network');
9+
const Mnemonic = require('../lib/hd/mnemonic');
10+
const HDPrivateKey = require('../lib/hd/private');
11+
const Script = require('../lib/script/script');
12+
const Address = require('../lib/primitives/address');
13+
const network = Network.get('regtest');
14+
const mnemonics = require('./data/mnemonic-english.json');
15+
// Commonly used test mnemonic
16+
const phrase = mnemonics[0][1];
17+
18+
const node = new FullNode({
19+
network: network.type,
20+
apiKey: 'bar',
21+
walletAuth: true,
22+
memory: true,
23+
workers: true,
24+
plugins: [require('../lib/wallet/plugin')]
25+
});
26+
27+
const nclient = new NodeClient({
28+
port: network.rpcPort,
29+
apiKey: 'bar'
30+
});
31+
32+
const wclient = new WalletClient({
33+
port: network.walletPort,
34+
apiKey: 'bar'
35+
});
36+
37+
describe('Wallet RPC Methods', function() {
38+
this.timeout(15000);
39+
40+
let xpub;
41+
42+
before(async () => {
43+
await node.open();
44+
await nclient.open();
45+
await wclient.open();
46+
47+
// Derive the xpub using the well known
48+
// mnemonic and network's coin type
49+
const mnemonic = Mnemonic.fromPhrase(phrase);
50+
const priv = HDPrivateKey.fromMnemonic(mnemonic);
51+
const type = network.keyPrefix.coinType;
52+
const key = priv.derive(44, true).derive(type, true).derive(0, true);
53+
54+
xpub = key.toPublic();
55+
56+
assert.equal(phrase, [
57+
'abandon', 'abandon', 'abandon', 'abandon',
58+
'abandon', 'abandon', 'abandon', 'abandon',
59+
'abandon', 'abandon', 'abandon', 'about'
60+
].join(' '));
61+
});
62+
63+
after(async () => {
64+
await nclient.close();
65+
await wclient.close();
66+
await node.close();
67+
});
68+
69+
describe('getaddressinfo', () => {
70+
const watchOnlyWalletId = 'foo';
71+
const standardWalletId = 'bar';
72+
73+
// m/44'/5355'/0'/0/{0,1}
74+
const pubkeys = [
75+
Buffer.from('03253ea6d6486d1b9cc3a'
76+
+ 'b01a9a321d65c350c6c26a9c536633e2ef36163316bf2', 'hex'),
77+
Buffer.from('02cd38edb6f9cb4fd7380'
78+
+ '3b49aed97bfa95ef402cac2c34e8f551f8537811d2159', 'hex')
79+
];
80+
81+
// set up the initial testing state
82+
before(async () => {
83+
{
84+
// Set up the testing environment
85+
// by creating a wallet and a watch
86+
// only wallet
87+
const info = await nclient.getInfo();
88+
assert.equal(info.chain.height, 0);
89+
}
90+
91+
{
92+
// Create a watch only wallet using the path
93+
// m/44'/5355'/0' and assert that the wallet
94+
// was properly created
95+
const accountKey = xpub.xpubkey(network.type);
96+
const response = await wclient.createWallet(watchOnlyWalletId, {
97+
watchOnly: true,
98+
accountKey: accountKey
99+
});
100+
101+
assert.equal(response.id, watchOnlyWalletId);
102+
103+
const wallet = wclient.wallet(watchOnlyWalletId);
104+
const info = await wallet.getAccount('default');
105+
assert.equal(info.accountKey, accountKey);
106+
assert.equal(info.watchOnly, true);
107+
}
108+
109+
{
110+
// Create a wallet that manages the private keys itself
111+
const response = await wclient.createWallet(standardWalletId);
112+
assert.equal(response.id, standardWalletId);
113+
114+
const info = await wclient.getAccount(standardWalletId, 'default');
115+
assert.equal(info.watchOnly, false);
116+
};
117+
});
118+
119+
// the rpc interface requires the wallet to be selected first
120+
it('should return iswatchonly correctly', async () => {
121+
// m/44'/5355'/0'/0/0
122+
const receive = 'rs1q4rvs9pp9496qawp2zyqpz3s90fjfk362q92vq8';
123+
124+
{
125+
await wclient.execute('selectwallet', [standardWalletId]);
126+
const response = await wclient.execute('getaddressinfo', [receive]);
127+
assert.equal(response.iswatchonly, false);
128+
}
129+
{
130+
await wclient.execute('selectwallet', [watchOnlyWalletId]);
131+
const response = await wclient.execute('getaddressinfo', [receive]);
132+
assert.equal(response.iswatchonly, true);
133+
}
134+
});
135+
136+
it('should return the correct address', async () => {
137+
// m/44'/5355'/0'/0/0
138+
const receive = 'rs1q4rvs9pp9496qawp2zyqpz3s90fjfk362q92vq8';
139+
140+
await wclient.execute('selectwallet', [watchOnlyWalletId]);
141+
const response = await wclient.execute('getaddressinfo', [receive]);
142+
assert.equal(response.address, receive);
143+
});
144+
145+
it('should detect owned address', async () => {
146+
// m/44'/5355'/0'/0/0
147+
const receive = 'rs1q4rvs9pp9496qawp2zyqpz3s90fjfk362q92vq8';
148+
149+
{
150+
await wclient.execute('selectwallet', [watchOnlyWalletId]);
151+
const response = await wclient.execute('getaddressinfo', [receive]);
152+
assert.equal(response.ismine, true);
153+
}
154+
{
155+
await wclient.execute('selectwallet', [standardWalletId]);
156+
const response = await wclient.execute('getaddressinfo', [receive]);
157+
assert.equal(response.ismine, false);
158+
}
159+
});
160+
161+
it('should return the correct program for a p2pkh address', async () => {
162+
// m/44'/5355'/0'/0/0
163+
const receive = 'rs1q4rvs9pp9496qawp2zyqpz3s90fjfk362q92vq8';
164+
165+
const address = Address.fromString(receive);
166+
const program = Script.decode(address.hash).toHex();
167+
const addr = address.toString(network);
168+
await wclient.execute('selectwallet', [watchOnlyWalletId]);
169+
const response = await wclient.execute('getaddressinfo', [addr]);
170+
assert.equal(response.witness_program, program);
171+
});
172+
173+
it('should detect a p2wsh and its witness program', async () => {
174+
const script = Script.fromMultisig(2, 2, pubkeys);
175+
const address = Address.fromScript(script);
176+
const program = Script.decode(address.hash).toHex();
177+
178+
const addr = address.toString(network);
179+
const response = await wclient.execute('getaddressinfo', [addr]);
180+
181+
assert.equal(response.isscript, true);
182+
assert.equal(response.witness_program, program);
183+
});
184+
185+
it('should detect ismine up to the lookahead', async () => {
186+
const info = await wclient.getAccount(watchOnlyWalletId, 'default');
187+
await wclient.execute('selectwallet', [watchOnlyWalletId]);
188+
189+
const addresses = [
190+
'rs1q4rvs9pp9496qawp2zyqpz3s90fjfk362q92vq8', // 0/0
191+
'rs1qwkzdtg56m5zqu4wuwtwuqltn4s9uxd3s93ec2n', // 0/1
192+
'rs1q9atns2u4ayv2hsug0xuha3acqxz450mgvaer4z', // 0/2
193+
'rs1qve2kd3lyhnm6r7knzvl3s5pkgem9a25xk42y5d', // 0/3
194+
'rs1q799vpn8u7524p54kaumclcfuayxxkuga84xle2', // 0/4
195+
'rs1q748rcdq767cxla4d0gkpc35r4cl6kk72z47qdq', // 0/5
196+
'rs1qq7fkaj2ruwcdsdr4l2f4jx0a8nqyg9qdr7y6am', // 0/6
197+
'rs1qm8jx0q9y2tq990tswes08gnjhfhej9vfjcql92', // 0/7
198+
'rs1qf3zef3m8tnl8el5wurtrg5qgt6c2aepu7pqeqc', // 0/8
199+
'rs1qermzxwthx9tz2h64fgmmxwc8k05zhxhfhx5khm', // 0/9
200+
'rs1qlvysusse4qgym5s7mv8ddaatgvgfq6g6vcjhvf' // 0/10
201+
];
202+
203+
// Assert that the lookahead is configured as expected
204+
// subtract one from addresses.length, it is 0 indexed
205+
assert.equal(addresses.length - 1, info.lookahead);
206+
207+
// Each address through the lookahead number should
208+
// be recognized as an owned address
209+
for (let i = 0; i < info.lookahead+1; i++) {
210+
const address = addresses[i];
211+
const response = await wclient.execute('getaddressinfo', [address]);
212+
assert.equal(response.ismine, true);
213+
}
214+
215+
// m/44'/5355'/0'/11
216+
// This address is outside of the lookahead range
217+
const failed = 'rs1qg8h0n5mrdt5u8jaxq9jequ3nekj8fg820lr5xg';
218+
219+
const response = await wclient.execute('getaddressinfo', [failed]);
220+
assert.equal(response.ismine, false);
221+
});
222+
223+
it('should detect change addresses', async () => {
224+
await wclient.execute('selectwallet', [watchOnlyWalletId]);
225+
// m/44'/5355'/0'/1/0
226+
const address = 'rs1qxps2ljf5604tgyz7pvecuq6twwt4k9qsxcd27y';
227+
const info = await wclient.execute('getaddressinfo', [address]);
228+
229+
assert.equal(info.ischange, true);
230+
});
231+
232+
it('should throw for the wrong network', async () => {
233+
// m/44'/5355'/0'/0/0
234+
const failed = 'hs1q4rvs9pp9496qawp2zyqpz3s90fjfk362rl50q4';
235+
236+
const fn = async () => await wclient.execute('getaddressinfo', [failed]);
237+
await assert.rejects(fn, 'Invalid address.');
238+
});
239+
240+
it('should throw for invalid address', async () => {
241+
let failed = 'rs1q4rvs9pp9496qawp2zyqpz3s90fjfk362q92vq8';
242+
// remove the first character
243+
failed = failed.slice(1, failed.length);
244+
245+
const fn = async () => await wclient.execute('getaddressinfo', [failed]);
246+
await assert.rejects(fn, 'Invalid address.');
247+
});
248+
});
249+
});

0 commit comments

Comments
 (0)