diff --git a/lib/node/rpc.js b/lib/node/rpc.js index aef49399e..b4ff7fe6f 100644 --- a/lib/node/rpc.js +++ b/lib/node/rpc.js @@ -229,6 +229,7 @@ class RPC extends RPCBase { this.add('getmemoryinfo', this.getMemoryInfo); this.add('setloglevel', this.setLogLevel); this.add('getdescriptorinfo', this.getDescriptorInfo); + this.add('deriveaddresses', this.deriveAddresses); } /* @@ -2346,10 +2347,97 @@ class RPC extends RPCBase { return result; } + async deriveAddresses(args, help) { + if (help || args.length > 2 || args.length === 0) + throw new RPCError( + errs.MISC_ERROR, 'deriveaddresses "descriptor" (range)' + ); + + const valid = new Validator(args); + + const desc = parseDescriptor(valid.str(0, ''), this.network, true); + + if (desc.isRange() && !valid.has(1)) { + throw new RPCError( + errs.INVALID_PARAMETER, + 'Range must be specified for ranged descriptor' + ); + } + + if (!desc.isRange() && valid.has(1)) { + throw new RPCError( + errs.INVALID_PARAMETER, + 'Range should not be specified for un-ranged descriptor' + ); + } + + const {low, high} = this.getDescriptorRangeParams(valid); + + const addresses = []; + for (let i = low; i <= high; i++) { + addresses.push(...desc.getAddresses(i)); + } + + return addresses; + } + /* * Helpers */ + getDescriptorRangeParams(valid) { + let low = 0, high = 0; + + // can be integer as well as array of integers + try { + high = valid.u32(1, 0); + } catch (e) { + try { + const arr = valid.array(1, []); + low = arr[0]; + high = arr[1]; + } catch (e) { + throw new RPCError( + errs.INVALID_PARAMETER, + 'Invalid range.' + ); + } + } + + if (!Number.isSafeInteger(low)) { + throw new RPCError( + errs.INVALID_PARAMETER, 'Range begin must be an integer' + ); + } + + if (!Number.isSafeInteger(high)) { + throw new RPCError( + errs.INVALID_PARAMETER, 'Range end must be an integer' + ); + } + + if (low > high) { + throw new RPCError( + errs.INVALID_PARAMETER, + 'Range specified as [begin,end] must not have begin after end' + ); + } + + if (low < 0) { + throw new RPCError(errs.INVALID_PARAMETER, 'Range should be >= 0'); + } + + if (high >= 0x80000000) { + throw new RPCError(errs.INVALID_PARAMETER, 'End of range is too high'); + } + + if (high >= low + 1000000) { + throw new RPCError(errs.INVALID_PARAMETER, 'Range is too large'); + } + + return {low, high}; + } + async handleLongpoll(lpid) { if (lpid.length !== 72) throw new RPCError(errs.INVALID_PARAMETER, 'Invalid longpoll ID.'); @@ -2884,9 +2972,9 @@ function parseAddress(raw, network) { } } -function parseDescriptor(raw, network) { +function parseDescriptor(raw, network, requireChecksum = false) { try { - return parse(raw, network, false); + return parse(raw, network, requireChecksum); } catch (e) { throw new RPCError(errs.INVALID_DESCRIPTOR, `Invalid descriptor: ${e.message}` diff --git a/test/node-rpc-test.js b/test/node-rpc-test.js index 67c79f242..f9086463f 100644 --- a/test/node-rpc-test.js +++ b/test/node-rpc-test.js @@ -174,6 +174,66 @@ describe('RPC', function() { } }); + it('should rpc deriveaddresses', async () => { + const data = [ + { + "input": "sh(wsh(sortedmulti(2,[e7dd1c50/48'/1'/40'/1']tpubDFh3VaUEs71ZMcVBmscSSnP4f4r6TvnLssu8yXvpj3uMfAehciMYTrgbfu4KCxXb7oSaz4kriuWRZtQVhZR2oA9toob6aELnsYLN94fXQLF/*,[e7dd1c50/48'/1'/20'/1']tpubDFPemvLnpMqE1BPuturDUh46KxsR8wGSQrA6HofYE7fqxpMAKCcoYWHGA46B6zKY4xcQAc1vLFTcqQ9BvsbHZ4UhzqqF5nUeeNBjNivHxPT/*,[aedb3d12/48'/1'/0'/1']tpubDEbuxto5Kftus28NyPddiEev2yUhzZGpkpQdCK732KBge5FJDhaMdhG1iVw3rMJ2qvABkaLR9HxobkeFkmQZ4RqQgN1KJadDjPn9ANBLo8V/*)))#zlh5y6z5", + "range": [0, "a"], + "error": "Range end must be an integer" + }, + { + "input": "sh(wsh(sortedmulti(2,[e7dd1c50/48'/1'/40'/1']tpubDFh3VaUEs71ZMcVBmscSSnP4f4r6TvnLssu8yXvpj3uMfAehciMYTrgbfu4KCxXb7oSaz4kriuWRZtQVhZR2oA9toob6aELnsYLN94fXQLF/*,[e7dd1c50/48'/1'/20'/1']tpubDFPemvLnpMqE1BPuturDUh46KxsR8wGSQrA6HofYE7fqxpMAKCcoYWHGA46B6zKY4xcQAc1vLFTcqQ9BvsbHZ4UhzqqF5nUeeNBjNivHxPT/*,[aedb3d12/48'/1'/0'/1']tpubDEbuxto5Kftus28NyPddiEev2yUhzZGpkpQdCK732KBge5FJDhaMdhG1iVw3rMJ2qvABkaLR9HxobkeFkmQZ4RqQgN1KJadDjPn9ANBLo8V/*)))#zlh5y6z5", + "range": [10, 0], + "error": "Range specified as [begin,end] must not have begin after end" + }, + { + "input": "sh(wsh(sortedmulti(2,[e7dd1c50/48'/1'/40'/1']tpubDFh3VaUEs71ZMcVBmscSSnP4f4r6TvnLssu8yXvpj3uMfAehciMYTrgbfu4KCxXb7oSaz4kriuWRZtQVhZR2oA9toob6aELnsYLN94fXQLF/*,[e7dd1c50/48'/1'/20'/1']tpubDFPemvLnpMqE1BPuturDUh46KxsR8wGSQrA6HofYE7fqxpMAKCcoYWHGA46B6zKY4xcQAc1vLFTcqQ9BvsbHZ4UhzqqF5nUeeNBjNivHxPT/*,[aedb3d12/48'/1'/0'/1']tpubDEbuxto5Kftus28NyPddiEev2yUhzZGpkpQdCK732KBge5FJDhaMdhG1iVw3rMJ2qvABkaLR9HxobkeFkmQZ4RqQgN1KJadDjPn9ANBLo8V/*)))#zlh5y6z5", + "range": [-1, 2], + "error": "Range should be >= 0" + }, + { + "input": "sh(wsh(sortedmulti(2,[e7dd1c50/48'/1'/40'/1']tpubDFh3VaUEs71ZMcVBmscSSnP4f4r6TvnLssu8yXvpj3uMfAehciMYTrgbfu4KCxXb7oSaz4kriuWRZtQVhZR2oA9toob6aELnsYLN94fXQLF/*,[e7dd1c50/48'/1'/20'/1']tpubDFPemvLnpMqE1BPuturDUh46KxsR8wGSQrA6HofYE7fqxpMAKCcoYWHGA46B6zKY4xcQAc1vLFTcqQ9BvsbHZ4UhzqqF5nUeeNBjNivHxPT/*,[aedb3d12/48'/1'/0'/1']tpubDEbuxto5Kftus28NyPddiEev2yUhzZGpkpQdCK732KBge5FJDhaMdhG1iVw3rMJ2qvABkaLR9HxobkeFkmQZ4RqQgN1KJadDjPn9ANBLo8V/*)))#zlh5y6z5", + "error": "Range must be specified for ranged descriptor" + }, + { + "input": "sh(wsh(sortedmulti(2,[e7dd1c50/48'/1'/40'/1']tpubDFh3VaUEs71ZMcVBmscSSnP4f4r6TvnLssu8yXvpj3uMfAehciMYTrgbfu4KCxXb7oSaz4kriuWRZtQVhZR2oA9toob6aELnsYLN94fXQLF/*,[e7dd1c50/48'/1'/20'/1']tpubDFPemvLnpMqE1BPuturDUh46KxsR8wGSQrA6HofYE7fqxpMAKCcoYWHGA46B6zKY4xcQAc1vLFTcqQ9BvsbHZ4UhzqqF5nUeeNBjNivHxPT/*,[aedb3d12/48'/1'/0'/1']tpubDEbuxto5Kftus28NyPddiEev2yUhzZGpkpQdCK732KBge5FJDhaMdhG1iVw3rMJ2qvABkaLR9HxobkeFkmQZ4RqQgN1KJadDjPn9ANBLo8V/*)))#zlh5y6z5", + "range": 10, + "addresses": [ + "2MtWBjxiAi4xYNUdtDe2sHkNw5kdAQqZZNb", + "2MstQfXgUwTR66bhMrbNU3qDqT6RT4hGHnJ", + "2N6QXTyf64KHWddFZ5swjaRmEwk4hEawYuo", + "2NCfXGhiA6EjK6o2JSejtzeP5fYkNxP1TQC", + "2NCU5HcmKUap923abUPEhGNnTFUf3K2hAYr", + "2N6K6jKKeuejPTeiPDbnq1qZsqGhigoApzK", + "2N4jy9MPJee7WvH3tfRVE3LeYQxVNhjt2yH", + "2NCXTtRLCjwWPeUoRz5qGoKSj84Ci4pbcWy", + "2NEwEq98wFu1EcSf5jFexCRZEuGjjeTo265", + "2MzcruaPLyHniY1qjJLXLXEuYRd5PJ1o1EW", + "2MuY8izcxH5KJfhvc432HQVos483krppmrf" + ] + }, + { + "input": "pkh(04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)#9907vvwz", + "range": [0, 5], + "error": "Range should not be specified for un-ranged descriptor" + }, + { + "input":"pkh([d34db33f/44h/0h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1h/1h/*h)#u5f4r0y7", + "range": [1, 5], + "error": "Private key not available for hardened derivation." + } + ]; + + for (const test of data) { + try { + const result = test.range ? await nclient.execute("deriveaddresses", [test.input, test.range]) : await nclient.execute("deriveaddresses", [test.input]); + assert.deepStrictEqual(result, test.addresses); + } catch (e) { + assert.strictEqual(e.message, test.error); + } + } + }); + it('should rpc getblockhash', async () => { const info = await nclient.execute('getblockhash', [node.chain.tip.height]); assert.strictEqual(util.revHex(node.chain.tip.hash), info);