diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f972564f8..69f8b6f8b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -57,7 +57,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node: [14.x, 16.x, 18.x, 20.x, 22.x, 24.x] + node: [16.x, 18.x, 20.x, 22.x, 24.x] steps: - uses: actions/checkout@v4 @@ -71,12 +71,6 @@ jobs: if: contains(matrix.os, 'ubuntu') run: sudo apt-get update && sudo apt-get install -y libunbound-dev - # Pythong 3.10->3.11 broke node-gyp. This upgrades node-gyp for older nodejs. - # https://github.com/nodejs/node-gyp/issues/2219 - - name: Update npm. - if: contains(matrix.node, '14.x') - run: npm i -g npm@9 - - name: Install dependencies run: npm install diff --git a/lib/client/wallet.js b/lib/client/wallet.js index 191e15cf3..fa16f6212 100644 --- a/lib/client/wallet.js +++ b/lib/client/wallet.js @@ -685,6 +685,32 @@ class WalletClient extends bcurl.Client { return this.patch(`/wallet/${id}/account/${name}`, options); } + /** + * Get receive address. (UNSAFE) + * @param {String} id + * @param {String} account + * @param {Number} index + * @param {Boolean} safe + * @returns {Promise} + */ + + getAddress(id, account, index, safe) { + return this.get(`/wallet/${id}/address`, { account, index, safe }); + } + + /** + * Get change address. (UNSAFE) + * @param {String} id + * @param {String} account + * @param {Number} index + * @param {Boolean} safe + * @returns {Promise} + */ + + getChange(id, account, index, safe) { + return this.get(`/wallet/${id}/change`, { account, index, safe }); + } + /** * Create address. * @param {Object} options @@ -1374,6 +1400,30 @@ class Wallet extends EventEmitter { return this.client.modifyAccount(this.id, name, options); } + /** + * Get receive address. (UNSAFE) + * @param {String} account + * @param {Number} index + * @param {Boolean} safe + * @returns {Promise} + */ + + getAddress(account, index, safe) { + return this.client.getAddress(this.id, account, index, safe); + } + + /** + * Get change address. (UNSAFE) + * @param {String} account + * @param {Number} index + * @param {Boolean} safe + * @returns {Promise} + */ + + getChange(account, index, safe) { + return this.client.getChange(this.id, account, index, safe); + } + /** * Create address. * @param {Object} options diff --git a/lib/node/rpc.js b/lib/node/rpc.js index 905e84dae..4d34da15d 100644 --- a/lib/node/rpc.js +++ b/lib/node/rpc.js @@ -255,6 +255,9 @@ class RPC extends RPCBase { // this.add('getnameinfo', this.getNameInfo); // this.add('getnameresource', this.getNameResource); // this.add('getnameproof', this.getNameProof); + + // Namebase specific events. + this.add('emitnamebaseevent', this.emitNamebaseEvent); } /* @@ -2634,6 +2637,18 @@ class RPC extends RPCBase { return null; } + async emitNamebaseEvent(args) { + if (args.length < 1) + throw new RPCError(errs.MISC_ERROR, 'emitnamebaseevent ( ...args )'); + + const valid = new Validator(args); + const eventName = valid.str(0); + + this.emit(`namebase-${eventName}`, args.slice(1)); + + return null; + } + /* * Helpers */ diff --git a/lib/primitives/mtx.js b/lib/primitives/mtx.js index f5b7980cb..3983a4406 100644 --- a/lib/primitives/mtx.js +++ b/lib/primitives/mtx.js @@ -870,11 +870,14 @@ class MTX extends TX { * @returns {Number} Number of inputs templated. */ - template(ring) { + async template(ring) { if (Array.isArray(ring)) { let total = 0; - for (const key of ring) + for (const key of ring) { total += this.template(key); + await yieldToRuntime(); + } + return total; } @@ -895,6 +898,9 @@ class MTX extends TX { continue; total += 1; + + if (total % 100 === 0) + await yieldToRuntime(); } return total; @@ -1484,6 +1490,10 @@ function sortLinked(a, b) { return a[0].compare(b[0]); } +function yieldToRuntime() { + return new Promise(res => setImmediate(res)); +} + /* * Expose */ diff --git a/lib/wallet/account.js b/lib/wallet/account.js index 84f3f658f..bc3c0137b 100644 --- a/lib/wallet/account.js +++ b/lib/wallet/account.js @@ -435,6 +435,39 @@ class Account extends bio.Struct { } } + /** + * Derive an address at `index`. Do not increment depth. + * Optionally check safety. + * @param {Number} branch + * @param {Number} index + * @param {Boolean} [safe] + * @param {MasterKey} [master] + * @returns {WalletKey} + */ + + deriveCheckedKey(branch, index, safe, master) { + assert(typeof branch === 'number'); + + if (safe) { + switch (branch) { + case 0: { + if (index > this.receiveDepth + this.lookahead) + throw new Error('Receive index out of bounds.'); + + break; + } + case 1: { + if (index > this.changeDepth + this.lookahead) + throw new Error('Change index out of bounds.'); + + break; + } + } + } + + return this.deriveKey(branch, index, master); + } + /** * Derive an address at `index`. Do not increment depth. * @param {Number} branch diff --git a/lib/wallet/common.js b/lib/wallet/common.js index 04d3eb26e..86f788267 100644 --- a/lib/wallet/common.js +++ b/lib/wallet/common.js @@ -162,3 +162,21 @@ common.coinSelectionTypes = { DB_SWEEPDUST: 'db-sweepdust', DB_AGE: 'db-age' }; + +common.AbortError = class AbortError extends Error { + constructor(message, options) { + super(message, options); + this.name = 'AbortError'; + this.message = 'Operation aborted.'; + + if (message) + this.message = message; + + if (options.cause) + this.message += ` (${options.cause})`; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, common.AbortError); + } + } +}; diff --git a/lib/wallet/http.js b/lib/wallet/http.js index 87102e88b..c5f7964a9 100644 --- a/lib/wallet/http.js +++ b/lib/wallet/http.js @@ -102,6 +102,14 @@ class HTTP extends Server { type: 'json' })); + this.use(async (req, res) => { + res.abortController = new AbortController(); + + res.on('close', () => { + res.abortController.abort(new Error('Client closed connection.')); + }); + }); + this.use(async (req, res) => { if (!this.options.walletAuth) { req.admin = true; @@ -192,6 +200,37 @@ class HTTP extends Server { this.logger.info('Successful auth for %s.', id); }); + this.get('/wdb', async (req, res) => { + if (!req.admin) { + res.json(403); + return; + } + + const state = this.wdb.state; + + res.json(200, { + general: { + version: this.wdb.version, + state: { + startHeight: state.startHeight, + startHash: state.startHash.toString('hex'), + height: state.height, + marked: state.marked + } + }, + guards: { + confirming: this.wdb.confirming, + rescanning: this.wdb.rescanning, + filterSent: this.wdb.filterSent + }, + locks: { + readLock: this.wdb.readLock.busy, + writeLock: this.wdb.writeLock.busy, + txLock: this.wdb.txLock.busy + } + }); + }); + // Rescan this.post('/rescan', async (req, res) => { if (!req.admin) { @@ -479,9 +518,11 @@ class HTTP extends Server { this.post('/wallet/:id/send', async (req, res) => { const valid = Validator.fromRequest(req); - const options = TransactionOptions.fromValidator(valid, this.network); - const tx = await req.wallet.send(options); + const options = TransactionOptions.fromValidator(valid, this.network, + res.abortController); + TXCreateTimeout(options); + const tx = await req.wallet.send(options); const details = await req.wallet.getDetails(tx.hash()); res.json(200, details.getJSON(this.network, this.wdb.height)); @@ -494,7 +535,10 @@ class HTTP extends Server { // TODO: Add create TX with locks for used Coins and/or // adds to the pending list. - const options = TransactionOptions.fromValidator(valid, this.network); + const options = TransactionOptions.fromValidator(valid, this.network, + res.abortController); + TXCreateTimeout(options); + const tx = await req.wallet.createTX(options); if (sign) @@ -532,11 +576,11 @@ class HTTP extends Server { enforce(age != null, 'Age is required.'); - const total = await req.wallet.zap(acct, age); + const zapped = await req.wallet.zap(acct, age); res.json(200, { success: true, - zapped: total + zapped: zapped.length }); }); @@ -646,6 +690,34 @@ class HTTP extends Server { res.json(200, { privateKey: key.toSecret(this.network) }); }); + // Get address + this.get('/wallet/:id/address', async (req, res) => { + const valid = Validator.fromRequest(req); + const acct = valid.str('account'); + const index = valid.u32('index'); + const safe = valid.bool('safe', false); + + enforce(index != null, 'Index is required.'); + + const addr = await req.wallet.deriveReceive(acct, index, safe); + + res.json(200, addr.getJSON(this.network)); + }); + + // Get change address + this.get('/wallet/:id/change', async (req, res) => { + const valid = Validator.fromRequest(req); + const acct = valid.str('account'); + const index = valid.u32('index'); + const safe = valid.bool('safe', false); + + enforce(index != null, 'Index is required.'); + + const addr = await req.wallet.deriveChange(acct, index, safe); + + res.json(200, addr.getJSON(this.network)); + }); + // Create address this.post('/wallet/:id/address', async (req, res) => { const valid = Validator.fromRequest(req); @@ -1105,7 +1177,9 @@ class HTTP extends Server { enforce(name, 'Name is required.'); enforce(broadcast ? sign : true, 'Must sign when broadcasting.'); - const options = TransactionOptions.fromValidator(valid, this.network); + const options = TransactionOptions.fromValidator(valid, this.network, + res.abortController); + TXCreateTimeout(options); if (broadcast) { // TODO: Add abort signal to close when request closes. @@ -1142,7 +1216,9 @@ class HTTP extends Server { enforce(lockup != null, 'Lockup is required.'); enforce(broadcast ? sign : true, 'Must sign when broadcasting.'); - const options = TransactionOptions.fromValidator(valid, this.network); + const options = TransactionOptions.fromValidator(valid, this.network, + res.abortController); + TXCreateTimeout(options); if (broadcast) { // TODO: Add abort signal to close when request closes. @@ -1181,7 +1257,9 @@ class HTTP extends Server { enforce(broadcastBid != null, 'broadcastBid is required.'); enforce(broadcastBid ? sign : true, 'Must sign when broadcasting.'); - const options = TransactionOptions.fromValidator(valid, this.network); + const options = TransactionOptions.fromValidator(valid, this.network, + res.abortController); + TXCreateTimeout(options); const auctionTXs = await req.wallet.createAuctionTXs( name, bid, @@ -1222,7 +1300,9 @@ class HTTP extends Server { enforce(broadcast ? sign : true, 'Must sign when broadcasting.'); - const options = TransactionOptions.fromValidator(valid, this.network); + const options = TransactionOptions.fromValidator(valid, this.network, + res.abortController); + TXCreateTimeout(options); if (broadcast) { let tx; @@ -1268,7 +1348,9 @@ class HTTP extends Server { enforce(broadcast ? sign : true, 'Must sign when broadcasting.'); - const options = TransactionOptions.fromValidator(valid, this.network); + const options = TransactionOptions.fromValidator(valid, this.network, + res.abortController); + TXCreateTimeout(options); if (broadcast) { let tx; @@ -1324,17 +1406,21 @@ class HTTP extends Server { return res.json(400); } - const options = TransactionOptions.fromValidator(valid, this.network); + const options = TransactionOptions.fromValidator(valid, this.network, + res.abortController); + TXCreateTimeout(options); + + const rawResource = resource.encode(); if (broadcast) { // TODO: Add abort signal to close when request closes. - const tx = await req.wallet.sendUpdate(name, resource, options); + const tx = await req.wallet.sendUpdate(name, rawResource, options); return res.json(200, tx.getJSON(this.network)); } // TODO: Add create TX with locks for used Coins and/or // adds to the pending list. - const mtx = await req.wallet.createUpdate(name, resource, options); + const mtx = await req.wallet.createUpdate(name, rawResource, options); if (sign) await req.wallet.sign(mtx, options.passphrase); @@ -1357,7 +1443,9 @@ class HTTP extends Server { enforce(broadcast ? sign : true, 'Must sign when broadcasting.'); enforce(name, 'Must pass name.'); - const options = TransactionOptions.fromValidator(valid, this.network); + const options = TransactionOptions.fromValidator(valid, this.network, + res.abortController); + TXCreateTimeout(options); if (broadcast) { // TODO: Add abort signal to close when request closes. @@ -1391,7 +1479,9 @@ class HTTP extends Server { enforce(address, 'Must pass address.'); const addr = Address.fromString(address, this.network); - const options = TransactionOptions.fromValidator(valid, this.network); + const options = TransactionOptions.fromValidator(valid, this.network, + res.abortController); + TXCreateTimeout(options); if (broadcast) { // TODO: Add abort signal to close when request closes. @@ -1424,7 +1514,9 @@ class HTTP extends Server { enforce(broadcast ? sign : true, 'Must sign when broadcasting.'); enforce(name, 'Must pass name.'); - const options = TransactionOptions.fromValidator(valid, this.network); + const options = TransactionOptions.fromValidator(valid, this.network, + res.abortController); + TXCreateTimeout(options); if (broadcast) { // TODO: Add abort signal to close when request closes. @@ -1457,7 +1549,9 @@ class HTTP extends Server { enforce(broadcast ? sign : true, 'Must sign when broadcasting.'); enforce(name, 'Must pass name.'); - const options = TransactionOptions.fromValidator(valid, this.network); + const options = TransactionOptions.fromValidator(valid, this.network, + res.abortController); + TXCreateTimeout(options); if (broadcast) { // TODO: Add abort signal to close when request closes. @@ -1488,7 +1582,9 @@ class HTTP extends Server { enforce(broadcast ? sign : true, 'Must sign when broadcasting.'); enforce(name, 'Must pass name.'); - const options = TransactionOptions.fromValidator(valid, this.network); + const options = TransactionOptions.fromValidator(valid, this.network, + res.abortController); + TXCreateTimeout(options); if (broadcast) { // TODO: Add abort signal to close when request closes. @@ -1857,9 +1953,10 @@ class TransactionOptions { * @constructor * @param {RequestValidator} [valid] * @param {(NetworkType|Network)?} [network] + * @param {AbortSignal} [signal] */ - constructor(valid, network) { + constructor(valid, network, signal) { this.rate = null; this.maxFee = null; this.selection = null; @@ -1876,10 +1973,13 @@ class TransactionOptions { this.hardFee = null; this.blocks = null; this.network = network || Network.primary; + this.abortOnClose = false; + this.controller = null; + this.timeout = null; this.outputs = []; if (valid) - this.fromValidator(valid, network); + this.fromValidator(valid, network, signal); } /** @@ -1887,15 +1987,33 @@ class TransactionOptions { * @private * @param {RequestValidator} valid * @param {(NetworkType|Network)?} [network] + * @param {AbortController} [controller] * @returns {TransactionOptions} */ - fromValidator(valid, network) { + fromValidator(valid, network, controller) { assert(valid); if (network) this.network = network; + if (controller) + this.controller = controller; + + if (valid.has('abortOnClose')) + this.abortOnClose = valid.bool('abortOnClose'); + + if (valid.has('timeout')) { + assert(controller); + this.abortOnClose = this.abortOnClose || valid.has('timeout'); + this.timeout = valid.float('timeout') * 1000 | 0; + } + + if (this.abortOnClose) { + assert(controller); + this.signal = controller.signal; + } + if (valid.has('rate')) this.rate = valid.u64('rate'); @@ -1974,14 +2092,29 @@ class TransactionOptions { * from Validator. * @param {RequestValidator} [valid] * @param {(NetworkType|Network)?} [network] + * @param {AbortController} [controller] * @returns {TransactionOptions} */ - static fromValidator(valid, network) { - return new this(valid, network); + static fromValidator(valid, network, controller) { + return new this(valid, network, controller); } } +/** + * @param {TransactionOptions} + */ + +function TXCreateTimeout(options) { + if (!options.timeout || !options.controller) + return null; + + return setTimeout(() => { + if (!options.controller.signal.aborted) + options.controller.abort(new Error('Timed out.')); + }, options.timeout); +} + /* * Helpers */ @@ -2000,5 +2133,6 @@ function enforce(value, msg) { HTTP.HTTP = HTTP; HTTP.TransactionOptions = TransactionOptions; +HTTP.TXCreateTimeout = TXCreateTimeout; module.exports = HTTP; diff --git a/lib/wallet/node.js b/lib/wallet/node.js index 226e2443f..72d2899ac 100644 --- a/lib/wallet/node.js +++ b/lib/wallet/node.js @@ -55,7 +55,8 @@ class WalletNode extends Node { icannlockup: this.config.bool('icannlockup', true), migrateNoRescan: this.config.bool('migrate-no-rescan', false), preloadAll: this.config.bool('preload-all', false), - maxHistoryTXs: this.config.uint('max-history-txs', 100) + maxHistoryTXs: this.config.uint('max-history-txs', 100), + sweepdustMinValue: this.config.uint('sweepdust-min-value', 1) }); this.rpc = new RPC(this); diff --git a/lib/wallet/plugin.js b/lib/wallet/plugin.js index 71ad830fa..5242a3f8d 100644 --- a/lib/wallet/plugin.js +++ b/lib/wallet/plugin.js @@ -46,6 +46,7 @@ class Plugin extends EventEmitter { this.network = node.network; this.logger = node.logger; + this.workers = node.workers; this.client = new NodeClient(node); @@ -64,7 +65,8 @@ class Plugin extends EventEmitter { icannlockup: this.config.bool('icannlockup', true), migrateNoRescan: this.config.bool('migrate-no-rescan', false), preloadAll: this.config.bool('preload-all', false), - maxHistoryTXs: this.config.uint('max-history-txs', 100) + maxHistoryTXs: this.config.uint('max-history-txs', 100), + sweepdustMinValue: this.config.uint('sweepdust-min-value', 1) }); this.rpc = new RPC(this); diff --git a/lib/wallet/rpc.js b/lib/wallet/rpc.js index 61ae0004d..10d159bda 100644 --- a/lib/wallet/rpc.js +++ b/lib/wallet/rpc.js @@ -2487,7 +2487,8 @@ class RPC extends RPCBase { async sendUpdate(args, help) { const opts = this._validateUpdate(args, help, 'sendupdate'); const wallet = this.wallet; - const tx = await wallet.sendUpdate(opts.name, opts.resource, { + const rawResource = opts.resource.encode(); + const tx = await wallet.sendUpdate(opts.name, rawResource, { account: opts.account }); @@ -2497,7 +2498,8 @@ class RPC extends RPCBase { async createUpdate(args, help) { const opts = this._validateUpdate(args, help, 'createupdate'); const wallet = this.wallet; - const mtx = await wallet.createUpdate(opts.name, opts.resource, { + const rawResource = opts.resource.encode(); + const mtx = await wallet.createUpdate(opts.name, rawResource, { paths: true, account: opts.account }); @@ -2813,7 +2815,8 @@ class RPC extends RPCBase { 'UPDATE action requires 2 arguments: name, data' ); const {name, resource} = this._validateUpdate(action); - actions.push({ type: type, args: [name, resource] }); + const rawResource = resource.encode(); + actions.push({ type: type, args: [name, rawResource] }); break; } case 'RENEW': { diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index 9f87c6075..2bfc74b60 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -694,6 +694,17 @@ class TXDB { b.put(layout.i.encode(nameHash, hash, index), bb.encode()); } + /** + * Updates a Bid in levelDB + * @param {BlindBid} bid bid to update + * @returns {Promise} void + */ + async updateBid(bid) { + const b = this.bucket.batch(); + this.putBid(b, bid.nameHash, bid.prevout, bid); + return b.write(); + } + /** * Delete a bid. * @param {Object} b @@ -4288,7 +4299,7 @@ class TXDB { * Zap pending transactions older than `age`. * @param {Number} acct * @param {Number} age - Age delta. - * @returns {Promise} - zapped tx hashes. + * @returns {Promise} - zapped tx hashes. */ async zap(acct, age) { @@ -4303,7 +4314,7 @@ class TXDB { let txs = await this.listUnconfirmedByTime(acct, options); - let zapped = 0; + const zapped = []; while (txs.length) { for (const wtx of txs) { @@ -4312,7 +4323,7 @@ class TXDB { await this.remove(wtx.hash); - zapped++; + zapped.push(wtx.hash); } txs = await this.listUnconfirmedByTime(acct, options); diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index af9e5c607..c5f183230 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -34,7 +34,6 @@ const MasterKey = require('./masterkey'); const policy = require('../protocol/policy'); const consensus = require('../protocol/consensus'); const rules = require('../covenants/rules'); -const {Resource} = require('../dns/resource'); const Claim = require('../primitives/claim'); const reserved = require('../covenants/reserved'); const {ownership} = require('../covenants/ownership'); @@ -121,6 +120,9 @@ class Wallet extends EventEmitter { this.maxAncestors = policy.MEMPOOL_MAX_ANCESTORS; this.absurdFactor = policy.ABSURD_FEE_FACTOR; + /** @type {Amount} */ + this.sweepdustMinValue = wdb.options.sweepdustMinValue || 1; + if (options) this.fromOptions(options); } @@ -882,6 +884,48 @@ class Wallet extends EventEmitter { return this.wdb.hasAccount(this.wid, index); } + /** + * Derive a receiving address (does not increment receiveDepth). + * @param {(Number|String)?} acct + * @param {Number} index + * @param {Boolean} [safe] + * @returns {Promise} + */ + + deriveReceive(acct = 0, index, safe) { + return this.deriveKey(acct, 0, index, safe); + } + + /** + * Derive a change address (does not increment changeDepth). + * @param {(Number|String)?} acct + * @param {Number} index + * @param {Boolean} [safe] + * @returns {Promise} + */ + + deriveChange(acct = 0, index, safe) { + return this.deriveKey(acct, 1, index, safe); + } + + /** + * Derive address (does not increment depth). + * @param {(Number|String)?} acct + * @param {Number} branch + * @param {Number} index + * @param {Boolean} safe + * @returns {Promise} + */ + + async deriveKey(acct = 0, branch, index, safe) { + const account = await this.getAccount(acct); + + if (!account) + throw new Error('Account not found.'); + + return account.deriveCheckedKey(branch, index, safe); + } + /** * Create a new receiving address (increments receiveDepth). * @param {(Number|String)?} acct @@ -1261,6 +1305,7 @@ class Wallet extends EventEmitter { selection: options.selection, // sweepdust options sweepdustMinValue: options.sweepdustMinValue, + remapSelection: options.remapSelection, round: options.round, depth: options.depth, @@ -1272,7 +1317,8 @@ class Wallet extends EventEmitter { coinbaseMaturity: this.network.coinbaseMaturity, rate: rate, maxFee: options.maxFee, - estimate: prev => this.estimateSize(prev) + estimate: prev => this.estimateSize(prev), + signal: options.signal }); mtx.fill(selected); @@ -1287,13 +1333,36 @@ class Wallet extends EventEmitter { */ async select(mtx, options) { - const selection = options.selection || DEFAULT_SELECTION; + const remap = options.remapSelection ?? true; + let selection = options.selection || DEFAULT_SELECTION; + + if (remap) { + switch (selection) { + case 'all': + options.selection = common.coinSelectionTypes.DB_ALL; + selection = options.selection; + break; + case 'value': + options.selection = common.coinSelectionTypes.DB_VALUE; + selection = options.selection; + break; + case 'age': + options.selection = common.coinSelectionTypes.DB_AGE; + selection = options.selection; + break; + case 'swepdust': + options.seletion = common.coinSelectionTypes.DB_SWEEPDUST; + selection = options.selection; + break; + } + } switch (selection) { case 'all': - case 'random': case 'value': - case 'age': { + case 'age': + assert(!remap); + case 'random': { let coins = options.coins || []; assert(Array.isArray(coins)); @@ -1821,6 +1890,9 @@ class Wallet extends EventEmitter { async _sendOpen(name, options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createOpen(name, options); + + checkSendAbort(options); + return this.sendMTX(mtx, passphrase); } @@ -1951,6 +2023,9 @@ class Wallet extends EventEmitter { async _sendBid(name, value, lockup, options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createBid(name, value, lockup, options); + + checkSendAbort(options); + return this.sendMTX(mtx, passphrase); } @@ -2177,6 +2252,9 @@ class Wallet extends EventEmitter { async _sendReveal(name, options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createReveal(name, options); + + checkSendAbort(options); + return this.sendMTX(mtx, passphrase); } @@ -2311,6 +2389,9 @@ class Wallet extends EventEmitter { async _sendRevealAll(options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createRevealAll(options); + + checkSendAbort(options); + return this.sendMTX(mtx, passphrase); } @@ -2455,6 +2536,9 @@ class Wallet extends EventEmitter { async _sendRedeem(name, options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createRedeem(name, options); + + checkSendAbort(options); + return this.sendMTX(mtx, passphrase); } @@ -2586,6 +2670,9 @@ class Wallet extends EventEmitter { async _sendRedeemAll(options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createRedeemAll(options); + + checkSendAbort(options); + return this.sendMTX(mtx, passphrase); } @@ -2609,14 +2696,14 @@ class Wallet extends EventEmitter { * Make a register MTX. * @private * @param {String} name - * @param {Resource?} resource + * @param {Buffer?} rawResource * @param {MTX?} [mtx] * @returns {Promise} */ - async _makeRegister(name, resource, mtx) { + async _makeRegister(name, rawResource, mtx) { assert(typeof name === 'string'); - assert(!resource || (resource instanceof Resource)); + assert(!rawResource || Buffer.isBuffer(rawResource)); if (!rules.verifyName(name)) throw new Error(`Invalid name: ${name}.`); @@ -2663,18 +2750,14 @@ class Wallet extends EventEmitter { output.address = coin.address; output.value = ns.value; - let rawResource = EMPTY; - - if (resource) { - const raw = resource.encode(); + if (!rawResource) + rawResource = EMPTY; - if (raw.length > rules.MAX_RESOURCE_SIZE) + if (rawResource.length > rules.MAX_RESOURCE_SIZE) { throw new Error( - `Resource size (${raw.length}) exceeds maximum `+ + `Resource size (${rawResource.length}) exceeds maximum `+ `(${rules.MAX_RESOURCE_SIZE}) for name: ${name}.` ); - - rawResource = raw; } const blockHash = await this.wdb.getRenewalBlock(); @@ -2689,18 +2772,113 @@ class Wallet extends EventEmitter { return mtx; } + /** + * Make a Register OR Redeem MTX. + * @param {String} name + * @param {Buffer} rawResource + * @param {(Number|String)?} acct + * @param {MTX?} [mtx] + * @returns {Promise} + */ + + async makeFinish(name, rawResource, acct, mtx) { + assert(typeof name === 'string'); + assert(Buffer.isBuffer(rawResource)); + + if (!rules.verifyName(name)) + throw new Error(`Invalid name: ${name}.`); + + let acctno; + + if (acct != null) { + assert((acct >>> 0) === acct || typeof acct === 'string'); + acctno = await this.getAccountIndex(acct); + } + + const rawName = Buffer.from(name, 'ascii'); + const nameHash = rules.hashName(rawName); + const ns = await this.getNameState(nameHash); + const height = this.wdb.height + 1; + const network = this.network; + + if (!ns) + throw new Error(`Auction not found: ${name}.`); + + if (ns.isExpired(height, network)) + throw new Error(`Name has expired: ${name}.`); + + if (!ns.isRedeemable(height, network)) + throw new Error(`Auction is not yet closed: ${name}.`); + + const reveals = await this.txdb.getReveals(nameHash); + + if (!mtx) + mtx = new MTX(); + + let pushed = 0; + for (const {prevout, own} of reveals) { + const {hash, index} = prevout; + + if (!own) + continue; + + const coin = await this.getUnspentCoin(hash, index); + + if (!coin) + continue; + + if (acctno != null) { + if (!await this.txdb.hasCoinByAccount(acctno, hash, index)) + continue; + } + + // Is local? + if (coin.height < ns.height) + continue; + + const output = new Output(); + output.address = coin.address; + + if (prevout.equals(ns.owner)) { + if (rawResource.length > rules.MAX_RESOURCE_SIZE) + throw new Error(`Resource exceeds maximum size: ${name}.`); + + const blockHash = await this.wdb.getRenewalBlock(); + + // REGISTER pays second highest. + output.value = ns.value; + output.covenant.setRegister( + nameHash, ns.height, rawResource, blockHash); + } else { + // REDEEM gets full value back. + output.value = coin.value; + output.covenant.setRedeem(nameHash, ns.height); + } + + mtx.addOutpoint(prevout); + mtx.outputs.push(output); + + pushed++; + } + + if (pushed === 0) + throw new Error(`No reveals to redeem for name: ${name}.`); + + return mtx; + } + /** * Make an update MTX. * @param {String} name - * @param {Resource} resource + * @param {Buffer} rawResource * @param {(Number|String)?} acct * @param {MTX?} [mtx] * @returns {Promise} */ - async makeUpdate(name, resource, acct, mtx) { + async makeUpdate(name, rawResource, acct, mtx) { assert(typeof name === 'string'); - assert(resource instanceof Resource); + assert(Buffer.isBuffer(rawResource)); if (!rules.verifyName(name)) throw new Error('Invalid name.'); @@ -2738,7 +2916,7 @@ class Wallet extends EventEmitter { const coin = credit.coin; if (coin.covenant.isReveal() || coin.covenant.isClaim()) - return this._makeRegister(name, resource, mtx); + return this._makeRegister(name, rawResource, mtx); if (ns.isExpired(height, network)) throw new Error(`Name has expired: ${name}.`); @@ -2757,15 +2935,13 @@ class Wallet extends EventEmitter { throw new Error(`Name is not registered: ${name}.`); } - const raw = resource.encode(); - - if (raw.length > rules.MAX_RESOURCE_SIZE) + if (rawResource.length > rules.MAX_RESOURCE_SIZE) throw new Error(`Resource exceeds maximum size: ${name}.`); const output = new Output(); output.address = coin.address; output.value = coin.value; - output.covenant.setUpdate(nameHash, ns.height, raw); + output.covenant.setUpdate(nameHash, ns.height, rawResource); if (!mtx) mtx = new MTX(); @@ -2779,14 +2955,14 @@ class Wallet extends EventEmitter { * Create and finalize an update * MTX without a lock. * @param {String} name - * @param {Resource} resource + * @param {Buffer} rawResource * @param {Object} options * @returns {Promise} */ - async _createUpdate(name, resource, options) { + async _createUpdate(name, rawResource, options) { const acct = options ? options.account : null; - const mtx = await this.makeUpdate(name, resource, acct); + const mtx = await this.makeUpdate(name, rawResource, acct); await this.fill(mtx, options); return this.finalize(mtx, options); } @@ -2813,14 +2989,17 @@ class Wallet extends EventEmitter { * Create and send an update * MTX without a lock. * @param {String} name - * @param {Resource} resource + * @param {Buffer} rawResource * @param {Object} options * @returns {Promise} */ - async _sendUpdate(name, resource, options) { + async _sendUpdate(name, rawResource, options) { const passphrase = options ? options.passphrase : null; - const mtx = await this._createUpdate(name, resource, options); + const mtx = await this._createUpdate(name, rawResource, options); + + checkSendAbort(options); + return this.sendMTX(mtx, passphrase); } @@ -2828,15 +3007,15 @@ class Wallet extends EventEmitter { * Create and send an update * MTX with a lock. * @param {String} name - * @param {Resource} resource + * @param {Buffer} rawResource * @param {Object} options * @returns {Promise} */ - async sendUpdate(name, resource, options) { + async sendUpdate(name, rawResource, options) { const unlock = await this.fundLock.lock(); try { - return await this._sendUpdate(name, resource, options); + return await this._sendUpdate(name, rawResource, options); } finally { unlock(); } @@ -3048,6 +3227,9 @@ class Wallet extends EventEmitter { async _sendRenewal(name, options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createRenewal(name, options); + + checkSendAbort(options); + return this.sendMTX(mtx, passphrase); } @@ -3199,6 +3381,9 @@ class Wallet extends EventEmitter { address, options ); + + checkSendAbort(options); + return this.sendMTX(mtx, passphrase); } @@ -3331,6 +3516,9 @@ class Wallet extends EventEmitter { async _sendCancel(name, options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createCancel(name, options); + + checkSendAbort(options); + return this.sendMTX(mtx, passphrase); } @@ -3565,6 +3753,9 @@ class Wallet extends EventEmitter { async _sendFinalize(name, options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createFinalize(name, options); + + checkSendAbort(options); + return this.sendMTX(mtx, passphrase); } @@ -3706,6 +3897,9 @@ class Wallet extends EventEmitter { async _sendRevoke(name, options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createRevoke(name, options); + + checkSendAbort(options); + return this.sendMTX(mtx, passphrase); } @@ -3880,16 +4074,19 @@ class Wallet extends EventEmitter { /** * @typedef {Object} BatchResult - * @property {MTX} mtx - The resulting transaction + * @property {MTX} [mtx] - The resulting transaction * @property {BatchError[]} errors - List of errors encountered during * processing. + * @property {WeakMap} outputMap - Output id id mappings. */ /** * @typedef {Object} BatchTXResult - * @property {TX} tx - The resulting transaction + * @property {MTX} [mtx] - The resulting transaction + * @property {TX} [tx] - The resulting transaction * @property {BatchError[]} errors - List of errors encountered during * processing. + * @property {WeakMap} outputMap - Output to id mappings. */ /** @@ -3898,6 +4095,8 @@ class Wallet extends EventEmitter { * @param {Object} options * @param {Number|String} [options.account=0] - Account index or name. * @param {Boolean} [options.partialFailure=false] - Allow partial failure. + * @param {Number} [options.maxOutputs=0] - Max limit of the outputs. + * NOTE: It will be +1 with change address. * @throws {Error} - general validations. * @returns {Promise} */ @@ -3905,8 +4104,11 @@ class Wallet extends EventEmitter { async makeBatch(actions, options = {}) { assert(Array.isArray(actions)); assert(actions.length, 'Batches require at least one action.'); + assert(options); + assert(!options.maxOutputs || options.maxOutputs > 0, + 'maxOutputs must be non-negative.'); - const acct = options ? options.account || 0 : 0; + const acct = options.account || 0; const mtx = new MTX(); // Track estimated size of batch TX to keep it under policy limit @@ -3929,6 +4131,7 @@ class Wallet extends EventEmitter { case 'REVEAL': case 'REDEEM': case 'UPDATE': + case 'FINISH': case 'RENEW': case 'TRANSFER': case 'FINALIZE': @@ -3947,6 +4150,9 @@ class Wallet extends EventEmitter { /** @type {BatchError[]} */ const errors = []; + /** @type {WeakMap} */ + const outputMap = new WeakMap(); + /** * @param {BatchAction} action * @param {Array} args @@ -4005,8 +4211,15 @@ class Wallet extends EventEmitter { case 'UPDATE': { assert(args.length === 2, 'Bad arguments for UPDATE.'); const name = args[0]; - const resource = args[1]; - await this.makeUpdate(name, resource, acct, mtx); + const rawResource = args[1]; + await this.makeUpdate(name, rawResource, acct, mtx); + break; + } + case 'FINISH': { + assert(args.length === 2, 'Bad arguments for FINISH'); + const name = args[0]; + const rawResource = args[1]; + await this.makeFinish(name, rawResource, acct, mtx); break; } case 'RENEW': { @@ -4066,7 +4279,44 @@ class Wallet extends EventEmitter { assert(Array.isArray(args), 'Action args must be an array.'); try { + const lastOutputsLen = mtx.outputs.length; + const lastInputsLen = mtx.inputs.length; await handleAction(action, args); + + const opens = rules.countOpens(mtx); + const updates = rules.countUpdates(mtx); + const renewals = rules.countRenewals(mtx); + + if (opens > consensus.MAX_BLOCK_OPENS) { + mtx.inputs.length = lastInputsLen; + mtx.outputs.length = lastOutputsLen; + throw new Error('Too many OPENs.'); + } + + if (updates > consensus.MAX_BLOCK_UPDATES) { + mtx.inputs.length = lastInputsLen; + mtx.outputs.length = lastOutputsLen; + throw new Error('Too many UPDATEs.'); + } + + if (renewals > consensus.MAX_BLOCK_RENEWALS) { + mtx.inputs.length = lastInputsLen; + mtx.outputs.length = lastOutputsLen; + throw new Error('Too many RENEWs.'); + } + + if (options.maxOutputs && mtx.outputs.length > options.maxOutputs) { + mtx.inputs.length = lastInputsLen; + mtx.outputs.length = lastOutputsLen; + throw new Error(`Too many outputs, limit: ${options.maxOutputs}.`); + } + + if (action.id) { + for (let i = lastOutputsLen; i < mtx.outputs.length; i++) { + const output = mtx.outputs[i]; + outputMap.set(output, action.id); + } + } } catch (err) { if (!options.partialFailure) throw err; @@ -4081,20 +4331,19 @@ class Wallet extends EventEmitter { id: action.id || null }); } + } - if (rules.countOpens(mtx) > consensus.MAX_BLOCK_OPENS) - throw new Error('Too many OPENs.'); - - if (rules.countUpdates(mtx) > consensus.MAX_BLOCK_UPDATES) - throw new Error('Too many UPDATEs.'); + if (!mtx.outputs.length) { + if (!options.partialFailure) + throw new Error('Nothing to do.'); - if (rules.countRenewals(mtx) > consensus.MAX_BLOCK_RENEWALS) - throw new Error('Too many RENEWs.'); + return { + mtx: null, + errors, + outputMap + }; } - if (!mtx.outputs.length) - throw new Error('Nothing to do.'); - // Clean up. // 1. Some actions MUST be the ONLY action for a name. // i.e. no duplicate OPENs or REVOKE/FINALIZE for same name in one tx. @@ -4132,7 +4381,7 @@ class Wallet extends EventEmitter { throw new Error('Batch output addresses would exceed lookahead.'); } - return {mtx, errors}; + return {mtx, errors, outputMap}; } /** @@ -4143,11 +4392,16 @@ class Wallet extends EventEmitter { */ async _createBatch(actions, options) { - const {mtx, errors} = await this.makeBatch(actions, options); + const {mtx, errors, outputMap} = await this.makeBatch(actions, options); + + if (!mtx) + return { mtx: null, errors, outputMap }; + await this.fill(mtx, options); return { mtx: await this.finalize(mtx, options), - errors + errors, + outputMap }; } @@ -4176,10 +4430,15 @@ class Wallet extends EventEmitter { async _sendBatch(actions, options) { const passphrase = options ? options.passphrase : null; - const {mtx, errors} = await this._createBatch(actions, options); + const {mtx, errors, outputMap} = await this._createBatch(actions, options); + + checkSendAbort(options); + return { - tx: await this.sendMTX(mtx, passphrase), - errors + mtx, + tx: mtx ? await this.sendMTX(mtx, passphrase) : null, + errors, + outputMap }; } @@ -4293,6 +4552,9 @@ class Wallet extends EventEmitter { async _send(options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createTX(options); + + checkSendAbort(options); + return this.sendMTX(mtx, passphrase); } @@ -5167,7 +5429,7 @@ class Wallet extends EventEmitter { * @private * @param {(Number|String)?} acct * @param {Number} age - * @returns {Promise} + * @returns {Promise} */ async _zap(acct, age) { @@ -5736,7 +5998,8 @@ class WalletCoinSource extends AbstractCoinSource { this.selection = DEFAULT_SELECTION; this.smart = false; - this.sweepdustMinValue = 1; + this.sweepdustMinValue = wallet.sweepdustMinValue; + this.signal = null; if (options) this.fromOptions(options); @@ -5771,6 +6034,12 @@ class WalletCoinSource extends AbstractCoinSource { this.sweepdustMinValue = options.sweepdustMinValue; } + if (options.signal != null) { + assert(typeof options.signal === 'object', + 'Signal must be an AbortSignal.'); + this.signal = options.signal; + } + return this; } @@ -5779,6 +6048,7 @@ class WalletCoinSource extends AbstractCoinSource { case common.coinSelectionTypes.DB_VALUE: case common.coinSelectionTypes.DB_ALL: { this.iter = this.txdb.getAccountCreditIterByValue(this.account, { + minValue: this.sweepdustMinValue, reverse: true }); break; @@ -5815,6 +6085,15 @@ class WalletCoinSource extends AbstractCoinSource { if (this.done) return null; + if (this.signal && this.signal.aborted) { + this.done = true; + + const error = new common.AbortError( + 'Coin selection aborted.', + { cause: this.signal.reason }); + return this.iter.throw(error); + } + // look for an usable credit. for (;;) { const item = await this.iter.next(); @@ -5846,7 +6125,7 @@ class WalletCoinSource extends AbstractCoinSource { if (!this.iter) return; - this.iter.return(); + await this.iter.return(); } /** @@ -5885,6 +6164,21 @@ class WalletCoinSource extends AbstractCoinSource { } } +/* + * Helpers + */ + +function checkSendAbort(options) { + if (!options || !options.signal) + return; + + if (!options.signal.aborted) + return; + + throw new common.AbortError('Send was aborted.', + { cause: options.signal.reason }); +} + /* * Expose */ diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index bd160d9d9..cd261f581 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -37,6 +37,7 @@ const {scanActions} = require('../blockchain/common'); /** @typedef {ReturnType} Batch */ /** @typedef {import('../types').Hash} Hash */ +/** @typedef {import('../types').Amount} Amount */ /** @typedef {import('../primitives/tx')} TX */ /** @typedef {import('../primitives/claim')} Claim */ /** @typedef {import('../blockchain/common').ScanAction} ScanAction */ @@ -2912,6 +2913,10 @@ class WalletOptions { this.preloadAll = false; this.maxHistoryTXs = 100; + // Used in sweepdust filter. + /** @type {Amount} */ + this.sweepdustMinValue = 1; + this.nowFn = util.now; if (options) @@ -3021,6 +3026,12 @@ class WalletOptions { this.maxHistoryTXs = options.maxHistoryTXs; } + if (options.sweepdustMinValue != null) { + assert(typeof options.sweepdustMinValue === 'number'); + assert(options.sweepdustMinValue >= 0); + this.sweepdustMinValue = options.sweepdustMinValue; + } + return this; } diff --git a/package-lock.json b/package-lock.json index 3195be916..26676974d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "hsd", - "version": "8.99.0", + "version": "8.0.0-nb.0.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hsd", - "version": "8.99.0", + "version": "8.0.0-nb.0.0.2", "license": "MIT", "dependencies": { "bcfg": "~0.2.2", @@ -53,7 +53,7 @@ "bslintrc": "^0.0.3" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.14.0" } }, "node_modules/bcfg": { diff --git a/package.json b/package.json index f07d3bcad..bbee89dd1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hsd", - "version": "8.99.0", + "version": "8.0.0-nb.0.0.2", "description": "Cryptocurrency bike-shed", "license": "MIT", "repository": "git://github.com/handshake-org/hsd.git", @@ -17,7 +17,7 @@ "wallet" ], "engines": { - "node": ">=14.0.0" + "node": ">=16.14.0" }, "dependencies": { "bfilter": "~2.4.0", diff --git a/test/anyone-can-renew-test.js b/test/anyone-can-renew-test.js index ee039bc4a..a2d49b317 100644 --- a/test/anyone-can-renew-test.js +++ b/test/anyone-can-renew-test.js @@ -127,7 +127,7 @@ describe('Anyone-can-renew address', function() { const resource = Resource.fromJSON({ records: [{type: 'TXT', txt: ['This name is ANYONE-CAN-RENEW']}] }); - await alice.sendUpdate(name, resource); + await alice.sendUpdate(name, resource.encode()); await mineBlocks(network.names.treeInterval); ns = await node.getNameStatus(nameHash); diff --git a/test/claim-test.js b/test/claim-test.js index 26cbc1392..91d2b436d 100644 --- a/test/claim-test.js +++ b/test/claim-test.js @@ -167,7 +167,7 @@ describe('Reserved Name Claims', function() { const resource = Resource.fromJSON({ records: [{type: 'TXT', txt: ['#CooperationGood']}] }); - const register = await wallet.sendUpdate('cloudflare', resource); + const register = await wallet.sendUpdate('cloudflare', resource.encode()); check(); await mineBlocks(1); check(); diff --git a/test/interactive-swap-test.js b/test/interactive-swap-test.js index 705418186..31f8c6db9 100644 --- a/test/interactive-swap-test.js +++ b/test/interactive-swap-test.js @@ -113,7 +113,7 @@ describe('Interactive name swap', function() { const resource = Resource.fromJSON({ records: [{type: 'TXT', txt: ['Contact Alice to buy this name!']}] }); - await alice.sendUpdate(name, resource); + await alice.sendUpdate(name, resource.encode()); await mineBlocks(network.names.treeInterval); }); @@ -292,7 +292,7 @@ describe('Interactive name swap', function() { const resource = Resource.fromJSON({ records: [{type: 'TXT', txt: ['Thanks Alice! --Bob']}] }); - await bob.sendUpdate(name, resource); + await bob.sendUpdate(name, resource.encode()); await mineBlocks(network.names.treeInterval); const actual = await node.chain.db.getNameState(nameHash); assert.bufferEqual(resource.encode(), actual.data); diff --git a/test/mempool-invalidation-test.js b/test/mempool-invalidation-test.js index 120d781e1..1a969dded 100644 --- a/test/mempool-invalidation-test.js +++ b/test/mempool-invalidation-test.js @@ -321,7 +321,8 @@ describe('Mempool Invalidation', function() { assert.strictEqual(await getNameState(name), states.CLOSED); - await wallet.sendUpdate(name, Resource.fromJSON({ records: [] })); + const res = Resource.fromJSON({ records: [] }); + await wallet.sendUpdate(name, res.encode()); for (let i = 0; i < network.names.renewalWindow - 2; i++) await mineBlock(node); diff --git a/test/mempool-reorg-test.js b/test/mempool-reorg-test.js index 16a870a83..2d2de61d7 100644 --- a/test/mempool-reorg-test.js +++ b/test/mempool-reorg-test.js @@ -41,9 +41,11 @@ describe('Mempool Covenant Reorg', function () { let counter = 0; function makeResource() { - return Resource.fromJSON({ + const res = Resource.fromJSON({ records: [{type: 'TXT', txt: [`${counter++}`]}] }); + + return res.encode(); } it('should fund wallet and win name', async () => { diff --git a/test/net-spv-test.js b/test/net-spv-test.js index 4a0d6cd84..974bf7577 100644 --- a/test/net-spv-test.js +++ b/test/net-spv-test.js @@ -122,16 +122,13 @@ describe('SPV', function() { await mineBlocks(biddingPeriod); await wallet.sendReveal(name); await mineBlocks(revealPeriod); - await wallet.sendUpdate( - name, - Resource.fromJSON( - { - records: [ - {type: 'NS', ns: 'one.'} - ] - } - ) - ); + const res = Resource.fromJSON({ + records: [ + {type: 'NS', ns: 'one.'} + ] + }); + + await wallet.sendUpdate(name, res.encode()); await mineBlocks(treeInterval + SAFE_ROOT); }); @@ -166,16 +163,13 @@ describe('SPV', function() { }); it('should update name data', async () => { - await wallet.sendUpdate( - name, - Resource.fromJSON( - { - records: [ - {type: 'NS', ns: 'two.'} - ] - } - ) - ); + const res = Resource.fromJSON({ + records: [ + {type: 'NS', ns: 'two.'} + ] + }); + + await wallet.sendUpdate(name, res.encode()); await mineBlocks(treeInterval + SAFE_ROOT); }); diff --git a/test/util/common.js b/test/util/common.js index 57b738558..e588e664e 100644 --- a/test/util/common.js +++ b/test/util/common.js @@ -237,6 +237,20 @@ common.enableLogger = () => { Logger.global.closed = false; }; +common.delayMethodOnce = function delayMethodOnce(obj, prop, n) { + const bak = obj[prop]; + + obj[prop] = async function(...args) { + await common.sleep(n); + + try { + return bak.call(this, ...args); + } finally { + obj[prop] = bak; + } + }; +}; + function parseUndo(data) { const br = bio.read(data); const items = []; diff --git a/test/util/node-context.js b/test/util/node-context.js index 9fc15958d..7053a98f6 100644 --- a/test/util/node-context.js +++ b/test/util/node-context.js @@ -175,14 +175,18 @@ class NodeContext { return this.chain.tip.height; } - get wdb() { + get wnode() { if (!this.options.wallet) return null; if (this.walletNode) - return this.walletNode.wdb; + return this.walletNode; + + return this.node.get('walletdb'); + } - return this.node.get('walletdb').wdb; + get wdb() { + return this.wnode?.wdb; } /* diff --git a/test/wallet-accounts-auction-test.js b/test/wallet-accounts-auction-test.js index 2244df580..7f036e3d9 100644 --- a/test/wallet-accounts-auction-test.js +++ b/test/wallet-accounts-auction-test.js @@ -5,7 +5,7 @@ const Network = require('../lib/protocol/network'); const FullNode = require('../lib/node/fullnode'); const Address = require('../lib/primitives/address'); const rules = require('../lib/covenants/rules'); -const Resource = require('../lib/dns/resource'); +const {Resource} = require('../lib/dns/resource'); const WalletClient = require('../lib/client/wallet'); const network = Network.get('regtest'); @@ -129,20 +129,20 @@ describe('Multiple accounts participating in same auction', function() { }); describe('UPDATE', function() { - const aliceResource = Resource.Resource.fromJSON({ + const aliceResource = Resource.fromJSON({ records: [ { type: 'TXT', txt: ['ALICE'] } - ]}); - const bobResource = Resource.Resource.fromJSON({ + ]}).encode(); + const bobResource = Resource.fromJSON({ records: [ { type: 'TXT', txt: ['BOB'] } - ]}); + ]}).encode(); it('should advance auction to REGISTER phase', async () => { await mineBlocks(network.names.revealPeriod); diff --git a/test/wallet-auction-test.js b/test/wallet-auction-test.js index 33ae3de32..a44554171 100644 --- a/test/wallet-auction-test.js +++ b/test/wallet-auction-test.js @@ -15,7 +15,7 @@ const Output = require('../lib/primitives/output'); const Coin = require('../lib/primitives/coin'); const Covenant = require('../lib/primitives/covenant'); const {Resource} = require('../lib/dns/resource'); -const {forEvent} = require('./util/common'); +const {forEvent, delayMethodOnce} = require('./util/common'); /** @typedef {import('../lib/wallet/wallet')} Wallet */ @@ -58,6 +58,8 @@ const wdb = new WalletDB({ workers: workers }); +const EMPTY_RESOURCE = (new Resource()).encode(); + describe('Wallet Auction', function() { let wallet, wallet2; @@ -499,14 +501,17 @@ describe('Wallet Auction', function() { }); it('should REGISTER and fail duplicate register', async () => { - const register1 = await wallet.sendUpdate(name1, Resource.fromString('name1.1')); + const res1 = Resource.fromString('name1.1').encode(); + const register1 = await wallet.sendUpdate(name1, res1); assert(register1); - const register2 = await wallet.sendUpdate(name2, Resource.fromString('name2.1')); + const res2 = Resource.fromString('name2.1').encode(); + const register2 = await wallet.sendUpdate(name2, res2); assert(register2); let err; try { - await wallet.sendUpdate(name1, Resource.fromString('hello')); + const res = Resource.fromString('hello').encode(); + await wallet.sendUpdate(name1, res); } catch (e) { err = e; } @@ -517,19 +522,22 @@ describe('Wallet Auction', function() { await mineBlock([register1, register2]); // This becomes update. - const update = await wallet.sendUpdate(name2, Resource.fromString('name2.2')); + const res22 = Resource.fromString('name2.2').encode(); + const update = await wallet.sendUpdate(name2, res22); assert(update); await mineBlock([update]); }); it('should UPDATE and fail duplicate UPDATE', async () => { - const update = await wallet.sendUpdate(name1, Resource.fromString('name1.2')); + const res = Resource.fromString('name1.2').encode(); + const update = await wallet.sendUpdate(name1, res); assert(update); let err = null; try { - await wallet.sendUpdate(name1, Resource.fromString('name1.3')); + const res = Resource.fromString('name1.3').encode(); + await wallet.sendUpdate(name1, res); } catch (e) { err = e; } @@ -640,7 +648,7 @@ describe('Wallet Auction', function() { describe('Batch TXs', function() { /** @type {Wallet} */ - let wallet; + let wallet, wallet2; let receive; const hardFee = 12345; @@ -649,11 +657,18 @@ describe('Wallet Auction', function() { const name3 = rules.grindName(5, 0, network); const name4 = rules.grindName(6, 0, network); const name5 = rules.grindName(7, 0, network); - - const res1 = Resource.fromJSON({records: [{type: 'TXT', txt: ['one']}]}); - const res2 = Resource.fromJSON({records: [{type: 'TXT', txt: ['two']}]}); - const res3 = Resource.fromJSON({records: [{type: 'TXT', txt: ['three']}]}); - const res4 = Resource.fromJSON({records: [{type: 'TXT', txt: ['four']}]}); + // used for FINISH test. + const name6 = rules.grindName(7, 0, network); // only REGISTER + const name7 = rules.grindName(7, 0, network); // only REDEEM + const name8 = rules.grindName(7, 0, network); // BOTH + + const res1 = Resource.fromJSON({records: [{type: 'TXT', txt: ['one']}]}).encode(); + const res2 = Resource.fromJSON({records: [{type: 'TXT', txt: ['two']}]}).encode(); + const res3 = Resource.fromJSON({records: [{type: 'TXT', txt: ['three']}]}).encode(); + const res4 = Resource.fromJSON({records: [{type: 'TXT', txt: ['four']}]}).encode(); + const res6 = Resource.fromJSON({records: [{type: 'TXT', txt: ['six']}]}).encode(); + const res7 = Resource.fromJSON({records: [{type: 'TXT', txt: ['seven']}]}).encode(); + const res8 = Resource.fromJSON({records: [{type: 'TXT', txt: ['eight']}]}).encode(); const mempool = []; wdb.send = (tx) => { @@ -689,12 +704,23 @@ describe('Wallet Auction', function() { before(async () => { // Create wallet wallet = await wdb.create(); + wallet2 = await wdb.create({ + id: 'wallet2' + }); + receive = await wallet.receiveAddress(); mempool.length = 0; // Fund wallet await mineBlocks(20); + await wallet.send({ + outputs: [{ + address: await wallet2.receiveAddress(), + value: 1e6 + }] + }); + // Verify funds const bal = await wallet.getBalance(); assert.strictEqual(bal.confirmed, 20 * 2000e6); @@ -996,15 +1022,146 @@ describe('Wallet Auction', function() { assert.strictEqual(mtx.outputs.length, 3); }); + it('should not broadcast tx if client is signal is aborted', async () => { + const controller = new AbortController(); + delayMethodOnce(wallet, 'finalize', 100); + + setTimeout(() => { + controller.abort(new Error('Aborted by user.')); + }, 90); + + let err; + try { + await wallet.sendBatch( + [ + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name2] }, + { type: 'OPEN', args: [name3] } + ], + { + signal: controller.signal + } + ); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, 'Send was aborted. (Error: Aborted by user.)'); + assert.strictEqual(err.name, 'AbortError'); + assert.strictEqual(err.cause.message, 'Aborted by user.'); + }); + + it('should abort coin selection for tx if client is signal is aborted', async () => { + const controller = new AbortController(); + delayMethodOnce(wallet, 'fill', 100); + + setTimeout(() => { + controller.abort(new Error('Aborted by user.')); + }, 90); + let err; + try { + await wallet.sendBatch( + [ + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name2] }, + { type: 'OPEN', args: [name3] } + ], + { + signal: controller.signal + } + ); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, 'Coin selection aborted. (Error: Aborted by user.)'); + assert.strictEqual(err.name, 'AbortError'); + assert.strictEqual(err.cause.message, 'Aborted by user.'); + }); + + describe('maxOutputs config', function() { + it('should fail if max outputs is incorrect', async () => { + const actions = [ + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name2] }, + { type: 'OPEN', args: [name3] } + ]; + + await assert.rejects(wallet.createBatch(actions, { + maxOutputs: -1 + }), { + message: 'maxOutputs must be non-negative.' + }); + + await assert.rejects(wallet.createBatch(actions, { + maxOutputs: -1, + partialFailure: true + }), { + message: 'maxOutputs must be non-negative.' + }); + }); + + it('should create batch if maxOutputs is within limits', async () => { + const actions = [ + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name2] } + ]; + + const {mtx, errors} = await wallet.createBatch(actions, { + maxOutputs: 2 + }); + + // +1 for change. + assert.strictEqual(mtx.outputs.length, 2 + 1); + assert.strictEqual(errors.length, 0); + }); + + it('should fail if maxOutputs is exceeded', async () => { + const actions = [ + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name2] }, + { type: 'OPEN', args: [name3] } + ]; + + await assert.rejects(wallet.createBatch(actions, { + maxOutputs: 2 + }), { + message: 'Too many outputs, limit: 2.' + }); + }); + + it('should only fail over the limit actions', async () => { + const actions = [ + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name2] }, + { type: 'OPEN', args: [name3] } + ]; + + const {mtx, errors} = await wallet.createBatch(actions, { + maxOutputs: 2, + partialFailure: true + }); + + assert.strictEqual(mtx.outputs.length, 3); + assert.strictEqual(errors.length, 1); + assert.strictEqual(errors[0].message, 'Too many outputs, limit: 2.'); + }); + }); + describe('Complete auction and diverse-action batches', function() { const addr = Address.fromProgram(0, Buffer.alloc(20, 0x01)).toString('regtest'); - it('3 OPENs and 1 NONE', async () => { + it('5 OPENs and 1 NONE', async () => { const {tx, errors} = await wallet.sendBatch( [ { type: 'OPEN', args: [name1] }, { type: 'OPEN', args: [name2] }, { type: 'OPEN', args: [name3] }, + { type: 'OPEN', args: [name6] }, + { type: 'OPEN', args: [name7] }, + { type: 'OPEN', args: [name8] }, { type: 'NONE', args: [addr, 10000] } ] ); @@ -1014,13 +1171,23 @@ describe('Wallet Auction', function() { await mineBlocks(treeInterval + 1); }); - it('4 BIDs', async () => { + it('9 BIDs', async () => { const {tx, errors} = await wallet.sendBatch( [ { type: 'BID', args: [name1, 10000, 20000] }, { type: 'BID', args: [name1, 10001, 20000] }, // self-snipe! { type: 'BID', args: [name2, 30000, 40000] }, { type: 'BID', args: [name3, 50000, 60000] }, + // FINISH tests + // only REG + { type: 'BID', args: [name6, 60000, 60000] }, + // only REDEEM (must lose) + { type: 'BID', args: [name7, 70000, 70000] }, + // 1 REGISTER 2 REDEEMS + { type: 'BID', args: [name8, 80000, 100000] }, + { type: 'BID', args: [name8, 82000, 100000] }, + { type: 'BID', args: [name8, 81000, 100000] }, + // Handle failed index increments. { type: 'BID', args: ['random', -100, -1]}, { type: 'BID', args: ['random', -100, -1]}, @@ -1032,10 +1199,35 @@ describe('Wallet Auction', function() { ); assert(uniqueAddrs(tx)); + assert.strictEqual(tx.outputs.length, 9 + 1 + 1); assert.strictEqual(errors.length, 3); + + await wallet2.sendBid(name7, 1e5, 1e5); + await mineBlocks(biddingPeriod); }); + it('should fail REVEAL all if outputs exceed maxOutputs', async () => { + const actions = [ + { type: 'REVEAL' } + ]; + + await assert.rejects(wallet.createBatch(actions, { + maxOutputs: 2 + }), { + message: 'Too many outputs, limit: 2.' + }); + + const {mtx, errors} = await wallet.createBatch(actions, { + maxOutputs: 2, + partialFailure: true + }); + + assert.strictEqual(mtx, null); + assert.strictEqual(errors.length, 1); + assert.strictEqual(errors[0].message, 'Too many outputs, limit: 2.'); + }); + it('REVEAL all', async () => { // Don't send this one const {mtx: revealAll, errors} = await wallet.createBatch( @@ -1044,7 +1236,7 @@ describe('Wallet Auction', function() { ] ); - assert.strictEqual(revealAll.outputs.length, 5); + assert.strictEqual(revealAll.outputs.length, 10); assert.strictEqual(errors.length, 0); let reveals = 0; @@ -1052,7 +1244,7 @@ describe('Wallet Auction', function() { if (output.covenant.type === rules.types.REVEAL) reveals++; } - assert.strictEqual(reveals, 4); + assert.strictEqual(reveals, 9); }); it('2 REVEALs then 1 REVEAL', async () => { @@ -1069,10 +1261,18 @@ describe('Wallet Auction', function() { // No "could not resolve preferred inputs" error // because names are being revealed individually. await wallet.sendBatch( - [ - { type: 'REVEAL', args: [name3] } - ] + [{ type: 'REVEAL', args: [name3] }] ); + + await wallet.sendBatch([ + { type: 'REVEAL', args: [name6] }, + { type: 'REVEAL', args: [name7] }, + { type: 'REVEAL', args: [name8] } + ]); + + const {tx: tx2, errors: errors2} = await wallet2.sendBatch([{ type: 'REVEAL' }]); + assert(tx2); + assert.strictEqual(errors2.length, 0); await mineBlocks(revealPeriod); }); @@ -1084,7 +1284,7 @@ describe('Wallet Auction', function() { ] ); - assert.strictEqual(redeemAll.outputs.length, 2); + assert.strictEqual(redeemAll.outputs.length, 5); assert.strictEqual(errors.length, 0); let redeems = 0; @@ -1092,7 +1292,59 @@ describe('Wallet Auction', function() { if (output.covenant.type === rules.types.REDEEM) redeems++; } - assert.strictEqual(redeems, 1); + assert.strictEqual(redeems, 4); + }); + + it('2 REGISTER 3 REDEEMS', async () => { + const {mtx, errors} = await wallet.createBatch([ + { type: 'FINISH', args: [name6, res6] }, + { type: 'FINISH', args: [name7, res7] }, + { type: 'FINISH', args: [name8, res8] } + ]); + + assert(mtx); + const allRedeems = mtx.outputs + .filter(o => o.covenant.type === rules.types.REDEEM).length; + const alLRegisters = mtx.outputs + .filter(o => o.covenant.type === rules.types.REGISTER).length; + + assert.strictEqual(mtx.outputs.length, 6); + assert.strictEqual(errors.length, 0); + assert.strictEqual(allRedeems, 3); + assert.strictEqual(alLRegisters, 2); + + // name6 only REG + assert.strictEqual(mtx.outputs[0].value, 0); + assert.strictEqual(mtx.outputs[0].covenant.type, rules.types.REGISTER); + assert.bufferEqual(mtx.outputs[0].covenant.items[0], rules.hashName(name6)); + assert.bufferEqual(mtx.outputs[0].covenant.items[2], res6); + + // name7 only redeem + assert.strictEqual(mtx.outputs[1].value, 70000); + assert.strictEqual(mtx.outputs[1].covenant.type, rules.types.REDEEM); + assert.bufferEqual(mtx.outputs[1].covenant.items[0], rules.hashName(name7)); + + // name8 - 2 redeems and 1 register + const name8outs = mtx.outputs.slice(2) + .filter(o => o.covenant.type !== rules.types.NONE); + + const register = name8outs + .filter(o => o.covenant.type === rules.types.REGISTER); + const redeems = name8outs + .filter(o => o.covenant.type === rules.types.REDEEM) + .sort((a, b) => a.value - b.value); + + assert.strictEqual(register.length, 1); + assert.strictEqual(register[0].value, 81000); + assert.bufferEqual(register[0].covenant.items[0], rules.hashName(name8)); + assert.bufferEqual(register[0].covenant.items[2], res8); + + assert.strictEqual(redeems.length, 2); + assert.strictEqual(redeems[0].value, 80000); + assert.bufferEqual(redeems[0].covenant.items[0], rules.hashName(name8)); + + assert.strictEqual(redeems[1].value, 81000); + assert.bufferEqual(redeems[1].covenant.items[0], rules.hashName(name8)); }); it('3 REGISTERs, 1 REDEEM and 1 OPEN', async () => { @@ -1207,10 +1459,10 @@ describe('Wallet Auction', function() { const ns3 = await chain.db.getNameStateByName(name3); const ns4 = await chain.db.getNameStateByName(name4); - assert.bufferEqual(ns1.data, res1.encode()); - assert.bufferEqual(ns2.data, res2.encode()); + assert.bufferEqual(ns1.data, res1); + assert.bufferEqual(ns2.data, res2); assert.bufferEqual(ns3.data, Buffer.from([])); // revoked name data is cleared - assert.bufferEqual(ns4.data, res4.encode()); + assert.bufferEqual(ns4.data, res4); const coin1 = await wallet.getCoin(ns1.owner.hash, ns1.owner.index); assert(!coin1); // name was transferred out of wallet @@ -1481,6 +1733,25 @@ describe('Wallet Auction', function() { ); }); + it('should stop batching after OPENs reach the LIMIT', async () => { + const batch = []; + for (let i = 0; i < consensus.MAX_BLOCK_OPENS + 10; i++) + batch.push({ type: 'OPEN', args: [names[i]], id: names[i] }); + + const {mtx, errors} = await wallet.createBatch(batch, { + partialFailure: true + }); + + // +1 for change address. + assert.strictEqual(mtx.outputs.length, consensus.MAX_BLOCK_OPENS + 1); + assert.strictEqual(errors.length, 10); + + for (const [i, err] of errors.entries()) { + assert.strictEqual(err.message, 'Too many OPENs.'); + assert.strictEqual(err.id, names[consensus.MAX_BLOCK_OPENS + i]); + } + }); + it('should send batches of OPENs in sequential blocks', async () => { let count = 0; for (let i = 1; i <= 8; i++) { @@ -1556,10 +1827,11 @@ describe('Wallet Auction', function() { it('should send batches of REGISTERs', async () => { let count = 0; + for (let i = 1; i <= 8; i++) { const batch = []; for (let j = 1; j <= 100; j++) { - batch.push({ type: 'UPDATE', args: [names[count++], new Resource()]}); + batch.push({ type: 'UPDATE', args: [names[count++], EMPTY_RESOURCE]}); } await wallet.sendBatch(batch); await mineBlocks(1); @@ -1582,7 +1854,7 @@ describe('Wallet Auction', function() { it('should not batch too many UPDATEs', async () => { const batch = []; for (let i = 0; i < consensus.MAX_BLOCK_UPDATES + 1; i++) - batch.push({ type: 'UPDATE', args: [names[i], new Resource()]}); + batch.push({ type: 'UPDATE', args: [names[i], EMPTY_RESOURCE]}); await assert.rejects( wallet.createBatch(batch), @@ -1590,6 +1862,30 @@ describe('Wallet Auction', function() { ); }); + it('should stop batching after UPDATEs reach the LIMIT', async () => { + const batch = []; + for (let i = 0; i < consensus.MAX_BLOCK_UPDATES + 10; i++) { + batch.push({ + type: 'UPDATE', + args: [names[i], EMPTY_RESOURCE], + id: names[i] + }); + } + + const {mtx, errors} = await wallet.createBatch(batch, { + partialFailure: true + }); + + // +1 for change address. + assert.strictEqual(mtx.outputs.length, consensus.MAX_BLOCK_UPDATES + 1); + assert.strictEqual(errors.length, 10); + + for (const [i, err] of errors.entries()) { + assert.strictEqual(err.message, 'Too many UPDATEs.'); + assert.strictEqual(err.id, names[consensus.MAX_BLOCK_UPDATES + i]); + } + }); + it('should not RENEW any names too early', async () => { await mineBlocks( ((network.names.renewalWindow / 8) * 7) @@ -1619,6 +1915,30 @@ describe('Wallet Auction', function() { ); }); + it('should stop batching after RENEWs reach the LIMIT', async () => { + const batch = []; + for (let i = 0; i < consensus.MAX_BLOCK_RENEWALS + 10; i++) { + batch.push({ + type: 'RENEW', + args: [names[i]], + id: names[i] + }); + } + + const {mtx, errors} = await wallet.createBatch(batch, { + partialFailure: true + }); + + // +1 for change address. + assert.strictEqual(mtx.outputs.length, consensus.MAX_BLOCK_RENEWALS + 1); + assert.strictEqual(errors.length, 10); + + for (const [i, err] of errors.entries()) { + assert.strictEqual(err.message, 'Too many RENEWs.'); + assert.strictEqual(err.id, names[consensus.MAX_BLOCK_RENEWALS + i]); + } + }); + it('should send all the batches of RENEWs it needs to', async () => { await mineBlocks(8); // All names expiring, none expired yet @@ -1656,6 +1976,30 @@ describe('Wallet Auction', function() { ); }); + it('should stop batching TRANSFERs reach the LIMIT (UPDATE)', async () => { + const batch = []; + for (const name of names) { + batch.push({ + type: 'TRANSFER', + args: [name, new Address()], + id: name + }); + } + + const {mtx, errors} = await wallet.createBatch(batch, { + partialFailure: true + }); + + // +1 for change address. + assert.strictEqual(mtx.outputs.length, consensus.MAX_BLOCK_UPDATES + 1); + assert.strictEqual(errors.length, names.length - consensus.MAX_BLOCK_UPDATES); + + for (const [i, err] of errors.entries()) { + assert.strictEqual(err.message, 'Too many UPDATEs.'); + assert.strictEqual(err.id, names[consensus.MAX_BLOCK_UPDATES + i]); + } + }); + it('should send batches of TRANSFERs', async () => { const addr = Address.fromProgram(0, Buffer.alloc(20, 0xd0)); let count = 0; diff --git a/test/wallet-balance-test.js b/test/wallet-balance-test.js index 664a2c603..91b2e8da4 100644 --- a/test/wallet-balance-test.js +++ b/test/wallet-balance-test.js @@ -112,7 +112,7 @@ const FINAL_PRICE_2 = 2e5; // less then 1e6/4 (2.5e5) // const BLIND_ONLY_3 = BLIND_AMOUNT_3 - BID_AMOUNT_3; // Empty resource -const EMPTY_RS = Resource.fromJSON({ records: [] }); +const EMPTY_RS = Resource.fromJSON({ records: [] }).encode(); /* * Wallet helpers diff --git a/test/wallet-deepclean-test.js b/test/wallet-deepclean-test.js index a9b6c0961..ced4f15c1 100644 --- a/test/wallet-deepclean-test.js +++ b/test/wallet-deepclean-test.js @@ -4,7 +4,7 @@ const assert = require('bsert'); const Network = require('../lib/protocol/network'); const FullNode = require('../lib/node/fullnode'); const Address = require('../lib/primitives/address'); -const Resource = require('../lib/dns/resource'); +const {Resource} = require('../lib/dns/resource'); const network = Network.get('regtest'); @@ -93,7 +93,7 @@ describe('Wallet Deep Clean', function() { await w.sendReveal(name, {account: 0}); await mineBlocks(network.names.revealPeriod); - const res = Resource.Resource.fromJSON({ + const res = Resource.fromJSON({ records: [ { type: 'TXT', @@ -102,7 +102,7 @@ describe('Wallet Deep Clean', function() { ] }); - await w.sendUpdate(name, res, {account: 0}); + await w.sendUpdate(name, res.encode(), {account: 0}); await w.sendRedeem(name, {account: 0}); await mineBlocks(network.names.treeInterval); } diff --git a/test/wallet-http-test.js b/test/wallet-http-test.js index 9aa8ecc4a..a83f4a5ac 100644 --- a/test/wallet-http-test.js +++ b/test/wallet-http-test.js @@ -7,6 +7,8 @@ const {Resource} = require('../lib/dns/resource'); const Address = require('../lib/primitives/address'); const Output = require('../lib/primitives/output'); const HD = require('../lib/hd/hd'); +const Outpoint = require('../lib/primitives/outpoint'); +const consensus = require('../lib/protocol/consensus'); const Mnemonic = require('../lib/hd/mnemonic'); const rules = require('../lib/covenants/rules'); const {types} = rules; @@ -15,11 +17,9 @@ const network = Network.get('regtest'); const assert = require('bsert'); const {BufferSet} = require('buffer-map'); const util = require('../lib/utils/util'); -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, sleep} = require('./util/common'); +const common = require('./util/common'); +const {forEvent, sleep, delayMethodOnce} = common; const {generateInitialBlocks} = require('./util/pagination'); const { @@ -29,6 +29,24 @@ const { transferLockup } = network.names; +const HTTP_CLIENT_TIMEOUT = 100; +const TIMEOUT_METHOD = 110; +const TIMEOUT_FULL = 150; +const TIMEOUT_OPT = 0.05; + +const delayMethod2error = { + // If we delay execution of fill until client closes, + // coin selection will get aborted. + 'fill': reason => `Coin selection aborted. (Error: ${reason})`, + + // If we delay finalize (After fill), abort + // will happen once before we reach sendMTX. + 'finalize': reason => `Send was aborted. (Error: ${reason})` +}; + +const CLIENT_CLOSED = 'Client closed connection.'; +const TIMEOUT_TRIGGERED = 'Timed out.'; + describe('Wallet HTTP', function() { this.timeout(20000); @@ -263,6 +281,122 @@ describe('Wallet HTTP', function() { assert.equal(0, key.branch); assert.equal(0, key.index); }); + + it('should get new receive address (increments depth)', async () => { + const acct = await wallet.getAccount('default'); + const recvDepth = acct.receiveDepth; + + const addr = await wallet.createAddress('default'); + assert.strictEqual(addr.index, recvDepth); + + const addr2 = await wallet.createAddress('default'); + assert.strictEqual(addr2.index, recvDepth + 1); + + { + const acct = await wallet.getAccount('default'); + const newRecvDepth = acct.receiveDepth; + assert.strictEqual(newRecvDepth, recvDepth + 2); + } + }); + + it('should get new change address (increments depth)', async () => { + const acct = await wallet.getAccount('default'); + const changeDepth = acct.changeDepth; + + const addr = await wallet.createChange('default'); + assert.strictEqual(addr.index, changeDepth); + + const addr2 = await wallet.createChange('default'); + assert.strictEqual(addr2.index, changeDepth + 1); + + { + const acct = await wallet.getAccount('default'); + const newRecvDepth = acct.changeDepth; + assert.strictEqual(newRecvDepth, changeDepth + 2); + } + }); + + it('should get receive address w/o incrementing', async () => { + const account = await wallet.getAccount('default'); + const lastRecv = await wallet.createAddress('default'); + + { + const addr = await wallet.getAddress('default', account.receiveDepth - 1); + assert.strictEqual(addr.address, account.receiveAddress); + } + + { + const addr = await wallet.getAddress('default', account.receiveDepth); + assert.strictEqual(addr.address, lastRecv.address); + } + + { + const addr = await wallet.getAddress('default', account.receiveDepth + 1); + assert.strictEqual(addr.index, account.receiveDepth + 1); + } + + { + const addr = await wallet.getAddress('default', + account.receiveDepth + account.lookahead, true); + assert.strictEqual(addr.index, account.receiveDepth + account.lookahead); + } + + { + const account = await wallet.getAccount('default'); + + let err = null; + try { + await wallet.getAddress('default', + account.receiveDepth + account.lookahead + 1, true); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, 'Receive index out of bounds.'); + } + }); + + it('should get change address w/o incrementing', async () => { + const account = await wallet.getAccount('default'); + const lastChange = await wallet.createChange('default'); + + { + const addr = await wallet.getChange('default', account.changeDepth - 1); + assert.strictEqual(addr.address, account.changeAddress); + } + + { + const addr = await wallet.getChange('default', account.changeDepth); + assert.strictEqual(addr.address, lastChange.address); + } + + { + const addr = await wallet.getChange('default', account.changeDepth + 1); + assert.strictEqual(addr.index, account.changeDepth + 1); + } + + { + const addr = await wallet.getChange('default', + account.changeDepth + account.lookahead, true); + assert.strictEqual(addr.index, account.changeDepth + account.lookahead); + } + + { + const account = await wallet.getAccount('default'); + + let err = null; + try { + await wallet.getChange('default', + account.changeDepth + account.lookahead + 1, true); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, 'Change index out of bounds.'); + } + }); }); describe('Mine/Fund', function() { @@ -337,13 +471,19 @@ describe('Wallet HTTP', function() { }); describe('Create/Send transaction', function() { - let wallet2; + let wclientTimeout, walletTimeout, wallet2; before(async () => { await beforeAll(); await nodeCtx.mineBlocks(20, cbAddress); - await wclient.createWallet('secondary'); - wallet2 = wclient.wallet('secondary'); + wclientTimeout = nodeCtx.walletClient({ + timeout: HTTP_CLIENT_TIMEOUT + }); + + walletTimeout = wclientTimeout.wallet('primary'); + + await wclientTimeout.createWallet('secondary'); + wallet2 = wclientTimeout.wallet('secondary'); }); after(afterAll); @@ -481,6 +621,78 @@ describe('Wallet HTTP', function() { } }); + it('should abort tx if client is closed', async () => { + const {address} = await wallet.createChange('default'); + const output = { address, value: 1e6 }; + + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await walletTimeout.send({ + abortOnClose: true, + outputs: [output] + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, 'Request timed out.'); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(CLIENT_CLOSED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, CLIENT_CLOSED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + + it('should abort tx on timeout', async () => { + const {address} = await wallet.createChange('default'); + const output = { address, value: 1e6 }; + + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await wallet.send({ + timeout: TIMEOUT_OPT, + outputs: [output] + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, errorMessage(TIMEOUT_TRIGGERED)); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(TIMEOUT_TRIGGERED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, TIMEOUT_TRIGGERED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + it('should mine to the secondary/default wallet', async () => { const height = 5; @@ -632,7 +844,7 @@ describe('Wallet HTTP', function() { describe('Wallet auction (Integration)', function() { const accountTwo = 'foobar'; - let name, wallet2; + let name, wallet2, walletTimeout; const ownedNames = []; const allNames = []; @@ -640,6 +852,12 @@ describe('Wallet HTTP', function() { before(async () => { await beforeAll(); + const wclientTimeout = nodeCtx.walletClient({ + timeout: HTTP_CLIENT_TIMEOUT + }); + + walletTimeout = wclientTimeout.wallet('primary'); + await nodeCtx.mineBlocks(20, cbAddress); await wallet.createAccount(accountTwo); @@ -808,6 +1026,72 @@ describe('Wallet HTTP', function() { await assert.rejects(fn, {message: /Not enough funds./}); }); + it('should abort open if client is closed', async () => { + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await walletTimeout.createOpen({ + name: name, + abortOnClose: true + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, 'Request timed out.'); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(CLIENT_CLOSED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, CLIENT_CLOSED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + + it('should abort open on timeout', async () => { + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await wallet.createOpen({ + name: name, + timeout: TIMEOUT_OPT + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, errorMessage(TIMEOUT_TRIGGERED)); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(TIMEOUT_TRIGGERED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, TIMEOUT_TRIGGERED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + it('should mine to the account with no monies', async () => { const height = 5; @@ -873,6 +1157,84 @@ describe('Wallet HTTP', function() { assert.equal(blind.length, 32 * 2); }); + it('should abort bid if client is closed', async () => { + await wallet.createOpen({ name }); + allNames.push(name); + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await walletTimeout.createBid({ + name: name, + bid: 1000, + lockup: 2000, + abortOnClose: true + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, 'Request timed out.'); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(CLIENT_CLOSED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, CLIENT_CLOSED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + + it('should abort bid on timeout', async () => { + await wallet.createOpen({ name }); + allNames.push(name); + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await walletTimeout.createBid({ + name: name, + bid: 1000, + lockup: 2000, + timeout: TIMEOUT_OPT + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, 'Request timed out.'); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(TIMEOUT_TRIGGERED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, TIMEOUT_TRIGGERED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + it('should be able to get nonce', async () => { const bid = 100; @@ -1135,6 +1497,114 @@ describe('Wallet HTTP', function() { assert.equal(reveals.length, 1); }); + it('should abort reveal if client is closed', async () => { + await wallet.createOpen({ + name: name + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + // Confirmed OPEN adds name to wallet's namemap + allNames.push(name); + + await wallet.createBid({ + name: name, + bid: 1000, + lockup: 2000 + }); + + await nodeCtx.mineBlocks(biddingPeriod + 1, cbAddress); + + const {info} = await nclient.execute('getnameinfo', [name]); + assert.equal(info.name, name); + assert.equal(info.state, 'REVEAL'); + + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await walletTimeout.createReveal({ + name: name, + abortOnClose: true + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, 'Request timed out.'); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(CLIENT_CLOSED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, CLIENT_CLOSED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + + it('should abort reveal on timeout', async () => { + await wallet.createOpen({ + name: name + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + // Confirmed OPEN adds name to wallet's namemap + allNames.push(name); + + await wallet.createBid({ + name: name, + bid: 1000, + lockup: 2000 + }); + + await nodeCtx.mineBlocks(biddingPeriod + 1, cbAddress); + + const {info} = await nclient.execute('getnameinfo', [name]); + assert.equal(info.name, name); + assert.equal(info.state, 'REVEAL'); + + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await wallet.createReveal({ + name: name, + timeout: TIMEOUT_OPT + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, errorMessage(TIMEOUT_TRIGGERED)); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(TIMEOUT_TRIGGERED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, TIMEOUT_TRIGGERED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + it('should create all reveals', async () => { await wallet.createOpen({ name: name @@ -1179,27 +1649,137 @@ describe('Wallet HTTP', function() { assert.ok(revealsByName.every(reveal => reveal.height === nodeCtx.height)); }); - it('should get all reveals (single player)', async () => { + it('should abort reveal all if client is closed', async () => { await wallet.createOpen({ name: name }); - const name2 = await nclient.execute('grindname', [5]); - - await wallet.createOpen({ - name: name2 - }); - await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); // Confirmed OPEN adds name to wallet's namemap allNames.push(name); - allNames.push(name2); - await wallet.createBid({ - name: name, - bid: 1000, - lockup: 2000 + for (let i = 0; i < 3; i++) { + await wallet.createBid({ + name: name, + bid: 1000, + lockup: 2000 + }); + } + + await nodeCtx.mineBlocks(biddingPeriod + 1, cbAddress); + + const {info} = await nclient.execute('getnameinfo', [name]); + assert.equal(info.name, name); + assert.equal(info.state, 'REVEAL'); + + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await walletTimeout.createReveal({ + abortOnClose: true + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, 'Request timed out.'); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(CLIENT_CLOSED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, CLIENT_CLOSED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + + it('should abort reveal all on timeout', async () => { + await wallet.createOpen({ + name: name + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + // Confirmed OPEN adds name to wallet's namemap + allNames.push(name); + + for (let i = 0; i < 3; i++) { + await wallet.createBid({ + name: name, + bid: 1000, + lockup: 2000 + }); + } + + await nodeCtx.mineBlocks(biddingPeriod + 1, cbAddress); + + const {info} = await nclient.execute('getnameinfo', [name]); + assert.equal(info.name, name); + assert.equal(info.state, 'REVEAL'); + + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await wallet.createReveal({ + timeout: TIMEOUT_OPT + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, errorMessage(TIMEOUT_TRIGGERED)); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(TIMEOUT_TRIGGERED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, TIMEOUT_TRIGGERED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + + it('should get all reveals (single player)', async () => { + await wallet.createOpen({ + name: name + }); + + const name2 = await nclient.execute('grindname', [5]); + + await wallet.createOpen({ + name: name2 + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + // Confirmed OPEN adds name to wallet's namemap + allNames.push(name); + allNames.push(name2); + + await wallet.createBid({ + name: name, + bid: 1000, + lockup: 2000 }); await wallet.createBid({ @@ -1489,6 +2069,140 @@ describe('Wallet HTTP', function() { assert.ok(redeem.length > 0); }); + it('should abort redeem if client is closed', async () => { + await wallet.createOpen({ + name: name + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + // Confirmed OPEN adds name to wallet's namemap + allNames.push(name); + + // wallet2 wins the auction, wallet can submit redeem + await wallet.createBid({ + name: name, + bid: 1000, + lockup: 2000 + }); + + await wallet2.createBid({ + name: name, + bid: 2000, + lockup: 3000 + }); + + await nodeCtx.mineBlocks(biddingPeriod + 1, cbAddress); + + await wallet.createReveal({ + name: name + }); + + await wallet2.createReveal({ + name: name + }); + + await nodeCtx.mineBlocks(revealPeriod + 1, cbAddress); + + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await walletTimeout.createRedeem({ + name: name, + abortOnClose: true + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, 'Request timed out.'); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(CLIENT_CLOSED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, CLIENT_CLOSED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + + it('should abort redeem on timeout', async () => { + await wallet.createOpen({ + name: name + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + // Confirmed OPEN adds name to wallet's namemap + allNames.push(name); + + // wallet2 wins the auction, wallet can submit redeem + await wallet.createBid({ + name: name, + bid: 1000, + lockup: 2000 + }); + + await wallet2.createBid({ + name: name, + bid: 2000, + lockup: 3000 + }); + + await nodeCtx.mineBlocks(biddingPeriod + 1, cbAddress); + + await wallet.createReveal({ + name: name + }); + + await wallet2.createReveal({ + name: name + }); + + await nodeCtx.mineBlocks(revealPeriod + 1, cbAddress); + + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await wallet.createRedeem({ + name: name, + timeout: TIMEOUT_OPT + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, errorMessage(TIMEOUT_TRIGGERED)); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(TIMEOUT_TRIGGERED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, TIMEOUT_TRIGGERED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + it('should create an update', async () => { await wallet.createOpen({ name: name @@ -1556,30 +2270,7 @@ describe('Wallet HTTP', function() { } }); - it('should get name resource', async () => { - const names = await wallet.getNames(); - // filter out names that have data - // this test depends on the previous test - const [ns] = names.filter(n => n.data.length > 0); - assert(ns); - - const state = Resource.decode(Buffer.from(ns.data, 'hex')); - - const resource = await wallet.getResource(ns.name); - assert(resource); - const res = Resource.fromJSON(resource); - - assert.deepEqual(state, res); - }); - - it('should fail to get name resource for non existent name', async () => { - const name = await nclient.execute('grindname', [10]); - - const resource = await wallet.getResource(name); - assert.equal(resource, null); - }); - - it('should create a renewal', async () => { + it('should abort update if client is closed', async () => { await wallet.createOpen({ name: name }); @@ -1606,31 +2297,47 @@ describe('Wallet HTTP', function() { // Confirmed REVEAL with highest bid makes wallet the owner ownedNames.push(name); - await wallet.createUpdate({ - name: name, - data: { - records: [ - { - type: 'TXT', - txt: ['foobar'] - } - ] + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await walletTimeout.createUpdate({ + name: name, + data: { + records: [ + { + type: 'TXT', + txt: ['foobar'] + } + ] + }, + abortOnClose: true + }); + } catch (e) { + err = e; } - }); - // mine up to the earliest point in which a renewal - // can be submitted, a treeInterval into the future - await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + assert.ok(err); + assert.strictEqual(err.message, 'Request timed out.'); + await sleep(TIMEOUT_FULL); - const json = await wallet.createRenewal({ - name - }); + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(CLIENT_CLOSED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, CLIENT_CLOSED); - const updates = json.outputs.filter(({covenant}) => covenant.type === types.RENEW); - assert.equal(updates.length, 1); + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } }); - it('should create a transfer', async () => { + it('should abort update on timeout', async () => { await wallet.createOpen({ name: name }); @@ -1657,32 +2364,593 @@ describe('Wallet HTTP', function() { // Confirmed REVEAL with highest bid makes wallet the owner ownedNames.push(name); - await wallet.createUpdate({ - name: name, - data: { - records: [ - { - type: 'TXT', - txt: ['foobar'] - } - ] - } - }); + const prePending = await wallet.getPending(); - await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); - const {receiveAddress} = await wallet.getAccount(accountTwo); + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); - const json = await wallet.createTransfer({ - name, - address: receiveAddress - }); + let err; + try { + await wallet.createUpdate({ + name: name, + data: { + records: [ + { + type: 'TXT', + txt: ['foobar'] + } + ] + }, + timeout: TIMEOUT_OPT + }); + } catch (e) { + err = e; + } - const xfer = json.outputs.filter(({covenant}) => covenant.type === types.TRANSFER); + assert.ok(err); + assert.strictEqual(err.message, errorMessage(TIMEOUT_TRIGGERED)); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(TIMEOUT_TRIGGERED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, TIMEOUT_TRIGGERED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + + it('should get name resource', async () => { + const names = await wallet.getNames(); + // filter out names that have data + // this test depends on the previous test + const [ns] = names.filter(n => n.data.length > 0); + assert(ns); + + const state = Resource.decode(Buffer.from(ns.data, 'hex')); + + const resource = await wallet.getResource(ns.name); + assert(resource); + const res = Resource.fromJSON(resource); + + assert.deepEqual(state, res); + }); + + it('should fail to get name resource for non existent name', async () => { + const name = await nclient.execute('grindname', [10]); + + const resource = await wallet.getResource(name); + assert.equal(resource, null); + }); + + it('should create a renewal', async () => { + await wallet.createOpen({ + name: name + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + // Confirmed OPEN adds name to wallet's namemap + allNames.push(name); + + await wallet.createBid({ + name: name, + bid: 1000, + lockup: 2000 + }); + + await nodeCtx.mineBlocks(biddingPeriod + 1, cbAddress); + + await wallet.createReveal({ + name: name + }); + + await nodeCtx.mineBlocks(revealPeriod + 1, cbAddress); + + // Confirmed REVEAL with highest bid makes wallet the owner + ownedNames.push(name); + + await wallet.createUpdate({ + name: name, + data: { + records: [ + { + type: 'TXT', + txt: ['foobar'] + } + ] + } + }); + + // mine up to the earliest point in which a renewal + // can be submitted, a treeInterval into the future + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + const json = await wallet.createRenewal({ + name + }); + + const updates = json.outputs.filter(({covenant}) => covenant.type === types.RENEW); + assert.equal(updates.length, 1); + }); + + it('should abort renewal if client is closed', async () => { + await wallet.createOpen({ + name: name + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + // Confirmed OPEN adds name to wallet's namemap + allNames.push(name); + + await wallet.createBid({ + name: name, + bid: 1000, + lockup: 2000 + }); + + await nodeCtx.mineBlocks(biddingPeriod + 1, cbAddress); + + await wallet.createReveal({ + name: name + }); + + await nodeCtx.mineBlocks(revealPeriod + 1, cbAddress); + + // Confirmed REVEAL with highest bid makes wallet the owner + ownedNames.push(name); + + await wallet.createUpdate({ + name: name, + data: { + records: [ + { + type: 'TXT', + txt: ['foobar'] + } + ] + } + }); + + // mine up to the earliest point in which a renewal + // can be submitted, a treeInterval into the future + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await walletTimeout.createRenewal({ + name, + abortOnClose: true + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, 'Request timed out.'); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(CLIENT_CLOSED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, CLIENT_CLOSED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + + it('should abort renewal on timeout', async () => { + await wallet.createOpen({ + name: name + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + // Confirmed OPEN adds name to wallet's namemap + allNames.push(name); + + await wallet.createBid({ + name: name, + bid: 1000, + lockup: 2000 + }); + + await nodeCtx.mineBlocks(biddingPeriod + 1, cbAddress); + + await wallet.createReveal({ + name: name + }); + + await nodeCtx.mineBlocks(revealPeriod + 1, cbAddress); + + // Confirmed REVEAL with highest bid makes wallet the owner + ownedNames.push(name); + + await wallet.createUpdate({ + name: name, + data: { + records: [ + { + type: 'TXT', + txt: ['foobar'] + } + ] + } + }); + + // mine up to the earliest point in which a renewal + // can be submitted, a treeInterval into the future + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await wallet.createRenewal({ + name, + timeout: TIMEOUT_OPT + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, errorMessage(TIMEOUT_TRIGGERED)); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(TIMEOUT_TRIGGERED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, TIMEOUT_TRIGGERED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + + it('should create a transfer', async () => { + await wallet.createOpen({ + name: name + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + // Confirmed OPEN adds name to wallet's namemap + allNames.push(name); + + await wallet.createBid({ + name: name, + bid: 1000, + lockup: 2000 + }); + + await nodeCtx.mineBlocks(biddingPeriod + 1, cbAddress); + + await wallet.createReveal({ + name: name + }); + + await nodeCtx.mineBlocks(revealPeriod + 1, cbAddress); + + // Confirmed REVEAL with highest bid makes wallet the owner + ownedNames.push(name); + + await wallet.createUpdate({ + name: name, + data: { + records: [ + { + type: 'TXT', + txt: ['foobar'] + } + ] + } + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + const {receiveAddress} = await wallet.getAccount(accountTwo); + + const json = await wallet.createTransfer({ + name, + address: receiveAddress + }); + + const xfer = json.outputs.filter(({covenant}) => covenant.type === types.TRANSFER); assert.equal(xfer.length, 1); }); - it('should create a finalize', async () => { + it('should abort on transfer if client is closed', async () => { + await wallet.createOpen({ name }); + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + allNames.push(name); + + await wallet.createBid({ + name: name, + bid: 1000, + lockup: 2000 + }); + + await nodeCtx.mineBlocks(biddingPeriod + 1, cbAddress); + + await wallet.createReveal({ name }); + await nodeCtx.mineBlocks(revealPeriod + 1, cbAddress); + + ownedNames.push(name); + + await wallet.createUpdate({ + name: name, + data: { records: [] } + }); + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + const { receiveAddress } = await wallet.getAccount(accountTwo); + + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await walletTimeout.createTransfer({ + name, + address: receiveAddress, + abortOnClose: true + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, 'Request timed out.'); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(CLIENT_CLOSED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, CLIENT_CLOSED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + + it('should abort on transfer on timeout', async () => { + await wallet.createOpen({ name }); + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + allNames.push(name); + + await wallet.createBid({ + name: name, + bid: 1000, + lockup: 2000 + }); + + await nodeCtx.mineBlocks(biddingPeriod + 1, cbAddress); + + await wallet.createReveal({ name }); + await nodeCtx.mineBlocks(revealPeriod + 1, cbAddress); + + ownedNames.push(name); + + await wallet.createUpdate({ + name: name, + data: { records: [] } + }); + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + const { receiveAddress } = await wallet.getAccount(accountTwo); + + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await wallet.createTransfer({ + name, + address: receiveAddress, + timeout: TIMEOUT_OPT + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, errorMessage(TIMEOUT_TRIGGERED)); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(TIMEOUT_TRIGGERED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, TIMEOUT_TRIGGERED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + + it('should create a finalize', async () => { + await wallet.createOpen({ + name: name + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + // Confirmed OPEN adds name to wallet's namemap + allNames.push(name); + + await wallet.createBid({ + name: name, + bid: 1000, + lockup: 2000 + }); + + await nodeCtx.mineBlocks(biddingPeriod + 1, cbAddress); + + await wallet.createReveal({ + name: name + }); + + await nodeCtx.mineBlocks(revealPeriod + 1, cbAddress); + + // Confirmed REVEAL with highest bid makes wallet the owner + ownedNames.push(name); + + await wallet.createUpdate({ + name: name, + data: { + records: [ + { + type: 'TXT', + txt: ['foobar'] + } + ] + } + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + const {receiveAddress} = await wallet2.getAccount('default'); + + await wallet.createTransfer({ + name, + address: receiveAddress + }); + + await nodeCtx.mineBlocks(transferLockup + 1, cbAddress); + + const json = await wallet.createFinalize({ + name + }); + + const final = json.outputs.filter(({covenant}) => covenant.type === types.FINALIZE); + assert.equal(final.length, 1); + + await nodeCtx.mineBlocks(1, cbAddress); + + // Confirmed FINALIZE means this wallet is not the owner anymore! + ownedNames.splice(ownedNames.indexOf(name), 1); + + const ns = await nclient.execute('getnameinfo', [name]); + const coin = await nclient.getCoin(ns.info.owner.hash, ns.info.owner.index); + + assert.equal(coin.address, receiveAddress); + }); + + it('should abort finalize if client is closed', async () => { + await wallet.createOpen({ + name: name + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + // Confirmed OPEN adds name to wallet's namemap + allNames.push(name); + + await wallet.createBid({ + name: name, + bid: 1000, + lockup: 2000 + }); + + await nodeCtx.mineBlocks(biddingPeriod + 1, cbAddress); + + await wallet.createReveal({ + name: name + }); + + await nodeCtx.mineBlocks(revealPeriod + 1, cbAddress); + + // Confirmed REVEAL with highest bid makes wallet the owner + ownedNames.push(name); + + await wallet.createUpdate({ + name: name, + data: { + records: [ + { + type: 'TXT', + txt: ['foobar'] + } + ] + } + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + const {receiveAddress} = await wallet2.getAccount('default'); + + await wallet.createTransfer({ + name, + address: receiveAddress + }); + + await nodeCtx.mineBlocks(transferLockup + 1, cbAddress); + + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await walletTimeout.createFinalize({ + name, + abortOnClose: true + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, 'Request timed out.'); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(CLIENT_CLOSED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, CLIENT_CLOSED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + + it('should abort finalize on timeout', async () => { await wallet.createOpen({ name: name }); @@ -1732,22 +3000,36 @@ describe('Wallet HTTP', function() { await nodeCtx.mineBlocks(transferLockup + 1, cbAddress); - const json = await wallet.createFinalize({ - name - }); + const prePending = await wallet.getPending(); - const final = json.outputs.filter(({covenant}) => covenant.type === types.FINALIZE); - assert.equal(final.length, 1); + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); - await nodeCtx.mineBlocks(1, cbAddress); + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); - // Confirmed FINALIZE means this wallet is not the owner anymore! - ownedNames.splice(ownedNames.indexOf(name), 1); + let err; + try { + await wallet.createFinalize({ + name, + timeout: TIMEOUT_OPT + }); + } catch (e) { + err = e; + } - const ns = await nclient.execute('getnameinfo', [name]); - const coin = await nclient.getCoin(ns.info.owner.hash, ns.info.owner.index); + assert.ok(err); + assert.strictEqual(err.message, errorMessage(TIMEOUT_TRIGGERED)); + await sleep(TIMEOUT_FULL); - assert.equal(coin.address, receiveAddress); + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(TIMEOUT_TRIGGERED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, TIMEOUT_TRIGGERED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } }); it('should create a cancel', async () => { @@ -1817,6 +3099,170 @@ describe('Wallet HTTP', function() { assert.ok(keyInfo); }); + it('should abort cancel if client is closed', async () => { + await wallet.createOpen({ + name: name + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + // Confirmed OPEN adds name to wallet's namemap + allNames.push(name); + + await wallet.createBid({ + name: name, + bid: 1000, + lockup: 2000 + }); + + await nodeCtx.mineBlocks(biddingPeriod + 1, cbAddress); + + await wallet.createReveal({ + name: name + }); + + await nodeCtx.mineBlocks(revealPeriod + 1, cbAddress); + + // Confirmed REVEAL with highest bid makes wallet the owner + ownedNames.push(name); + + await wallet.createUpdate({ + name: name, + data: { + records: [ + { + type: 'TXT', + txt: ['foobar'] + } + ] + } + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + const {receiveAddress} = await wallet.getAccount(accountTwo); + + await wallet.createTransfer({ + name, + address: receiveAddress + }); + + await nodeCtx.mineBlocks(transferLockup + 1, cbAddress); + + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await walletTimeout.createCancel({ + name, + abortOnClose: true + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, 'Request timed out.'); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(CLIENT_CLOSED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, CLIENT_CLOSED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + + it('should abort cancel on timeout', async () => { + await wallet.createOpen({ + name: name + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + // Confirmed OPEN adds name to wallet's namemap + allNames.push(name); + + await wallet.createBid({ + name: name, + bid: 1000, + lockup: 2000 + }); + + await nodeCtx.mineBlocks(biddingPeriod + 1, cbAddress); + + await wallet.createReveal({ + name: name + }); + + await nodeCtx.mineBlocks(revealPeriod + 1, cbAddress); + + // Confirmed REVEAL with highest bid makes wallet the owner + ownedNames.push(name); + + await wallet.createUpdate({ + name: name, + data: { + records: [ + { + type: 'TXT', + txt: ['foobar'] + } + ] + } + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + const {receiveAddress} = await wallet.getAccount(accountTwo); + + await wallet.createTransfer({ + name, + address: receiveAddress + }); + + await nodeCtx.mineBlocks(transferLockup + 1, cbAddress); + + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await wallet.createCancel({ + name, + timeout: TIMEOUT_OPT + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, errorMessage(TIMEOUT_TRIGGERED)); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(TIMEOUT_TRIGGERED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, TIMEOUT_TRIGGERED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + it('should create a revoke', async () => { await wallet.createOpen({ name: name @@ -1873,6 +3319,152 @@ describe('Wallet HTTP', function() { assert.equal(ns.info.state, 'REVOKED'); }); + it('should abort revoke if client is closed', async () => { + await wallet.createOpen({ + name: name + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + // Confirmed OPEN adds name to wallet's namemap + allNames.push(name); + + await wallet.createBid({ + name: name, + bid: 1000, + lockup: 2000 + }); + + await nodeCtx.mineBlocks(biddingPeriod + 1, cbAddress); + + await wallet.createReveal({ + name: name + }); + + await nodeCtx.mineBlocks(revealPeriod + 1, cbAddress); + + // Confirmed REVEAL with highest bid makes wallet the owner + ownedNames.push(name); + + await wallet.createUpdate({ + name: name, + data: { + records: [ + { + type: 'TXT', + txt: ['foobar'] + } + ] + } + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await walletTimeout.createRevoke({ + name, + abortOnClose: true + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, 'Request timed out.'); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(CLIENT_CLOSED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, CLIENT_CLOSED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + + it('should abort revoke on timeout', async () => { + await wallet.createOpen({ + name: name + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + // Confirmed OPEN adds name to wallet's namemap + allNames.push(name); + + await wallet.createBid({ + name: name, + bid: 1000, + lockup: 2000 + }); + + await nodeCtx.mineBlocks(biddingPeriod + 1, cbAddress); + + await wallet.createReveal({ + name: name + }); + + await nodeCtx.mineBlocks(revealPeriod + 1, cbAddress); + + // Confirmed REVEAL with highest bid makes wallet the owner + ownedNames.push(name); + + await wallet.createUpdate({ + name: name, + data: { + records: [ + { + type: 'TXT', + txt: ['foobar'] + } + ] + } + }); + + await nodeCtx.mineBlocks(treeInterval + 1, cbAddress); + + const prePending = await wallet.getPending(); + + for (const [delayMethodName, errorMessage] of Object.entries(delayMethod2error)) { + delayMethodOnce(nodeCtx.wdb.primary, delayMethodName, TIMEOUT_METHOD); + + let wnodeError = null; + nodeCtx.wnode.once('error', e => wnodeError = e); + + let err; + try { + await wallet.createRevoke({ + name, + timeout: TIMEOUT_OPT + }); + } catch (e) { + err = e; + } + + assert.ok(err); + assert.strictEqual(err.message, errorMessage(TIMEOUT_TRIGGERED)); + await sleep(TIMEOUT_FULL); + + assert(wnodeError); + assert.strictEqual(wnodeError.message, errorMessage(TIMEOUT_TRIGGERED)); + assert.strictEqual(wnodeError.name, 'AbortError'); + assert.strictEqual(wnodeError.cause.message, TIMEOUT_TRIGGERED); + + const pending = await wallet.getPending(); + assert.strictEqual(pending.length - prePending.length, 0); + } + }); + it('should require passphrase for auction TXs', async () => { const passphrase = 'BitDNS!5353'; await wclient.createWallet('lockedWallet', {passphrase}); diff --git a/test/wallet-test.js b/test/wallet-test.js index 82ecb51a3..a6862c091 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -2400,7 +2400,7 @@ describe('Wallet', function() { // Advance to close wdb.height += network.names.revealPeriod; - const resource = Resource.fromJSON({records: []}); + const resource = Resource.fromJSON({records: []}).encode(); const register = await wallet.sendUpdate(name, resource, {hardFee: fee}); uTXCount++; @@ -2779,7 +2779,7 @@ describe('Wallet', function() { // Advance to close wdb.height += network.names.revealPeriod; - const resource = Resource.fromJSON({records: []}); + const resource = Resource.fromJSON({records: []}).encode(); const register = await wallet.createUpdate(name, resource, {hardFee: fee}); // Check @@ -3174,7 +3174,7 @@ describe('Wallet', function() { records: [{type: 'NS', ns: 'ns1.easyhandshake.com.'}] }); - update = await wallet.sendUpdate('cloudflare', records); + update = await wallet.sendUpdate('cloudflare', records.encode()); const entry = nextEntry(wdb); await wdb.addBlock(entry, [update]); @@ -3558,7 +3558,7 @@ describe('Wallet', function() { it('should send and confirm REGISTER', async () => { const resource = Resource.fromJSON({ records: [] }); - const register = await wallet.sendUpdate(name, resource, { + const register = await wallet.sendUpdate(name, resource.encode(), { hardFee: fee }); uTXCount++; @@ -3964,7 +3964,7 @@ describe('Wallet', function() { // Age becomes: 5 - 4 = 1. So, zap will zap all txs with age 1 // - so first 2 txs. const zapped = await wallet.zap(-1, time - 1); - assert.strictEqual(zapped, 2); + assert.strictEqual(zapped.length, 2); const txsAfterZap = await wallet.listUnconfirmed(-1, { limit: 20, @@ -3990,7 +3990,7 @@ describe('Wallet', function() { // two transactions from default (calculation above.) const zapped = await wallet.zap(DEFAULT, time - 3); - assert.strictEqual(zapped, 2); + assert.strictEqual(zapped.length, 2); const txsAfterZap = await wallet.listUnconfirmed(DEFAULT, { limit: 20,