Skip to content

Commit 8c49016

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. Commit 62b4de5 updates `validateaddress` to remove the `ismine` property because that property can now be found on the `getaddressinfo` RPC. The `isspendable` property refers to an address will null data, which is 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 17dceef commit 8c49016

File tree

2 files changed

+271
-0
lines changed

2 files changed

+271
-0
lines changed

lib/wallet/rpc.js

+24
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,29 @@ 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 wallet = this.wallet.toJSON();
462+
const path = await this.wallet.getPath(address);
463+
464+
return {
465+
address: address.toString(this.network),
466+
ismine: path != null,
467+
iswatchonly: wallet.watchOnly,
468+
ischange: path ? path.branch === 1 : false,
469+
isspendable: !address.isUnspendable(),
470+
isscript: address.isScripthash(),
471+
witness_version: address.version,
472+
witness_program: address.hash.toString('hex')
473+
};
474+
}
475+
452476
async getBalance(args, help) {
453477
if (help || args.length > 3) {
454478
throw new RPCError(errs.MISC_ERROR,

test/wallet-rpc-test.js

+247
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
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 addr = address.toString(network);
167+
await wclient.execute('selectwallet', [watchOnlyWalletId]);
168+
const response = await wclient.execute('getaddressinfo', [addr]);
169+
assert.equal(response.witness_program, address.hash.toString('hex'));
170+
});
171+
172+
it('should detect a p2wsh and its witness program', async () => {
173+
const script = Script.fromMultisig(2, 2, pubkeys);
174+
const address = Address.fromScript(script);
175+
176+
const addr = address.toString(network);
177+
const response = await wclient.execute('getaddressinfo', [addr]);
178+
179+
assert.equal(response.isscript, true);
180+
assert.equal(response.witness_program, address.hash.toString('hex'));
181+
});
182+
183+
it('should detect ismine up to the lookahead', async () => {
184+
const info = await wclient.getAccount(watchOnlyWalletId, 'default');
185+
await wclient.execute('selectwallet', [watchOnlyWalletId]);
186+
187+
const addresses = [
188+
'rs1q4rvs9pp9496qawp2zyqpz3s90fjfk362q92vq8', // 0/0
189+
'rs1qwkzdtg56m5zqu4wuwtwuqltn4s9uxd3s93ec2n', // 0/1
190+
'rs1q9atns2u4ayv2hsug0xuha3acqxz450mgvaer4z', // 0/2
191+
'rs1qve2kd3lyhnm6r7knzvl3s5pkgem9a25xk42y5d', // 0/3
192+
'rs1q799vpn8u7524p54kaumclcfuayxxkuga84xle2', // 0/4
193+
'rs1q748rcdq767cxla4d0gkpc35r4cl6kk72z47qdq', // 0/5
194+
'rs1qq7fkaj2ruwcdsdr4l2f4jx0a8nqyg9qdr7y6am', // 0/6
195+
'rs1qm8jx0q9y2tq990tswes08gnjhfhej9vfjcql92', // 0/7
196+
'rs1qf3zef3m8tnl8el5wurtrg5qgt6c2aepu7pqeqc', // 0/8
197+
'rs1qermzxwthx9tz2h64fgmmxwc8k05zhxhfhx5khm', // 0/9
198+
'rs1qlvysusse4qgym5s7mv8ddaatgvgfq6g6vcjhvf' // 0/10
199+
];
200+
201+
// Assert that the lookahead is configured as expected
202+
// subtract one from addresses.length, it is 0 indexed
203+
assert.equal(addresses.length - 1, info.lookahead);
204+
205+
// Each address through the lookahead number should
206+
// be recognized as an owned address
207+
for (let i = 0; i < info.lookahead+1; i++) {
208+
const address = addresses[i];
209+
const response = await wclient.execute('getaddressinfo', [address]);
210+
assert.equal(response.ismine, true);
211+
}
212+
213+
// m/44'/5355'/0'/11
214+
// This address is outside of the lookahead range
215+
const failed = 'rs1qg8h0n5mrdt5u8jaxq9jequ3nekj8fg820lr5xg';
216+
217+
const response = await wclient.execute('getaddressinfo', [failed]);
218+
assert.equal(response.ismine, false);
219+
});
220+
221+
it('should detect change addresses', async () => {
222+
await wclient.execute('selectwallet', [watchOnlyWalletId]);
223+
// m/44'/5355'/0'/1/0
224+
const address = 'rs1qxps2ljf5604tgyz7pvecuq6twwt4k9qsxcd27y';
225+
const info = await wclient.execute('getaddressinfo', [address]);
226+
227+
assert.equal(info.ischange, true);
228+
});
229+
230+
it('should throw for the wrong network', async () => {
231+
// m/44'/5355'/0'/0/0
232+
const failed = 'hs1q4rvs9pp9496qawp2zyqpz3s90fjfk362rl50q4';
233+
234+
const fn = async () => await wclient.execute('getaddressinfo', [failed]);
235+
await assert.rejects(fn, 'Invalid address.');
236+
});
237+
238+
it('should throw for invalid address', async () => {
239+
let failed = 'rs1q4rvs9pp9496qawp2zyqpz3s90fjfk362q92vq8';
240+
// remove the first character
241+
failed = failed.slice(1, failed.length);
242+
243+
const fn = async () => await wclient.execute('getaddressinfo', [failed]);
244+
await assert.rejects(fn, 'Invalid address.');
245+
});
246+
});
247+
});

0 commit comments

Comments
 (0)