Skip to content

Commit 4a0ce94

Browse files
authored
Merge pull request #122 from tynes/wallet-getaddressinfo
rpc: new wallet rpc getaddressinfo
2 parents 696dfc4 + 7244b84 commit 4a0ce94

File tree

2 files changed

+282
-0
lines changed

2 files changed

+282
-0
lines changed

lib/wallet/rpc.js

Lines changed: 24 additions & 0 deletions
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

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

0 commit comments

Comments
 (0)