From 5751b8c330e481ab23bd8234bc7d8011518acca2 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Thu, 19 Jul 2018 19:03:12 +0000 Subject: [PATCH] rpc: multiple fixes and tests wallet-rpc: fix listsinceblock height wdb: fix getWalletPaths() always return nothing wallet: add unlockCoins() for lockunspent rpc wallet-rpc: correct num args for some rpc calls wallet-rpc: fix args for importwallet and importprunedfunds wallet-rpc: importprunedfunds error misleading wallet-rpc: importPubkey args wallet-rpc: listsinceblock arg count error wallet-rpc: listTransactions ignoring account param wallet-rpc: better sendMany error msgs wallet-rpc: sendMany subtractfee is bool not obj wallet-rpc: dont check if not implemented test: add rpc-test --- lib/wallet/rpc.js | 55 ++++----- lib/wallet/txdb.js | 9 ++ lib/wallet/wallet.js | 8 ++ lib/wallet/walletdb.js | 2 +- test/rpc-test.js | 257 +++++++++++++++++++++++++++++++++++++++++ test/util/assert.js | 12 ++ 6 files changed, 309 insertions(+), 34 deletions(-) create mode 100644 test/rpc-test.js diff --git a/lib/wallet/rpc.js b/lib/wallet/rpc.js index b6020c02..d14ccc36 100644 --- a/lib/wallet/rpc.js +++ b/lib/wallet/rpc.js @@ -257,19 +257,11 @@ class RPC extends RPCBase { } async addMultisigAddress(args, help) { - if (help || args.length < 2 || args.length > 3) { - throw new RPCError(errs.MISC_ERROR, - 'addmultisigaddress nrequired ["key",...] ( "account" )'); - } - // Impossible to implement in bcoin (no address book). throw new Error('Not implemented.'); } async addWitnessAddress(args, help) { - if (help || args.length < 1 || args.length > 1) - throw new RPCError(errs.MISC_ERROR, 'addwitnessaddress "address"'); - // Unlikely to be implemented. throw new Error('Not implemented.'); } @@ -491,7 +483,7 @@ class RPC extends RPCBase { const valid = new Validator(args); let name = valid.str(0); - if (name === '') + if (name === '' || args.length === 0) name = 'default'; const addr = await wallet.createReceive(name); @@ -500,7 +492,7 @@ class RPC extends RPCBase { } async getRawChangeAddress(args, help) { - if (help || args.length > 1) + if (help || args.length !== 0) throw new RPCError(errs.MISC_ERROR, 'getrawchangeaddress'); const wallet = this.wallet; @@ -750,7 +742,7 @@ class RPC extends RPCBase { } async importWallet(args, help) { - if (help || args.length !== 1) + if (help || args.length < 1 || args.length > 2) throw new RPCError(errs.MISC_ERROR, 'importwallet "filename" ( rescan )'); const wallet = this.wallet; @@ -839,7 +831,7 @@ class RPC extends RPCBase { } async importPubkey(args, help) { - if (help || args.length < 1 || args.length > 4) { + if (help || args.length < 1 || args.length > 3) { throw new RPCError(errs.MISC_ERROR, 'importpubkey "pubkey" ( "label" rescan )'); } @@ -899,8 +891,6 @@ class RPC extends RPCBase { } async listAddressGroupings(args, help) { - if (help) - throw new RPCError(errs.MISC_ERROR, 'listaddressgroupings'); throw new Error('Not implemented.'); } @@ -1033,6 +1023,11 @@ class RPC extends RPCBase { } async listSinceBlock(args, help) { + if (help || args.length > 3) { + throw new RPCError(errs.MISC_ERROR, + 'listsinceblock ( "blockhash" target-confirmations includeWatchonly)'); + } + const wallet = this.wallet; const chainHeight = this.wdb.state.height; const valid = new Validator(args); @@ -1040,11 +1035,6 @@ class RPC extends RPCBase { const minconf = valid.u32(1, 0); const watchOnly = valid.bool(2, false); - if (help) { - throw new RPCError(errs.MISC_ERROR, - 'listsinceblock ( "blockhash" target-confirmations includeWatchonly)'); - } - if (wallet.watchOnly !== watchOnly) return []; @@ -1054,10 +1044,12 @@ class RPC extends RPCBase { const entry = await this.client.getEntry(block); if (entry) height = entry.height; + else + throw new RPCError(errs.MISC_ERROR, 'Block not found'); } if (height === -1) - height = this.chain.height; + height = chainHeight; const txs = await wallet.getHistory(); const out = []; @@ -1192,7 +1184,7 @@ class RPC extends RPCBase { if (name === '') name = 'default'; - const txs = await wallet.getHistory(); + const txs = await wallet.getHistory(name); common.sortTX(txs); @@ -1368,7 +1360,7 @@ class RPC extends RPCBase { if (help || args.length < 2 || args.length > 5) { throw new RPCError(errs.MISC_ERROR, 'sendmany "fromaccount" {"address":amount,...}' - + ' ( minconf "comment" ["address",...] )'); + + ' ( minconf "comment" subtractfee )'); } const wallet = this.wallet; @@ -1382,7 +1374,7 @@ class RPC extends RPCBase { name = 'default'; if (!sendTo) - throw new RPCError(errs.TYPE_ERROR, 'Invalid parameter.'); + throw new RPCError(errs.TYPE_ERROR, 'Invalid send-to address.'); const to = new Validator(sendTo); const uniq = new BufferSet(); @@ -1394,10 +1386,11 @@ class RPC extends RPCBase { const hash = addr.getHash(); if (value == null) - throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter.'); + throw new RPCError(errs.INVALID_PARAMETER, 'Invalid amount.'); if (uniq.has(hash)) - throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter.'); + throw new RPCError(errs.INVALID_PARAMETER, + 'Each send-to address must be unique.'); uniq.add(hash); @@ -1451,11 +1444,6 @@ class RPC extends RPCBase { } async setAccount(args, help) { - if (help || args.length < 1 || args.length > 2) { - throw new RPCError(errs.MISC_ERROR, - 'setaccount "bitcoinaddress" "account"'); - } - // Impossible to implement in bcoin: throw new Error('Not implemented.'); } @@ -1576,9 +1564,9 @@ class RPC extends RPCBase { } async importPrunedFunds(args, help) { - if (help || args.length < 2 || args.length > 3) { + if (help || args.length !== 2) { throw new RPCError(errs.MISC_ERROR, - 'importprunedfunds "rawtransaction" "txoutproof" ( "label" )'); + 'importprunedfunds "rawtransaction" "txoutproof"'); } const valid = new Validator(args); @@ -1610,7 +1598,8 @@ class RPC extends RPCBase { }; if (!await this.wdb.addTX(tx, entry)) - throw new RPCError(errs.WALLET_ERROR, 'No tracked address for TX.'); + throw new RPCError(errs.WALLET_ERROR, + 'Address for TX not in wallet, or TX already in wallet'); return null; } diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index 7af7b69a..6b140b5f 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -1176,6 +1176,15 @@ class TXDB { return this.locked.delete(key); } + /** + * Unlock all coins. + */ + + unlockCoins() { + for (const coin of this.getLocked()) + this.unlockCoin(coin); + } + /** * Test locked status of a single coin. * @param {Coin|Outpoint} coin diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 1df16bf3..38ac731c 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -1955,6 +1955,14 @@ class Wallet extends EventEmitter { return this.txdb.unlockCoin(coin); } + /** + * Unlock all locked coins. + */ + + unlockCoins() { + return this.txdb.unlockCoins(); + } + /** * Test locked status of a single coin. * @param {Coin|Outpoint} coin diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index f99dfab8..5745cb74 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -1354,7 +1354,7 @@ class WalletDB extends EventEmitter { async getWalletPaths(wid) { const items = await this.db.range({ gte: layout.P.min(wid), - lte: layout.P.min(wid) + lte: layout.P.max(wid) }); const paths = []; diff --git a/test/rpc-test.js b/test/rpc-test.js new file mode 100644 index 00000000..da4e43a9 --- /dev/null +++ b/test/rpc-test.js @@ -0,0 +1,257 @@ +/* eslint-env mocha */ +/* eslint prefer-arrow-callback: "off" */ + +'use strict'; + +const assert = require('./util/assert'); +const consensus = require('../lib/protocol/consensus'); +const Address = require('../lib/primitives/address'); +const FullNode = require('../lib/node/fullnode'); + +const ports = { + p2p: 49331, + node: 49332, + wallet: 49333 +}; + +const node = new FullNode({ + network: 'regtest', + apiKey: 'foo', + walletAuth: true, + memory: true, + workers: true, + plugins: [require('../lib/wallet/plugin')], + port: ports.p2p, + httpPort: ports.node, + env: { + 'BCOIN_WALLET_HTTP_PORT': ports.wallet.toString() + }}); + +const {NodeClient, WalletClient} = require('bclient'); + +const nclient = new NodeClient({ + port: ports.node, + apiKey: 'foo' +}); + +const wclient = new WalletClient({ + port: ports.wallet, + apiKey: 'foo' +}); + +const {wdb} = node.require('walletdb'); +const defaultCoinbaseMaturity = consensus.COINBASE_MATURITY; + +let addressHot = null; +let addressMiner = null; +let walletHot = null; +let walletMiner = null; +let blocks = null; +let txid = null; +let utxo = null; + +describe('RPC', function() { + this.timeout(15000); + + before(() => { + consensus.COINBASE_MATURITY = 0; + }); + + after(() => { + consensus.COINBASE_MATURITY = defaultCoinbaseMaturity; + }); + + it('should open node and create wallets', async () => { + await node.open(); + await nclient.open(); + await wclient.open(); + + const walletHotInfo = await wclient.createWallet('hot'); + walletHot = wclient.wallet('hot', walletHotInfo.token); + const walletMinerInfo = await wclient.createWallet('miner'); + walletMiner = wclient.wallet('miner', walletMinerInfo.token); + await walletHot.open(); + await walletMiner.open(); + }); + + it('should rpc help', async () => { + assert(await nclient.execute('help', [])); + assert(await wclient.execute('help', [])); + + await assert.asyncThrows(async () => { + await nclient.execute('help', ['getinfo']); + }, 'getinfo'); + + await assert.asyncThrows(async () => { + await wclient.execute('help', ['getbalance']); + }, 'getbalance'); + }); + + it('should rpc getinfo', async () => { + const info = await nclient.execute('getinfo', []); + assert.strictEqual(info.blocks, 0); + }); + + it('should rpc selectwallet', async () => { + const response = await wclient.execute('selectwallet', ['miner']); + assert.strictEqual(response, null); + }); + + it('should rpc getnewaddress from default account', async () => { + const acctAddr = await wclient.execute('getnewaddress', []); + assert(Address.fromString(acctAddr.toString())); + }); + + it('should fail rpc getnewaddress from nonexistent account', async () => { + await assert.asyncThrows(async () => { + await wclient.execute('getnewaddress', ['bad-account-name']); + }, 'Account not found.'); + }); + + it('should rpc getaccountaddress', async () => { + addressMiner = await wclient.execute('getaccountaddress', ['default']); + assert(Address.fromString(addressMiner.toString())); + }); + + it('should rpc generatetoaddress', async () => { + blocks = await nclient.execute('generatetoaddress', + [10, addressMiner]); + assert.strictEqual(blocks.length, 10); + }); + + it('should rpc sendtoaddress', async () => { + const acctHotDefault = await walletHot.getAccount('default'); + addressHot = acctHotDefault.receiveAddress; + + txid = await wclient.execute('sendtoaddress', [addressHot, 0.1234]); + assert.strictEqual(txid.length, 64); + }); + + it('should rpc sendmany', async () => { + const sendTo = {}; + sendTo[addressHot] = 1.0; + sendTo[addressMiner] = 0.1111; + txid = await wclient.execute('sendmany', ['default', sendTo]); + assert.strictEqual(txid.length, 64); + }); + + it('should fail malformed rpc sendmany', async () => { + await assert.asyncThrows(async () => { + await wclient.execute('sendmany', ['default', null]); + }, 'Invalid send-to address'); + + const sendTo = {}; + sendTo[addressHot] = null; + await assert.asyncThrows(async () => { + await wclient.execute('sendmany', ['default', sendTo]); + }, 'Invalid amount.'); + }); + + it('should rpc listreceivedbyaddress', async () => { + await wclient.execute('selectwallet', ['hot']); + + const listZeroConf = await wclient.execute('listreceivedbyaddress', + [0, false, false]); + assert.deepStrictEqual(listZeroConf, [{ + 'involvesWatchonly': false, + 'address': addressHot, + 'account': 'default', + 'amount': 1.1234, + 'confirmations': 0, + 'label': '' + }]); + + blocks.push(await nclient.execute('generatetoaddress', [1, addressMiner])); + await wdb.syncChain(); + + const listSomeConf = await wclient.execute('listreceivedbyaddress', + [1, false, false]); + assert.deepStrictEqual(listSomeConf, [{ + 'involvesWatchonly': false, + 'address': addressHot, + 'account': 'default', + 'amount': 1.1234, + 'confirmations': 1, + 'label': '' + }]); + + const listTooManyConf = await wclient.execute('listreceivedbyaddress', + [100, false, false]); + assert.deepStrictEqual(listTooManyConf, []); + }); + + it('should rpc listtransactions with no args', async () => { + const txs = await wclient.execute('listtransactions', []); + assert.strictEqual(txs.length, 2); + assert.strictEqual(txs[0].amount + txs[1].amount, 1.1234); + assert.strictEqual(txs[0].account, 'default'); + }); + + it('should rpc listtransactions from specified account', async () => { + const wallet = await wclient.wallet('hot'); + await wallet.createAccount('foo'); + + const txs = await wclient.execute('listtransactions', ['foo']); + assert.strictEqual(txs.length, 0); + }); + + it('should fail rpc listtransactions from nonexistent account', async () => { + assert.asyncThrows(async () => { + await wclient.execute('listtransactions', ['nonexistent']); + }, 'Account not found.'); + }); + + it('should rpc listunspent', async () => { + utxo = await wclient.execute('listunspent', []); + assert.strictEqual(utxo.length, 2); + }); + + it('should rpc lockunspent and listlockunspent', async () => { + let result = await wclient.execute('listlockunspent', []); + assert.deepStrictEqual(result, []); + + // lock one utxo + const output = utxo[0]; + const outputsToLock = [{'txid': output.txid, 'vout': output.vout}]; + result = await wclient.execute('lockunspent', [false, outputsToLock]); + assert(result); + + result = await wclient.execute('listlockunspent', []); + assert.deepStrictEqual(result, outputsToLock); + + // unlock all + result = await wclient.execute('lockunspent', [true]); + assert(result); + + result = await wclient.execute('listlockunspent', []); + assert.deepStrictEqual(result, []); + }); + + it('should rpc listsinceblock', async () => { + const listNoBlock = await wclient.execute('listsinceblock', []); + assert.strictEqual(listNoBlock.transactions.length, 2); + // txs returned in unpredictable order + const txids = [ + listNoBlock.transactions[0].txid, + listNoBlock.transactions[1].txid + ]; + assert(txids.includes(txid)); + + const block5 = blocks[5]; + const listOldBlock = await wclient.execute('listsinceblock', [block5]); + assert.strictEqual(listOldBlock.transactions.length, 2); + + const nonexistentBlock = consensus.ZERO_HASH.toString('hex'); + await assert.asyncThrows(async () => { + await wclient.execute('listsinceblock', [nonexistentBlock]); + }, 'Block not found'); + }); + + it('should cleanup', async () => { + await walletHot.close(); + await walletMiner.close(); + await wclient.close(); + await nclient.close(); + await node.close(); + }); +}); diff --git a/test/util/assert.js b/test/util/assert.js index c3a9e56b..e1bdcb3e 100644 --- a/test/util/assert.js +++ b/test/util/assert.js @@ -107,6 +107,18 @@ assert.notBufferEqual = function notBufferEqual(actual, expected, message) { } }; +// node V10 implements assert.rejects() but this is compatible with V8 +assert.asyncThrows = async function asyncThrows(func, expectedError) { + let err = null; + try { + await func(); + } catch (e) { + err = e; + } + const re = new RegExp('^' + expectedError); + assert(re.test(err.message)); +}; + function _isString(value, message, stackStartFunction) { if (typeof value !== 'string') { throw new assert.AssertionError({