From 6fa20139ea48d82a3f9f0a959f8ebf06b5c944ce Mon Sep 17 00:00:00 2001 From: Nodari Chkuaselidze Date: Sat, 30 Mar 2024 23:55:57 +0400 Subject: [PATCH] wallet-rpc: add new methods for listing history. wallet-rpc: The following new methods have been added: - `listhistory` - List history with a limit and in reverse order. - `listhistoryafter` - List history after a txid _(subsequent pages)_. - `listhistorybytime` - List history by giving a timestamp in epoch seconds _(block median time past)_. - `listunconfirmed` - List unconfirmed transactions with a limit and in reverse order. - `listunconfirmedafter` - List unconfirmed transactions after a txid _(subsequent pages)_. - `listunconfirmedbytime` - List unconfirmed transactions by time they where added. wallet-rpc: The following methods have been deprecated: - `listtransactions` - Use `listhistory` and the related methods and the `after` argument for results that do not shift when new blocks arrive. wallet: Remove getHistory and related methods form wallet and txdb. --- CHANGELOG.md | 29 ++- lib/wallet/rpc.js | 351 +++++++++++++++++++++++----- lib/wallet/txdb.js | 98 +------- lib/wallet/wallet.js | 30 +-- lib/wallet/walletdb.js | 1 + test/util/pagination.js | 56 +++++ test/wallet-auction-test.js | 1 - test/wallet-http-test.js | 53 +---- test/wallet-rpc-test.js | 445 +++++++++++++++++++++++++++++++++++- 9 files changed, 843 insertions(+), 221 deletions(-) create mode 100644 test/util/pagination.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 74cba90d4..88c6c30a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,11 +99,6 @@ These endpoints have been deprecated: - `GET /wallet/:id/tx/last` - Instead use `reverse` param for the history and unconfirmed endpoints. -##### Wallet CLI (hsw-cli) - - `history` now accepts new args on top of `--account`: `--reverse`, - `--limit`, `--after`, `--after`. - - `pending` now accepts new args, same as above. - ##### Examples ``` @@ -133,6 +128,30 @@ GET /wallet/:id/tx/unconfirmed?time=&limit=50&reverse=false The same will apply to unconfirmed transactions. The `time` is in epoch seconds and indexed based on when the transaction was added to the wallet. +##### Wallet RPC + +The following new methods have been added: + - `listhistory` - List history with a limit and in reverse order. + - `listhistoryafter` - List history after a txid _(subsequent pages)_. + - `listhistorybytime` - List history by giving a timestamp in epoch seconds + _(block median time past)_. + - `listunconfirmed` - List unconfirmed transactions with a limit and in + reverse order. + - `listunconfirmedafter` - List unconfirmed transactions after a txid + _(subsequent pages)_. + - `listunconfirmedbytime` - List unconfirmed transactions by time they + where added. + +The following methods have been deprecated: + +- `listtransactions` - Use `listhistory` and the related methods and the + `after` argument for results that do not shift when new blocks arrive. + +##### Wallet CLI (hsw-cli) + - `history` now accepts new args on top of `--account`: `--reverse`, + `--limit`, `--after`, `--after`. + - `pending` now accepts new args, same as above. + ### Client changes #### Wallet HTTP Client diff --git a/lib/wallet/rpc.js b/lib/wallet/rpc.js index f85bf5596..afa1bbd1f 100644 --- a/lib/wallet/rpc.js +++ b/lib/wallet/rpc.js @@ -98,6 +98,8 @@ class RPC extends RPCBase { this.wallet = null; + this.maxTXs = this.wdb.options.maxHistoryTXs; + this.init(); } @@ -161,6 +163,12 @@ class RPC extends RPCBase { this.add('listreceivedbyaddress', this.listReceivedByAddress); this.add('listsinceblock', this.listSinceBlock); this.add('listtransactions', this.listTransactions); + this.add('listhistory', this.listHistory); + this.add('listhistoryafter', this.listHistoryAfter); + this.add('listhistorybytime', this.listHistoryByTime); + this.add('listunconfirmed', this.listUnconfirmed); + this.add('listunconfirmedafter', this.listUnconfirmedAfter); + this.add('listunconfirmedbytime', this.listUnconfirmedByTime); this.add('listunspent', this.listUnspent); this.add('lockunspent', this.lockUnspent); this.add('sendfrom', this.sendFrom); @@ -576,25 +584,42 @@ class RPC extends RPCBase { for (const path of paths) filter.add(path.hash); - const txs = await wallet.getHistory(name); + let txs = await wallet.listHistory(name, { + limit: this.maxTXs, + reverse: false + }); let total = 0; let lastConf = -1; - for (const wtx of txs) { - const conf = wtx.getDepth(height); + // While this doesn't consume a lot of memory + // it can still consume a lot of CPU and be very + // slow. If this is a common information to query, + // it would be better to calculate the total per + // account while indexing and adding blocks. - if (conf < minconf) - continue; + while (txs.length) { + for (const wtx of txs) { + const conf = wtx.getDepth(height); + + if (conf < minconf) + continue; - if (lastConf === -1 || conf < lastConf) - lastConf = conf; + if (lastConf === -1 || conf < lastConf) + lastConf = conf; - for (const output of wtx.tx.outputs) { - const hash = output.getHash(); - if (hash && filter.has(hash)) - total += output.value; + for (const output of wtx.tx.outputs) { + const hash = output.getHash(); + if (hash && filter.has(hash)) + total += output.value; + } } + + txs = await wallet.listHistoryAfter(name, { + hash: txs[txs.length - 1].hash, + limit: this.maxTXs, + reverse: false + }); } return Amount.coin(total, true); @@ -613,18 +638,36 @@ class RPC extends RPCBase { const height = this.wdb.state.height; const hash = parseHash(addr, this.network); - const txs = await wallet.getHistory(); + + // While this doesn't consume a lot of memory + // it can still consume a lot of CPU and be very + // slow. If this is a common information to query, + // it would be better to calculate the total per + // address while indexing and adding blocks. + let txs = await wallet.listHistory(null, { + limit: this.maxTXs, + reverse: false + }); let total = 0; - for (const wtx of txs) { - if (wtx.getDepth(height) < minconf) - continue; + while (txs.length) { + for (const wtx of txs) { + if (wtx.getDepth(height) < minconf) + continue; - for (const output of wtx.tx.outputs) { - if (output.getHash().equals(hash)) - total += output.value; + for (const output of wtx.tx.outputs) { + const ohash = output.getHash(); + if (ohash && ohash.equals(hash)) + total += output.value; + } } + + txs = await wallet.listHistoryAfter(null, { + hash: txs[txs.length - 1].hash, + limit: this.maxTXs, + reverse: false + }); } return Amount.coin(total, true); @@ -1033,30 +1076,48 @@ class RPC extends RPCBase { }); } - const txs = await wallet.getHistory(); - - for (const wtx of txs) { - const conf = wtx.getDepth(height); + // With large number of paths this could consume a lot + // of memory and give back large results. There is also + // the potential for the query to have a large CPU hit + // and be slow. If this is a common to query, it would + // be better to calculate while indexing and adding + // blocks instead of at query time. - if (conf < minconf) - continue; + let txs = await wallet.listHistory(null, { + limit: this.maxTXs, + reverse: false + }); - for (const output of wtx.tx.outputs) { - const addr = output.getAddress(); + while (txs.length) { + for (const wtx of txs) { + const conf = wtx.getDepth(height); - if (!addr) + if (conf < minconf) continue; - const hash = addr.getHash(); - const entry = map.get(hash); + for (const output of wtx.tx.outputs) { + const addr = output.getAddress(); - if (entry) { - if (entry.confirmations === -1 || conf < entry.confirmations) - entry.confirmations = conf; - entry.address = addr.toString(this.network); - entry.amount += output.value; + if (!addr) + continue; + + const hash = addr.getHash(); + const entry = map.get(hash); + + if (entry) { + if (entry.confirmations === -1 || conf < entry.confirmations) + entry.confirmations = conf; + entry.address = addr.toString(this.network); + entry.amount += output.value; + } } } + + txs = await wallet.listHistoryAfter(null, { + hash: txs[txs.length -1].hash, + limit: this.maxTXs, + reverse: false + }); } let out = []; @@ -1127,24 +1188,35 @@ class RPC extends RPCBase { if (height === -1) height = chainHeight; - const txs = await wallet.getHistory(); + let txs = await wallet.listHistory(null, { + limit: this.maxTXs, + reverse: false + }); const out = []; let highest = null; - for (const wtx of txs) { - if (wtx.height < height) - continue; + while (txs.length) { + for (const wtx of txs) { + if (wtx.height < height) + continue; - if (wtx.getDepth(chainHeight) < minconf) - continue; + if (wtx.getDepth(chainHeight) < minconf) + continue; - if (!highest || wtx.height > highest) - highest = wtx; + if (!highest || wtx.height > highest) + highest = wtx; - const json = await this._toListTX(wtx); + const json = await this._toListTX(wtx); + + out.push(json); + } - out.push(json); + txs = await wallet.listHistoryAfter(null, { + hash: txs[txs.length - 1].hash, + limit: this.maxTXs, + reverse: false + }); } return { @@ -1236,41 +1308,200 @@ class RPC extends RPCBase { } async listTransactions(args, help) { + throw new Error('Deprecated: `listtransactions`. ' + + 'Use `listhistory` and related methods.'); + } + + async listHistory(args, help) { if (help || args.length > 4) { throw new RPCError(errs.MISC_ERROR, - 'listtransactions ( "account" count from includeWatchonly)'); + 'listhistory "account" ( limit, reverse )'); } const wallet = this.wallet; const valid = new Validator(args); let name = valid.str(0); - const count = valid.u32(1, 10); - const from = valid.u32(2, 0); - const watchOnly = valid.bool(3, false); + const limit = valid.u32(1, this.maxTXs); + const reverse = valid.bool(2, false); - if (wallet.watchOnly !== watchOnly) - return []; + if (limit > this.maxTXs) { + throw new RPCError(errs.INVALID_PARAMETER, + `Limit above max of ${this.maxTXs}.`); + } - if (name === '') - name = 'default'; + if (name === '*') + name = null; + + const recs = await wallet.listHistory(name, {limit, reverse}); + + const out = []; + for (let i = 0; i < recs.length; i++) + out.push(await this._toListTX(recs[i])); + + return out; + } + + async listHistoryAfter(args, help) { + if (help || args.length > 4 || args.length < 2) { + throw new RPCError(errs.MISC_ERROR, + 'listhistoryafter "account", "txid" ( limit, reverse )'); + } + + const wallet = this.wallet; + const valid = new Validator(args); + let name = valid.str(0); + const hash = valid.bhash(1); + const limit = valid.u32(2, this.maxTXs); + const reverse = valid.bool(3, false); + + if (limit > this.maxTXs) { + throw new RPCError(errs.INVALID_PARAMETER, + `Limit above max of ${this.maxTXs}.`); + } if (name === '*') name = null; - const txs = await wallet.getHistory(name); + const recs = await wallet.listHistoryAfter(name, { + hash, + limit, + reverse + }); - common.sortTX(txs); + const out = []; + for (let i = 0; i < recs.length; i++) + out.push(await this._toListTX(recs[i])); + + return out; + } + + async listHistoryByTime(args, help) { + if (help || args.length > 4 || args.length < 2) { + throw new RPCError(errs.MISC_ERROR, + 'listhistorybytime "account", "timestamp" ( limit, reverse )'); + } + + const wallet = this.wallet; + const valid = new Validator(args); + let name = valid.str(0); + const time = valid.uint(1); + const limit = valid.u32(2, this.maxTXs); + const reverse = valid.bool(3, false); + + if (limit > this.maxTXs) { + throw new RPCError(errs.INVALID_PARAMETER, + `Limit above max of ${this.maxTXs}.`); + } + + if (name === '*') + name = null; + + const recs = await wallet.listHistoryByTime(name, {time, limit, reverse}); + + const out = []; + for (let i = 0; i < recs.length; i++) + out.push(await this._toListTX(recs[i])); + + return out; + } + + async listUnconfirmed(args, help) { + if (help || args.length > 4) { + throw new RPCError(errs.MISC_ERROR, + 'listunconfirmed "account" ( limit, reverse )'); + } + + const wallet = this.wallet; + const valid = new Validator(args); + let name = valid.str(0); + const limit = valid.u32(1, this.maxTXs); + const reverse = valid.bool(2, false); + + if (limit > this.maxTXs) { + throw new RPCError(errs.INVALID_PARAMETER, + `Limit above max of ${this.maxTXs}.`); + } + + if (name === '*') + name = null; + + const recs = await wallet.listUnconfirmed(name, {limit, reverse}); + + const out = []; + for (let i = 0; i < recs.length; i++) + out.push(await this._toListTX(recs[i])); + + return out; + } + + async listUnconfirmedAfter(args, help) { + if (help || args.length > 4 || args.length < 2) { + throw new RPCError(errs.MISC_ERROR, + 'listunconfirmedafter "account", "txid" ( limit, reverse )'); + } + + const wallet = this.wallet; + const valid = new Validator(args); + let name = valid.str(0); + const hash = valid.bhash(1); + const limit = valid.u32(2, this.maxTXs); + const reverse = valid.bool(3, false); + + if (!hash) + throw new RPCError(errs.INVALID_PARAMETER, 'Missing txid parameter.'); + + if (limit > this.maxTXs) { + throw new RPCError(errs.INVALID_PARAMETER, + `Limit above max of ${this.maxTXs}.`); + } + + if (name === '*') + name = null; + + const recs = await wallet.listUnconfirmedAfter(name, { + hash, + limit, + reverse + }); - const end = from + count; - const to = Math.min(end, txs.length); const out = []; + for (let i = 0; i < recs.length; i++) + out.push(await this._toListTX(recs[i])); + + return out; + } - for (let i = from; i < to; i++) { - const wtx = txs[i]; - const json = await this._toListTX(wtx); - out.push(json); + async listUnconfirmedByTime(args, help) { + if (help || args.length > 4 || args.length < 2) { + throw new RPCError(errs.MISC_ERROR, + 'listunconfirmedbytime "account", "timestamp" ( limit, reverse )'); } + const wallet = this.wallet; + const valid = new Validator(args); + let name = valid.str(0); + const time = valid.uint(1); + const limit = valid.u32(2, this.maxTXs); + const reverse = valid.bool(3, false); + + if (limit > this.maxTXs) { + throw new RPCError(errs.INVALID_PARAMETER, + `Limit above max of ${this.maxTXs}.`); + } + + if (name === '*') + name = null; + + const recs = await wallet.listUnconfirmedByTime(name, { + time, + limit, + reverse + }); + + const out = []; + for (let i = 0; i < recs.length; i++) + out.push(await this._toListTX(recs[i])); + return out; } diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index 6d84f817c..bba57cea9 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -26,6 +26,7 @@ const {TXRecord} = records; const {types} = rules; /** @typedef {import('./records').BlockMeta} BlockMeta */ +/** @typedef {import('./records').TXRecord} TXRecord */ /** @typedef {import('./walletdb')} WalletDB */ /** @@ -2128,7 +2129,7 @@ class TXDB { * @param {Object} options * @param {Number} options.limit * @param {Boolean} options.reverse - * @returns {Promise} + * @returns {Promise} */ async listHistory(acct, options) { @@ -2172,7 +2173,7 @@ class TXDB { * @param {Buffer} options.time * @param {Number} options.limit * @param {Boolean} options.reverse - * @returns {Promise} + * @returns {Promise} */ async listHistoryByTime(acct, options) { @@ -2244,7 +2245,7 @@ class TXDB { * @param {Buffer} options.hash * @param {Number} options.limit * @param {Boolean} options.reverse - * @returns {Promise} + * @returns {Promise} */ async listHistoryAfter(acct, options) { @@ -2267,7 +2268,7 @@ class TXDB { * @param {Buffer} options.hash * @param {Number} options.limit * @param {Boolean} options.reverse - * @returns {Promise} + * @returns {Promise} */ async listHistoryFrom(acct, options) { @@ -2292,7 +2293,7 @@ class TXDB { * @param {Number} options.limit * @param {Boolean} options.reverse * @param {Boolean} options.inclusive - * @returns {Promise} + * @returns {Promise} */ async _listHistory(acct, options) { @@ -2352,7 +2353,7 @@ class TXDB { * @param {Object} options * @param {Number} options.limit * @param {Boolean} options.reverse - * @returns {Promise} - Returns {@link TX}[]. + * @returns {Promise} */ async listUnconfirmed(acct, options) { @@ -2397,7 +2398,7 @@ class TXDB { * @param {Object} options * @param {Number} options.limit * @param {Boolean} options.reverse - * @returns {Promise} + * @returns {Promise} */ async listUnconfirmedByTime(acct, options) { @@ -2469,7 +2470,7 @@ class TXDB { * @param {Buffer} options.hash * @param {Number} options.limit * @param {Boolean} options.reverse - * @returns {Promise} + * @returns {Promise} */ async listUnconfirmedAfter(acct, options) { @@ -2493,7 +2494,7 @@ class TXDB { * @param {Buffer} options.hash * @param {Number} options.limit * @param {Boolean} options.reverse - * @returns {Promise} + * @returns {Promise} */ async listUnconfirmedFrom(acct, options) { @@ -2520,7 +2521,7 @@ class TXDB { * @param {Number} options.limit * @param {Boolean} options.reverse * @param {Boolean} options.inclusive - * @returns {Promise} + * @returns {Promise} */ async _listUnconfirmed(acct, options) { @@ -3310,24 +3311,6 @@ class TXDB { return outpoints; } - /** - * Get hashes of all transactions in the database. - * @param {Number} acct - * @returns {Promise} - */ - - getAccountHistoryHashes(acct) { - assert(typeof acct === 'number'); - return this.bucket.keys({ - gte: layout.T.min(acct), - lte: layout.T.max(acct), - parse: (key) => { - const [, hash] = layout.T.decode(key); - return hash; - } - }); - } - /** * Test whether an account owns a coin. * @param {Number} acct @@ -3342,25 +3325,6 @@ class TXDB { return this.bucket.has(layout.C.encode(acct, hash, index)); } - /** - * Get hashes of all transactions in the database. - * @param {Number} acct - * @returns {Promise} - */ - - getHistoryHashes(acct) { - assert(typeof acct === 'number'); - - if (acct !== -1) - return this.getAccountHistoryHashes(acct); - - return this.bucket.keys({ - gte: layout.t.min(), - lte: layout.t.max(), - parse: key => layout.t.decode(key)[0] - }); - } - /** * Get hashes of all unconfirmed transactions in the database. * @param {Number} acct @@ -3519,46 +3483,6 @@ class TXDB { return this.getHeightRangeHashes({ start: height, end: height }); } - /** - * Get all transactions. - * @param {Number} acct - * @returns {Promise} - */ - - getHistory(acct) { - assert(typeof acct === 'number'); - - // Slow case - if (acct !== -1) - return this.getAccountHistory(acct); - - // Fast case - return this.bucket.values({ - gte: layout.t.min(), - lte: layout.t.max(), - parse: data => TXRecord.decode(data) - }); - } - - /** - * Get all acct transactions. - * @param {Number} acct - * @returns {Promise} - */ - - async getAccountHistory(acct) { - const hashes = await this.getHistoryHashes(acct); - const txs = []; - - for (const hash of hashes) { - const tx = await this.getTX(hash); - assert(tx); - txs.push(tx); - } - - return txs; - } - /** * Get unconfirmed transactions. * @param {Number} acct diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 1f47c993a..5ad75c89e 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -41,6 +41,7 @@ const Coin = require('../primitives/coin'); const Outpoint = require('../primitives/outpoint'); /** @typedef {import('./records').BlockMeta} BlockMeta */ +/** @typedef {import('./records').TXRecord} TXRecord */ /** @typedef {import('../primitives/tx')} TX */ /** @typedef {import('./txdb').BlockExtraInfo} BlockExtraInfo */ @@ -4925,17 +4926,6 @@ class Wallet extends EventEmitter { return this.txdb.getLocked(); } - /** - * Get all transactions in transaction history. - * @param {(String|Number)?} acct - * @returns {Promise} - Returns {@link TX}[]. - */ - - async getHistory(acct) { - const account = await this.ensureIndex(acct); - return this.txdb.getHistory(account); - } - /** * Get all available coins. * @param {(String|Number)?} account @@ -5019,12 +5009,12 @@ class Wallet extends EventEmitter { return this.txdb.getBalance(account); } - /** + /** * @param {(String|Number)} [acct] * @param {Object} options * @param {Number} options.limit * @param {Boolean} options.reverse - * @returns {Promise} + * @returns {Promise} */ async listHistory(acct, options) { @@ -5038,7 +5028,7 @@ class Wallet extends EventEmitter { * @param {String} options.hash * @param {Number} options.limit * @param {Boolean} options.reverse - * @returns {Promise} + * @returns {Promise} */ async listHistoryAfter(acct, options) { @@ -5052,7 +5042,7 @@ class Wallet extends EventEmitter { * @param {String} options.hash * @param {Number} options.limit * @param {Boolean} options.reverse - * @returns {Promise} + * @returns {Promise} */ async listHistoryFrom(acct, options) { @@ -5066,7 +5056,7 @@ class Wallet extends EventEmitter { * @param {Number} options.time - Time in seconds. * @param {Number} options.limit * @param {Boolean} options.reverse - * @returns {Promise} + * @returns {Promise} */ async listHistoryByTime(acct, options) { @@ -5079,7 +5069,7 @@ class Wallet extends EventEmitter { * @param {Object} options * @param {Number} options.limit * @param {Boolean} options.reverse - * @returns {Promise} + * @returns {Promise} */ async listUnconfirmed(acct, options) { @@ -5093,7 +5083,7 @@ class Wallet extends EventEmitter { * @param {Buffer} options.hash * @param {Number} options.limit * @param {Boolean} options.reverse - * @returns {Promise} + * @returns {Promise} */ async listUnconfirmedAfter(acct, options) { @@ -5107,7 +5097,7 @@ class Wallet extends EventEmitter { * @param {Buffer} options.hash * @param {Number} options.limit * @param {Boolean} options.reverse - * @returns {Promise} + * @returns {Promise} */ async listUnconfirmedFrom(acct, options) { @@ -5121,7 +5111,7 @@ class Wallet extends EventEmitter { * @param {Number} options.time - Time in seconds. * @param {Number} options.limit * @param {Boolean} options.reverse - * @returns {Promise} + * @returns {Promise} */ async listUnconfirmedByTime(acct, options) { diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index f0ab361a6..93d0e89a4 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -2823,6 +2823,7 @@ class WalletOptions { if (options.maxHistoryTXs != null) { assert((options.maxHistoryTXs >>> 0) === options.maxHistoryTXs); + assert(options.maxHistoryTXs > 0); this.maxHistoryTXs = options.maxHistoryTXs; } diff --git a/test/util/pagination.js b/test/util/pagination.js new file mode 100644 index 000000000..c778850a0 --- /dev/null +++ b/test/util/pagination.js @@ -0,0 +1,56 @@ +'use strict'; + +const assert = require('bsert'); +const {forEventCondition} = require('./common'); + +exports.generateInitialBlocks = async (options) => { + const { + nodeCtx, + coinbase, + sendTXs, + singleAccount, + genesisTime + } = options; + + const blockInterval = 600; + const timewrap = 3200; + + async function mineBlock(coinbase, wrap = false) { + const height = nodeCtx.height; + let blocktime = genesisTime + (height + 1) * blockInterval; + + if (wrap && height % 5) + blocktime -= timewrap; + + await nodeCtx.nclient.execute('setmocktime', [blocktime]); + + const blocks = await nodeCtx.mineBlocks(1, coinbase); + const block = await nodeCtx.nclient.execute('getblock', [blocks[0]]); + + assert(block.time <= blocktime + 1); + assert(block.time >= blocktime); + + return block; + } + + let c = 0; + + // Establish baseline block interval for a median time + for (; c < 11; c++) + await mineBlock(coinbase); + + const h20 = entry => entry.height === 20; + const walletEvents = forEventCondition(nodeCtx.wdb, 'block connect', h20); + + for (; c < 20; c++) + await mineBlock(coinbase, true); + + await walletEvents; + + // 20 blocks * (20 txs per wallet, 19 default + 1 single account) + for (; c < 40; c++) { + await sendTXs(19); + await sendTXs(1, singleAccount); + await mineBlock(coinbase, true); + } +}; diff --git a/test/wallet-auction-test.js b/test/wallet-auction-test.js index 0820aee02..b733e871e 100644 --- a/test/wallet-auction-test.js +++ b/test/wallet-auction-test.js @@ -846,7 +846,6 @@ describe('Wallet Auction', function() { ...historyOptions })); - console.log(txs.length); const wtxs = await wallet.toDetails(txs); for (const wtx of wtxs) { for (const output of wtx.outputs) diff --git a/test/wallet-http-test.js b/test/wallet-http-test.js index dd3189906..f7d63b975 100644 --- a/test/wallet-http-test.js +++ b/test/wallet-http-test.js @@ -18,7 +18,8 @@ const common = require('./util/common'); const Outpoint = require('../lib/primitives/outpoint'); const consensus = require('../lib/protocol/consensus'); const NodeContext = require('./util/node-context'); -const {forEvent, forEventCondition, sleep} = require('./util/common'); +const {forEvent, sleep} = require('./util/common'); +const {generateInitialBlocks} = require('./util/pagination'); const { treeInterval, @@ -2281,10 +2282,7 @@ describe('Wallet HTTP', function() { }); describe('Wallet TX pagination', function() { - const BLOCK_INTERVAL = 3200; const GENESIS_TIME = 1580745078; - const TIME_WARP = 3200; - const START_TIME = GENESIS_TIME + BLOCK_INTERVAL; // account to receive single tx per block. const SINGLE_ACCOUNT = 'single'; @@ -2303,24 +2301,6 @@ describe('Wallet HTTP', function() { await mempoolTXs; } - async function mineBlock(coinbase, wrap = false) { - const height = nodeCtx.height; - let blocktime = START_TIME + height * BLOCK_INTERVAL; - - if (wrap && height % 5) - blocktime -= TIME_WARP; - - await nclient.execute('setmocktime', [blocktime]); - - const blocks = await nodeCtx.mineBlocks(1, coinbase); - const block = await nclient.execute('getblock', [blocks[0]]); - - assert(block.time <= blocktime + 1); - assert(block.time >= blocktime); - - return block; - } - before(async () => { await beforeAll(); @@ -2332,29 +2312,14 @@ describe('Wallet HTTP', function() { const fundAddress = (await fundWallet.createAddress('default')).address; - let c = 0; - - // Establish baseline block interval for a median time - for (; c < 11; c++) - await mineBlock(fundAddress); - - const walletEvents = forEventCondition(nodeCtx.wdb, 'block connect', (entry) => { - return entry.height === 20; + await generateInitialBlocks({ + nodeCtx, + sendTXs, + singleAccount: SINGLE_ACCOUNT, + coinbase: fundAddress, + genesisTime: GENESIS_TIME }); - // Mature coinbase transactions - for (; c < 20; c++) - await mineBlock(fundAddress, true); - - await walletEvents; - - // 20 blocks * (20 txs per wallet, 19 default + 1 single account) - for (; c < 40; c++) { - await sendTXs(19); - await sendTXs(1, SINGLE_ACCOUNT); - await mineBlock(fundAddress, true); - } - unconfirmedTime = Math.floor(Date.now() / 1000); // 20 txs unconfirmed @@ -2408,7 +2373,7 @@ describe('Wallet HTTP', function() { assert.strictEqual(two[19].confirmations, 5); assert.strictEqual(two[20].confirmations, 6); assert.strictEqual(two[99].confirmations, 9); - assert.notStrictEqual(two[0].hash, one[11].hash); + assert.notStrictEqual(two[0].hash, one[99].hash); }); it('first page (w/ account)', async () => { diff --git a/test/wallet-rpc-test.js b/test/wallet-rpc-test.js index fa11742c6..81b8e94af 100644 --- a/test/wallet-rpc-test.js +++ b/test/wallet-rpc-test.js @@ -9,6 +9,8 @@ const Address = require('../lib/primitives/address'); const rules = require('../lib/covenants/rules'); const Amount = require('../lib/ui/amount'); const NodeContext = require('./util/node-context'); +const {forEvent} = require('./util/common'); +const {generateInitialBlocks} = require('./util/pagination'); const {types} = rules; const {forValue} = require('./util/common'); @@ -43,7 +45,7 @@ describe('Wallet RPC Methods', function() { let xpub; - before(async () => { + const beforeAll = async () => { nodeCtx = new NodeContext({ network: network.type, apiKey: 'bar', @@ -71,11 +73,11 @@ describe('Wallet RPC Methods', function() { 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'about' ].join(' ')); - }); + }; - after(async () => { + const afterAll = async () => { await nodeCtx.close(); - }); + }; describe('getaddressinfo', () => { const watchOnlyWalletId = 'foo'; @@ -91,6 +93,8 @@ describe('Wallet RPC Methods', function() { // set up the initial testing state before(async () => { + await beforeAll(); + { // Set up the testing environment // by creating a wallet and a watch @@ -127,6 +131,8 @@ describe('Wallet RPC Methods', function() { }; }); + after(afterAll); + // the rpc interface requires the wallet to be selected first it('should return iswatchonly correctly', async () => { // m/44'/5355'/0'/0/0 @@ -243,6 +249,9 @@ describe('Wallet RPC Methods', function() { }); describe('signmessage', function() { + before(beforeAll); + after(afterAll); + const nonWalletAddress = 'rs1q7q3h4chglps004u3yn79z0cp9ed24rfrhvrxnx'; const message = 'This is just a test message'; @@ -364,6 +373,8 @@ describe('Wallet RPC Methods', function() { } before(async () => { + await beforeAll(); + // Create new wallets await wclient.createWallet('alice'); await wclient.createWallet('bob'); @@ -405,6 +416,8 @@ describe('Wallet RPC Methods', function() { // Still in reveal phase }); + after(afterAll); + it('should fail to sign before auction is closed', async () => { await wclient.execute('selectwallet', ['alice']); @@ -568,6 +581,7 @@ describe('Wallet RPC Methods', function() { let wallet; before(async () => { + await beforeAll(); // Prevent mempool from sending duplicate TXs back to the walletDB and txdb. // This will prevent a race condition when we need to remove spent (but // unconfirmed) outputs from the wallet so they can be reused in other tests. @@ -580,6 +594,8 @@ describe('Wallet RPC Methods', function() { await nclient.execute('generatetoaddress', [10, addr]); }); + after(afterAll); + it('should do an auction', async () => { const NAME1 = rules.grindName(GNAME_SIZE, 2, network); const NAME2 = rules.grindName(GNAME_SIZE, 3, network); @@ -689,6 +705,9 @@ describe('Wallet RPC Methods', function() { }); describe('Wallet RPC Auction', function() { + before(beforeAll); + after(afterAll); + let addr1, addr2, name1, name2; it('should create wallets', async () => { @@ -775,6 +794,7 @@ describe('Wallet RPC Methods', function() { let addr; before(async () => { + await beforeAll(); await wclient.createWallet('batchWallet'); wclient.wallet('batchWallet'); await wclient.execute('selectwallet', ['batchWallet']); @@ -782,6 +802,8 @@ describe('Wallet RPC Methods', function() { await nclient.execute('generatetoaddress', [100, addr]); }); + after(afterAll); + it('should have paths when creating batch', async () => { const json = await wclient.execute( 'createbatch', @@ -940,6 +962,7 @@ describe('Wallet RPC Methods', function() { let alexAddr, barrieAddr; before(async () => { + await beforeAll(); await wclient.createWallet('alex'); await wclient.createWallet('barrie'); await wclient.execute('selectwallet', ['alex']); @@ -948,6 +971,8 @@ describe('Wallet RPC Methods', function() { barrieAddr = await wclient.execute('getnewaddress', []); }); + after(afterAll); + async function getCoinbaseTXID(height) { const block = await nclient.execute('getblockbyheight', [height]); return block.tx[0]; @@ -1026,6 +1051,7 @@ describe('Wallet RPC Methods', function() { } before(async () => { + await beforeAll(); await wclient.createWallet('msAlice', { type: 'multisig', m: 2, @@ -1054,6 +1080,8 @@ describe('Wallet RPC Methods', function() { await nclient.execute('generatetoaddress', [100, addr]); }); + after(afterAll); + it('(alice) should open name for auction', async () => { await wclient.execute('selectwallet', ['msAlice']); @@ -1122,4 +1150,413 @@ describe('Wallet RPC Methods', function() { assert.strictEqual(ownedNames.length, 1); }); }); + + describe('transactions', function() { + const GENESIS_TIME = 1580745078; + + // account to receive single tx per block. + const SINGLE_ACCOUNT = 'single'; + const DEFAULT_ACCOUNT = 'default'; + + let fundWallet, testWallet, unconfirmedTime; + let fundAddress; + + async function sendTXs(count, account = DEFAULT_ACCOUNT) { + const mempoolTXs = forEvent(nodeCtx.mempool, 'tx', count); + + for (let i = 0; i < count; i++) { + const {address} = await testWallet.createAddress(account); + await fundWallet.send({ outputs: [{address, value: 1e6}] }); + } + + await mempoolTXs; + } + + before(async () => { + await beforeAll(); + await wclient.createWallet('test'); + fundWallet = wclient.wallet('primary'); + testWallet = wclient.wallet('test'); + + await testWallet.createAccount(SINGLE_ACCOUNT); + + fundAddress = (await fundWallet.createAddress('default')).address; + + await generateInitialBlocks({ + nodeCtx, + sendTXs, + singleAccount: SINGLE_ACCOUNT, + coinbase: fundAddress, + genesisTime: GENESIS_TIME + }); + + unconfirmedTime = Math.floor(Date.now() / 1000); + + // 20 txs unconfirmed + const all = forEvent(nodeCtx.wdb, 'tx', 20); + await sendTXs(20); + await all; + }); + + after(afterAll); + + beforeEach(async () => { + await wclient.execute('selectwallet', ['test']); + }); + + describe('getreceivedbyaccount', function() { + it('should get the correct balance', async () => { + const bal = await wclient.execute('getreceivedbyaccount', + [SINGLE_ACCOUNT]); + assert.strictEqual(bal, 20); + }); + }); + + describe('listreceivedbyaccount', function() { + it('should get expected number of results', async () => { + const res = await wclient.execute('listreceivedbyaccount'); + assert.strictEqual(res.length, 2); + }); + }); + + describe('getreceivedbyaddress', function() { + it('should get the correct balance', async () => { + await wclient.execute('selectwallet', ['primary']); + const bal = await wclient.execute('getreceivedbyaddress', + [fundAddress]); + assert.strictEqual(bal, 80001.12); + }); + }); + + describe('listreceivedbyaddress', function() { + it('should get expected number of results', async () => { + const res = await wclient.execute('listreceivedbyaddress'); + assert.strictEqual(res.length, 420); + }); + }); + + describe('listsinceblock', function() { + it('should get expected number of results', async () => { + const res = await wclient.execute('listsinceblock'); + assert.strictEqual(res.transactions.length, 20); + }); + }); + + describe('listhistory', function() { + it('should get wallet history (desc)', async () => { + const history = await wclient.execute('listhistory', ['*', 100, true]);; + assert.strictEqual(history.length, 100); + assert.strictEqual(history[0].confirmations, 0); + assert.strictEqual(history[19].confirmations, 0); + assert.strictEqual(history[20].confirmations, 1); + assert.strictEqual(history[39].confirmations, 1); + assert.strictEqual(history[40].confirmations, 2); + assert.strictEqual(history[99].confirmations, 4); + }); + + it('should get wallet history (desc w/ account)', async () => { + const history = await wclient.execute('listhistory', + [SINGLE_ACCOUNT, 100, true]); + + assert.strictEqual(history.length, 20); + assert.strictEqual(history[0].confirmations, 1); + assert.strictEqual(history[1].confirmations, 2); + assert.strictEqual(history[2].confirmations, 3); + }); + + it('should get wallet history (asc)', async () => { + const history = await wclient.execute('listhistory', ['*', 100, false]); + assert.strictEqual(history.length, 100); + + assert.strictEqual(history[0].confirmations, 20); + assert.strictEqual(history[19].confirmations, 20); + assert.strictEqual(history[20].confirmations, 19); + assert.strictEqual(history[39].confirmations, 19); + assert.strictEqual(history[40].confirmations, 18); + assert.strictEqual(history[99].confirmations, 16); + }); + + it('should get wallet history (asc w/ account)', async () => { + const history = await wclient.execute('listhistory', + [SINGLE_ACCOUNT, 100, false]); + + assert.strictEqual(history.length, 20); + assert.strictEqual(history[0].confirmations, 20); + assert.strictEqual(history[1].confirmations, 19); + assert.strictEqual(history[19].confirmations, 1); + }); + }); + + describe('listhistoryafter', function() { + it('should get wallet history after (desc)', async () => { + const history = await wclient.execute('listhistory', ['*', 100, true]); + const historyAfter = await wclient.execute('listhistoryafter', + ['*', history[99].txid, 100, true]); + + assert.strictEqual(historyAfter.length, 100); + assert.strictEqual(historyAfter[0].confirmations, 5); + assert.strictEqual(historyAfter[19].confirmations, 5); + assert.strictEqual(historyAfter[20].confirmations, 6); + assert.strictEqual(historyAfter[99].confirmations, 9); + assert.notStrictEqual(historyAfter[0].txid, history[99].txid); + }); + + it('should get wallet history after (desc w/ account)', async () => { + const history = await wclient.execute('listhistory', + [SINGLE_ACCOUNT, 10, true]); + + const historyAfter = await wclient.execute('listhistoryafter', + [SINGLE_ACCOUNT, history[9].txid, 10, true]); + + assert.strictEqual(historyAfter.length, 10); + assert.strictEqual(historyAfter[0].confirmations, 11); + assert.strictEqual(historyAfter[9].confirmations, 20); + assert.notStrictEqual(historyAfter[0].txid, history[9].txid); + }); + + it('should get wallet history after (asc)', async () => { + const history = await wclient.execute('listhistory', ['*', 100, false]); + const historyAfter = await wclient.execute('listhistoryafter', + ['*', history[99].txid, 100, false]); + + assert.strictEqual(historyAfter.length, 100); + assert.strictEqual(historyAfter[0].confirmations, 15); + assert.strictEqual(historyAfter[19].confirmations, 15); + assert.strictEqual(historyAfter[20].confirmations, 14); + assert.strictEqual(historyAfter[99].confirmations, 11); + assert.notStrictEqual(historyAfter[0].txid, history[99].txid); + }); + + it('should get wallet history after (asc w/ account)', async () => { + const history = await wclient.execute('listhistory', + [SINGLE_ACCOUNT, 10, false]); + const historyAfter = await wclient.execute('listhistoryafter', + [SINGLE_ACCOUNT, history[9].txid, 10, false]); + + assert.strictEqual(historyAfter.length, 10); + assert.strictEqual(historyAfter[0].confirmations, 10); + assert.strictEqual(historyAfter[9].confirmations, 1); + assert.notStrictEqual(historyAfter[0].txid, history[9].txid); + }); + }); + + describe('listhistorybytime', function() { + it('should get wallet history by time (desc)', async () => { + const time = Math.ceil(Date.now() / 1000); + // This will look latest first confirmed. (does not include unconfirmed) + const history = await wclient.execute('listhistorybytime', + ['*', time, 100, true]); + + assert.strictEqual(history.length, 100); + assert.strictEqual(history[0].confirmations, 1); + assert.strictEqual(history[19].confirmations, 1); + assert.strictEqual(history[20].confirmations, 2); + assert.strictEqual(history[99].confirmations, 5); + assert(history[0].confirmations <= history[99].confirmations); + }); + + it('should get wallet history by time (desc w/ account)', async () => { + const time = Math.ceil(Date.now() / 1000); + const history = await wclient.execute('listhistorybytime', + [SINGLE_ACCOUNT, time, 100, true]); + + assert.strictEqual(history.length, 20); + assert.strictEqual(history[0].confirmations, 1); + assert.strictEqual(history[19].confirmations, 20); + assert(history[0].confirmations <= history[19].confirmations); + }); + + it('should get wallet history by time (asc)', async () => { + const time = GENESIS_TIME; + const history = await wclient.execute('listhistorybytime', + ['*', time, 100, false]); + + assert.strictEqual(history.length, 100); + assert.strictEqual(history[0].confirmations, 20); + assert.strictEqual(history[19].confirmations, 20); + assert.strictEqual(history[20].confirmations, 19); + assert.strictEqual(history[99].confirmations, 16); + assert(history[0].confirmations >= history[99].confirmations); + }); + + it('should get wallet history by time (asc w/ account)', async () => { + const time = GENESIS_TIME; + const history = await wclient.execute('listhistorybytime', + [SINGLE_ACCOUNT, time, 100, false]); + + assert.strictEqual(history.length, 20); + assert.strictEqual(history[0].confirmations, 20); + assert.strictEqual(history[19].confirmations, 1); + assert(history[0].confirmations >= history[19].confirmations); + }); + }); + + describe('listunconfirmed', function() { + it('should get wallet unconfirmed txs (desc)', async () => { + const unconfirmed = await wclient.execute('listunconfirmed', + ['*', 100, true]); + + assert.strictEqual(unconfirmed.length, 20); + assert.strictEqual(unconfirmed[0].confirmations, 0); + assert.strictEqual(unconfirmed[19].confirmations, 0); + const a = unconfirmed[0].time; + const b = unconfirmed[19].time; + assert(a >= b); + }); + + it('should get wallet unconfirmed txs (desc w/ account)', async () => { + const unconfirmed = await wclient.execute('listunconfirmed', + [DEFAULT_ACCOUNT, 100, true]); + + assert.strictEqual(unconfirmed.length, 20); + assert.strictEqual(unconfirmed[0].confirmations, 0); + assert.strictEqual(unconfirmed[19].confirmations, 0); + const a = unconfirmed[0].time; + const b = unconfirmed[19].time; + assert(a >= b); + }); + + it('should get wallet unconfirmed txs (asc)', async () => { + const unconfirmed = await wclient.execute('listunconfirmed', + ['*', 100, false]); + + assert.strictEqual(unconfirmed.length, 20); + assert.strictEqual(unconfirmed[0].confirmations, 0); + assert.strictEqual(unconfirmed[19].confirmations, 0); + const a = unconfirmed[0].time; + const b = unconfirmed[19].time; + assert(a <= b); + }); + + it('should get wallet unconfirmed txs (asc w/ account)', async () => { + const unconfirmed = await wclient.execute('listunconfirmed', + [DEFAULT_ACCOUNT, 100, false]); + + assert.strictEqual(unconfirmed.length, 20); + assert.strictEqual(unconfirmed[0].confirmations, 0); + assert.strictEqual(unconfirmed[19].confirmations, 0); + const a = unconfirmed[0].time; + const b = unconfirmed[19].time; + assert(a <= b); + }); + }); + + describe('listunconfirmedafter', function() { + it('should get wallet unconfirmed txs after (desc)', async () => { + const unconfirmed = await wclient.execute('listunconfirmed', + ['*', 10, true]); + const unconfirmedAfter = await wclient.execute('listunconfirmedafter', + ['*', unconfirmed[9].txid, 10, true]); + + assert.strictEqual(unconfirmedAfter.length, 10); + assert.strictEqual(unconfirmedAfter[0].confirmations, 0); + assert.strictEqual(unconfirmedAfter[9].confirmations, 0); + assert.notStrictEqual(unconfirmedAfter[0].txid, unconfirmed[9].txid); + + const a = unconfirmedAfter[0].time; + const b = unconfirmedAfter[9].time; + assert(a >= b); + }); + + it('should get wallet unconfirmed txs after (desc w/ account)', async () => { + const unconfirmed = await wclient.execute('listunconfirmed', + [DEFAULT_ACCOUNT, 10, true]); + const unconfirmedAfter = await wclient.execute('listunconfirmedafter', + [DEFAULT_ACCOUNT, unconfirmed[9].txid, 10, true]); + + assert.strictEqual(unconfirmedAfter.length, 10); + assert.strictEqual(unconfirmedAfter[0].confirmations, 0); + assert.strictEqual(unconfirmedAfter[9].confirmations, 0); + assert.notStrictEqual(unconfirmedAfter[0].txid, unconfirmed[9].txid); + + const a = unconfirmedAfter[0].time; + const b = unconfirmedAfter[9].time; + assert(a >= b); + }); + + it('should get wallet unconfirmed txs after (asc)', async () => { + const unconfirmed = await wclient.execute('listunconfirmed', + ['*', 10, false]); + const unconfirmedAfter = await wclient.execute('listunconfirmedafter', + ['*', unconfirmed[9].txid, 10, false]); + + assert.strictEqual(unconfirmedAfter.length, 10); + assert.strictEqual(unconfirmedAfter[0].confirmations, 0); + assert.strictEqual(unconfirmedAfter[9].confirmations, 0); + assert.notStrictEqual(unconfirmedAfter[0].txid, unconfirmed[9].txid); + + const a = unconfirmedAfter[0].time; + const b = unconfirmedAfter[9].time; + assert(a <= b); + }); + + it('should get wallet unconfirmed txs after (asc w/ account)', async () => { + const unconfirmed = await wclient.execute('listunconfirmed', + [DEFAULT_ACCOUNT, 10, false]); + const unconfirmedAfter = await wclient.execute('listunconfirmedafter', + [DEFAULT_ACCOUNT, unconfirmed[9].txid, 10, false]); + + assert.strictEqual(unconfirmedAfter.length, 10); + assert.strictEqual(unconfirmedAfter[0].confirmations, 0); + assert.strictEqual(unconfirmedAfter[9].confirmations, 0); + assert.notStrictEqual(unconfirmedAfter[0].txid, unconfirmed[9].txid); + + const a = unconfirmedAfter[0].time; + const b = unconfirmedAfter[9].time; + assert(a <= b); + }); + }); + + describe('listunconfirmedbytime', function() { + it('should get wallet unconfirmed txs by time (desc)', async () => { + const time = Math.ceil((Date.now() + 2000) / 1000); + const unconfirmed = await wclient.execute('listunconfirmedbytime', + ['*', time, 20, true]); + + assert.strictEqual(unconfirmed.length, 20); + assert.strictEqual(unconfirmed[0].confirmations, 0); + assert.strictEqual(unconfirmed[19].confirmations, 0); + const a = unconfirmed[0].time; + const b = unconfirmed[19].time; + assert(a >= b); + }); + + it('should get wallet unconfirmed txs by time (desc w/ account)', async () => { + const time = Math.ceil((Date.now() + 2000) / 1000); + const unconfirmed = await wclient.execute('listunconfirmedbytime', + [DEFAULT_ACCOUNT, time, 20, true]); + + assert.strictEqual(unconfirmed.length, 20); + assert.strictEqual(unconfirmed[0].confirmations, 0); + assert.strictEqual(unconfirmed[19].confirmations, 0); + const a = unconfirmed[0].time; + const b = unconfirmed[19].time; + assert(a >= b); + }); + + it('should get wallet unconfirmed txs by time (asc)', async () => { + const unconfirmed = await wclient.execute('listunconfirmedbytime', + ['*', unconfirmedTime, 20, false]); + + assert.strictEqual(unconfirmed.length, 20); + assert.strictEqual(unconfirmed[0].confirmations, 0); + assert.strictEqual(unconfirmed[19].confirmations, 0); + const a = unconfirmed[0].time; + const b = unconfirmed[19].time; + assert(a <= b); + }); + + it('should get wallet unconfirmed txs by time (asc w/ account)', async () => { + const unconfirmed = await wclient.execute('listunconfirmedbytime', + [DEFAULT_ACCOUNT, unconfirmedTime, 20, false]); + + assert.strictEqual(unconfirmed.length, 20); + assert.strictEqual(unconfirmed[0].confirmations, 0); + assert.strictEqual(unconfirmed[19].confirmations, 0); + const a = unconfirmed[0].time; + const b = unconfirmed[19].time; + assert(a <= b); + }); + }); + }); });