diff --git a/lib/wallet/common.js b/lib/wallet/common.js index 367f4b261..b5e0a0d2c 100644 --- a/lib/wallet/common.js +++ b/lib/wallet/common.js @@ -146,3 +146,11 @@ common.sortDeps = function sortDeps(txs) { return result; }; + +common.AbortError = class AbortError extends Error { + constructor(msg = 'Operation was aborted.') { + super(msg); + this.type = 'ABORT'; + this.message = msg; + } +}; diff --git a/lib/wallet/http.js b/lib/wallet/http.js index 21d0f1fbe..73bdf9ede 100644 --- a/lib/wallet/http.js +++ b/lib/wallet/http.js @@ -89,6 +89,17 @@ class HTTP extends Server { */ initRouter() { + // TODO: This needs to go to the bweb using proper + // shim for the EventTarget/Event and AbortController/Signal. + // This can potentially allow us to even Abort coin selection. + this.use(async (req, res) => { + res.signal = { + get aborted() { + return res.closed; + } + }; + }); + if (this.options.cors) this.use(this.cors()); @@ -128,6 +139,9 @@ class HTTP extends Server { this.use(this.router()); this.error((err, req, res) => { + if (!err.statusCode && err.type === 'ABORT') + err.statusCode = 408; + const code = err.statusCode || 500; res.json(code, { error: { @@ -449,16 +463,9 @@ class HTTP extends Server { // Send TX this.post('/wallet/:id/send', async (req, res) => { const valid = Validator.fromRequest(req); - const passphrase = valid.str('passphrase'); - const abortOnClose = valid.str('abortOnClose', true); + const options = TransactionOptions.fromValidator(valid, res.signal); + const tx = await req.wallet.send(options); - const options = TransactionOptions.fromValidator(valid); - const mtx = await req.wallet.createTX(options); - - if (abortOnClose && res.closed) - throw abortError(); - - const tx = await req.wallet.sendMTX(mtx, passphrase); const details = await req.wallet.getDetails(tx.hash()); res.json(200, details.getJSON(this.network, this.wdb.height)); }); @@ -469,7 +476,7 @@ class HTTP extends Server { const passphrase = valid.str('passphrase'); const sign = valid.bool('sign', true); - const options = TransactionOptions.fromValidator(valid); + const options = TransactionOptions.fromValidator(valid, res.signal); const tx = await req.wallet.createTX(options); if (sign) @@ -1184,27 +1191,23 @@ class HTTP extends Server { const valid = Validator.fromRequest(req); const name = valid.str('name'); const force = valid.bool('force', false); - const passphrase = valid.str('passphrase'); const broadcast = valid.bool('broadcast', true); const sign = valid.bool('sign', true); - const abortOnClose = valid.str('abortOnClose', true); assert(name, 'Name is required.'); assert(broadcast ? sign : true, 'Must sign when broadcasting.'); - const options = TransactionOptions.fromValidator(valid); - const mtx = await req.wallet.createOpen(name, force, options); - - if (abortOnClose && res.closed) - throw abortError(); + const options = TransactionOptions.fromValidator(valid, res.signal); if (broadcast) { - const tx = await req.wallet.sendMTX(mtx, passphrase); + const tx = await req.wallet.sendOpen(name, force, options); return res.json(200, tx.getJSON(this.network)); } + const mtx = await req.wallet.createOpen(name, force, options); + if (sign) - await req.wallet.sign(mtx, passphrase); + await req.wallet.sign(mtx, options.passphrase); return res.json(200, mtx.getJSON(this.network)); }); @@ -1215,36 +1218,25 @@ class HTTP extends Server { const name = valid.str('name'); const bid = valid.u64('bid'); const lockup = valid.u64('lockup'); - const passphrase = valid.str('passphrase'); const broadcast = valid.bool('broadcast', true); const sign = valid.bool('sign', true); - const abortOnClose = valid.str('abortOnClose', true); assert(name, 'Name is required.'); assert(bid != null, 'Bid is required.'); assert(lockup != null, 'Lockup is required.'); assert(broadcast ? sign : true, 'Must sign when broadcasting.'); - const options = TransactionOptions.fromValidator(valid); - const mtx = await req.wallet.createBid(name, bid, lockup, options); - - if (abortOnClose && res.closed) - throw abortError(); + const options = TransactionOptions.fromValidator(valid, res.signal); if (broadcast) { - const isBlockingSend = (bid <= 10_000_000); - let tx; - if (isBlockingSend) { - tx = await req.wallet.sendMTXBlocking(mtx, passphrase); - } else { - tx = await req.wallet.sendMTX(mtx, passphrase); - } - + const tx = await req.wallet.sendBid(name, bid, lockup, options); return res.json(200, tx.getJSON(this.network)); } + const mtx = await req.wallet.createBid(name, bid, lockup, options); + if (sign) - await req.wallet.sign(mtx, passphrase); + await req.wallet.sign(mtx, options.passphrase); return res.json(200, mtx.getJSON(this.network)); }); @@ -1266,7 +1258,7 @@ class HTTP extends Server { assert(broadcastBid != null, 'broadcastBid is required.'); assert(broadcastBid ? sign : true, 'Must sign when broadcasting.'); - const options = TransactionOptions.fromValidator(valid); + const options = TransactionOptions.fromValidator(valid, res.signal); const auctionTxs = await req.wallet.createAuctionTxs( name, bid, @@ -1296,31 +1288,32 @@ class HTTP extends Server { this.post('/wallet/:id/reveal', async (req, res) => { const valid = Validator.fromRequest(req); const name = valid.str('name'); - const passphrase = valid.str('passphrase'); const broadcast = valid.bool('broadcast', true); const sign = valid.bool('sign', true); - const abortOnClose = valid.str('abortOnClose', true); assert(broadcast ? sign : true, 'Must sign when broadcasting.'); - if (!name) { - const tx = await req.wallet.sendRevealAll(); + const options = TransactionOptions.fromValidator(valid, res.signal); + + if (broadcast && !name) { + const tx = await req.wallet.sendRevealAll(options); return res.json(200, tx.getJSON(this.network)); } - const options = TransactionOptions.fromValidator(valid); - const mtx = await req.wallet.createReveal(name, options); - - if (abortOnClose && res.closed) - throw abortError(); - - if (broadcast) { - const tx = await req.wallet.sendMTX(mtx, passphrase); + if (broadcast && name) { + const tx = await req.wallet.sendReveal(name, options); return res.json(200, tx.getJSON(this.network)); } + let mtx; + + if (!name) + mtx = await req.wallet.createRevealAll(options); + else + mtx = await req.wallet.createReveal(name, options); + if (sign) - await req.wallet.sign(mtx, passphrase); + await req.wallet.sign(mtx, options.passphrase); return res.json(200, mtx.getJSON(this.network)); }); @@ -1329,31 +1322,32 @@ class HTTP extends Server { this.post('/wallet/:id/redeem', async (req, res) => { const valid = Validator.fromRequest(req); const name = valid.str('name'); - const passphrase = valid.str('passphrase'); const broadcast = valid.bool('broadcast', true); const sign = valid.bool('sign', true); - const abortOnClose = valid.str('abortOnClose', true); assert(broadcast ? sign : true, 'Must sign when broadcasting.'); - if (!name) { - const tx = await req.wallet.sendRedeemAll(); + const options = TransactionOptions.fromValidator(valid, res.signal); + + if (broadcast && !name) { + const tx = await req.wallet.sendRedeemAll(options); return res.json(200, tx.getJSON(this.network)); } - const options = TransactionOptions.fromValidator(valid); - const mtx = await req.wallet.createRedeem(name, options); - - if (abortOnClose && res.closed) - throw abortError(); - - if (broadcast) { - const tx = await req.wallet.sendMTX(mtx, passphrase); + if (broadcast && name) { + const tx = await req.wallet.sendRedeem(name, options); return res.json(200, tx.getJSON(this.network)); } + let mtx; + + if (!name) + mtx = await req.wallet.createRedeemAll(options); + else + mtx = await req.wallet.createRedeem(name, options); + if (sign) - await req.wallet.sign(mtx, passphrase); + await req.wallet.sign(mtx, options.passphrase); return res.json(200, mtx.getJSON(this.network)); }); @@ -1363,10 +1357,8 @@ class HTTP extends Server { const valid = Validator.fromRequest(req); const name = valid.str('name'); const data = valid.obj('data'); - const passphrase = valid.str('passphrase'); const broadcast = valid.bool('broadcast', true); const sign = valid.bool('sign', true); - const abortOnClose = valid.str('abortOnClose', true); assert(broadcast ? sign : true, 'Must sign when broadcasting.'); assert(name, 'Must pass name.'); @@ -1379,19 +1371,17 @@ class HTTP extends Server { return res.json(400); } - const options = TransactionOptions.fromValidator(valid); - const mtx = await req.wallet.createUpdate(name, resource, options); - - if (abortOnClose && res.closed) - throw abortError(); + const options = TransactionOptions.fromValidator(valid, res.signal); if (broadcast) { - const tx = await req.wallet.sendMTX(mtx, passphrase); + const tx = await req.wallet.sendUpdate(name, resource, options); return res.json(200, tx.getJSON(this.network)); } + const mtx = await req.wallet.createUpdate(name, resource, options); + if (sign) - await req.wallet.sign(mtx, passphrase); + await req.wallet.sign(mtx, options.passphrase); return res.json(200, mtx.getJSON(this.network)); }); @@ -1401,28 +1391,24 @@ class HTTP extends Server { const valid = Validator.fromRequest(req); const name = valid.str('name'); const resourceHex = valid.str('resourceHex'); - const passphrase = valid.str('passphrase'); const broadcast = valid.bool('broadcast', true); const sign = valid.bool('sign', true); - const abortOnClose = valid.str('abortOnClose', true); assert(broadcast ? sign : true, 'Must sign when broadcasting.'); assert(name, 'Must pass name.'); assert(resourceHex, 'Must pass resourceHex.'); - const options = TransactionOptions.fromValidator(valid); - const mtx = await req.wallet.createRawUpdate(name, resourceHex, options); - - if (abortOnClose && res.closed) - throw abortError(); + const options = TransactionOptions.fromValidator(valid, res.signal); if (broadcast) { - const tx = await req.wallet.sendMTX(mtx, passphrase); + const tx = await req.wallet.sendRawUpdate(name, resourceHex, options); return res.json(200, tx.getJSON(this.network)); } + const mtx = await req.wallet.createRawUpdate(name, resourceHex, options); + if (sign) - await req.wallet.sign(mtx, passphrase); + await req.wallet.sign(mtx, options.passphrase); return res.json(200, mtx.getJSON(this.network)); }); @@ -1431,27 +1417,23 @@ class HTTP extends Server { this.post('/wallet/:id/renewal', async (req, res) => { const valid = Validator.fromRequest(req); const name = valid.str('name'); - const passphrase = valid.str('passphrase'); const broadcast = valid.bool('broadcast', true); const sign = valid.bool('sign', true); - const abortOnClose = valid.str('abortOnClose', true); assert(broadcast ? sign : true, 'Must sign when broadcasting.'); assert(name, 'Must pass name.'); - const options = TransactionOptions.fromValidator(valid); - const mtx = await req.wallet.createRenewal(name, options); - - if (abortOnClose && res.closed) - throw abortError(); + const options = TransactionOptions.fromValidator(valid, res.signal); if (broadcast) { - const tx = await req.wallet.sendMTX(mtx, passphrase); + const tx = await req.wallet.sendRenewal(name, options); return res.json(200, tx.getJSON(this.network)); } + const mtx = await req.wallet.createRenewal(name, options); + if (sign) - await req.wallet.sign(mtx, passphrase); + await req.wallet.sign(mtx, options.passphrase); return res.json(200, mtx.getJSON(this.network)); }); @@ -1461,29 +1443,25 @@ class HTTP extends Server { const valid = Validator.fromRequest(req); const name = valid.str('name'); const address = valid.str('address'); - const passphrase = valid.str('passphrase'); const broadcast = valid.bool('broadcast', true); const sign = valid.bool('sign', true); - const abortOnClose = valid.str('abortOnClose', true); assert(broadcast ? sign : true, 'Must sign when broadcasting.'); assert(name, 'Must pass name.'); assert(address, 'Must pass address.'); const addr = Address.fromString(address, this.network); - const options = TransactionOptions.fromValidator(valid); - const mtx = await req.wallet.createTransfer(name, addr, options); - - if (abortOnClose && res.closed) - throw abortError(); + const options = TransactionOptions.fromValidator(valid, res.signal); if (broadcast) { - const tx = await req.wallet.sendMTX(mtx, passphrase); + const tx = await req.wallet.sendTransfer(name, addr, options); return res.json(200, tx.getJSON(this.network)); } + const mtx = await req.wallet.createTransfer(name, addr, options); + if (sign) - await req.wallet.sign(mtx, passphrase); + await req.wallet.sign(mtx, options.passphrase); return res.json(200, mtx.getJSON(this.network)); }); @@ -1492,27 +1470,23 @@ class HTTP extends Server { this.post('/wallet/:id/cancel', async (req, res) => { const valid = Validator.fromRequest(req); const name = valid.str('name'); - const passphrase = valid.str('passphrase'); const broadcast = valid.bool('broadcast', true); const sign = valid.bool('sign', true); - const abortOnClose = valid.str('abortOnClose', true); assert(broadcast ? sign : true, 'Must sign when broadcasting.'); assert(name, 'Must pass name.'); - const options = TransactionOptions.fromValidator(valid); - const mtx = await req.wallet.createCancel(name, options); - - if (abortOnClose && res.closed) - throw abortError(); + const options = TransactionOptions.fromValidator(valid, res.signal); if (broadcast) { - const tx = await req.wallet.sendMTX(mtx, passphrase); + const tx = await req.wallet.sendCancel(name, options); return res.json(200, tx.getJSON(this.network)); } + const mtx = await req.wallet.createCancel(name, options); + if (sign) - await req.wallet.sign(mtx, passphrase); + await req.wallet.sign(mtx, options.passphrase); return res.json(200, mtx.getJSON(this.network)); }); @@ -1521,27 +1495,23 @@ class HTTP extends Server { this.post('/wallet/:id/finalize', async (req, res) => { const valid = Validator.fromRequest(req); const name = valid.str('name'); - const passphrase = valid.str('passphrase'); const broadcast = valid.bool('broadcast', true); const sign = valid.bool('sign', true); - const abortOnClose = valid.str('abortOnClose', true); assert(broadcast ? sign : true, 'Must sign when broadcasting.'); assert(name, 'Must pass name.'); - const options = TransactionOptions.fromValidator(valid); - const mtx = await req.wallet.createFinalize(name, options); - - if (abortOnClose && res.closed) - throw abortError(); + const options = TransactionOptions.fromValidator(valid, res.signal); if (broadcast) { - const tx = await req.wallet.sendMTX(mtx, passphrase); + const tx = await req.wallet.sendFinalize(name, options); return res.json(200, tx.getJSON(this.network)); } + const mtx = await req.wallet.createFinalize(name, options); + if (sign) - await req.wallet.sign(mtx, passphrase); + await req.wallet.sign(mtx, options.passphrase); return res.json(200, mtx.getJSON(this.network)); }); @@ -1550,27 +1520,23 @@ class HTTP extends Server { this.post('/wallet/:id/revoke', async (req, res) => { const valid = Validator.fromRequest(req); const name = valid.str('name'); - const passphrase = valid.str('passphrase'); const broadcast = valid.bool('broadcast', true); const sign = valid.bool('sign', true); - const abortOnClose = valid.str('abortOnClose', true); assert(broadcast ? sign : true, 'Must sign when broadcasting.'); assert(name, 'Must pass name.'); - const options = TransactionOptions.fromValidator(valid); - const mtx = await req.wallet.createRevoke(name, options); - - if (abortOnClose && res.closed) - throw abortError(); + const options = TransactionOptions.fromValidator(valid, res.signal); if (broadcast) { - const tx = await req.wallet.sendMTX(mtx, passphrase); + const tx = await req.wallet.sendRevoke(name, options); return res.json(200, tx.getJSON(this.network)); } + const mtx = await req.wallet.createRevoke(name, options); + if (sign) - await req.wallet.sign(mtx, passphrase); + await req.wallet.sign(mtx, options.passphrase); return res.json(200, mtx.getJSON(this.network)); }); @@ -1978,27 +1944,37 @@ class HTTPOptions { } } +const NULL_SIGNAL = { + get aborted() { + return false; + } +}; + class TransactionOptions { /** * TransactionOptions * @alias module:http.TransactionOptions * @constructor * @param {Validator} valid + * @param {Signal} signal */ - constructor(valid) { + constructor(valid, signal) { + this.signal = NULL_SIGNAL; + if (valid) - return this.fromValidator(valid); + return this.fromValidator(valid, signal); } /** * Inject properties from Validator. * @private * @param {Validator} valid + * @param {Signal} signal * @returns {TransactionOptions} */ - fromValidator(valid) { + fromValidator(valid, signal) { assert(valid); this.rate = valid.u64('rate'); @@ -2012,6 +1988,15 @@ class TransactionOptions { this.subtractIndex = valid.i32('subtractIndex'); this.depth = valid.u32(['confirmations', 'depth']); this.paths = valid.bool('paths'); + this.passphrase = valid.str('passphrase'); + this.hardFee = valid.u64('hardFee'); + this.abortOnClose = valid.bool('abortOnClose', true); + + if (this.abortOnClose) { + assert(signal); + this.signal = signal; + } + this.outputs = []; if (valid.has('outputs')) { @@ -2045,11 +2030,12 @@ class TransactionOptions { * Instantiate transaction options * from Validator. * @param {Validator} valid + * @param {Signal} signal * @returns {TransactionOptions} */ - static fromValidator(valid) { - return new this().fromValidator(valid); + static fromValidator(valid, signal) { + return new this().fromValidator(valid, signal); } } diff --git a/lib/wallet/nodeclient.js b/lib/wallet/nodeclient.js index 892c41640..166267a73 100644 --- a/lib/wallet/nodeclient.js +++ b/lib/wallet/nodeclient.js @@ -137,16 +137,11 @@ class NodeClient extends AsyncEmitter { /** * Send a transaction. Do wait for promise. * @param {TX} tx - * @param {Boolean} isSendBlocking * @returns {Promise} */ - async send(tx, isSendBlocking) { - if (isSendBlocking) { - await this.node.relay(tx, isSendBlocking); - } else { - this.node.relay(tx); - } + async send(tx) { + this.node.relay(tx); } /** diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index d577002e9..6103c552d 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -69,6 +69,13 @@ const util = require('../utils/util'); * @property {ErrorMessage[]} errors */ +/** + * @typedef {Object} BatchSendResponse + * @property {TX} tx + * @property {MTX} mtx + * @property {ErrorMessage[]} errors + */ + const Coin = require('../primitives/coin'); const Outpoint = require('../primitives/outpoint'); @@ -1682,7 +1689,7 @@ class Wallet extends EventEmitter { * @param {Array} names * @param {Number|String} acct * @param {Boolean} force - * @returns {MTX} + * @returns {Promise} */ async makeBatchOpen(names, force, acct) { @@ -1706,7 +1713,9 @@ class Wallet extends EventEmitter { // TODO: Handle expired behavior. if (rules.isReserved(nameHash, height, network)) { - errorMessages.push({ name: name, error: 'Name is reserved' }); + errorMessages.push({ + name: name, + error: 'Name is reserved' }); continue; } @@ -1726,12 +1735,18 @@ class Wallet extends EventEmitter { const start = ns.height; if (state !== states.OPENING) { - errorMessages.push({ name: name, error: `Name is not available: "${name}".` }); + errorMessages.push({ + name: name, + error: `Name is not available: "${name}".` + }); continue; } if (start !== 0 && start !== height) { - errorMessages.push({ name: name, error: `Name is already opening: "${name}".` }); + errorMessages.push({ + name: name, + error: `Name is already opening: "${name}".` + }); continue; } @@ -1746,7 +1761,10 @@ class Wallet extends EventEmitter { output.covenant.push(rawName); if (await this.txdb.isCovenantDoubleOpen(output.covenant)) { - errorMessages.push({ name: name, error: `Already sent an open for: ${name}.` }); + errorMessages.push({ + name: name, + error: `Already sent an open for: ${name}.` + }); continue; } @@ -1754,7 +1772,11 @@ class Wallet extends EventEmitter { } const isAllError = (names.length === errorMessages.length); - return { mtx: mtx, errors: errorMessages, isAllError: isAllError }; + return { + mtx: mtx, + errors: errorMessages, + isAllError: isAllError + }; } /** @@ -1768,15 +1790,28 @@ class Wallet extends EventEmitter { async _createBatchOpen(names, force, options) { const acct = options ? options.account || 0 : 0; - const { mtx, errors, isAllError } = await this - .makeBatchOpen(names, force, acct); - if (!isAllError) { - await this.fill(mtx, options); - const finalizedMtx = await this.finalize(mtx, options); - return { mtx: finalizedMtx, errors: errors }; - } else { - return { mtx: null, errors: errors, isAllError: true }; + const { + mtx, + errors, + isAllError + } = await this.makeBatchOpen(names, force, acct); + + if (isAllError) { + return { + mtx: null, + errors: errors, + isAllError: true + }; } + + await this.fill(mtx, options); + const finalizedMtx = await this.finalize(mtx, options); + + return { + mtx: finalizedMtx, + errors: errors, + isAllError: false + }; } /** @@ -1797,6 +1832,62 @@ class Wallet extends EventEmitter { } } + /** + * Create and send a batch open + * without a lock. + * @param {Array} names + * @param {Boolean} force + * @param {Object} options + * @returns {Promise} + */ + + async _sendBatchOpen(names, force, options) { + const passphrase = options ? options.passphrase : null; + const { + mtx, + errors, + isAllError + } = await this._createBatchOpen(names, force, options); + + if (isAllError) { + return { + tx: null, + mtx: null, + errors, + isAllError + }; + } + + checkAbort(options && options.signal); + + const tx = await this.sendMTX(mtx, passphrase); + + return { + tx, + mtx, + errors, + isAllError + }; + } + + /** + * Create and send a batch open + * with a lock. + * @param {Array} names + * @param {Boolean} force + * @param {Object} options + * @returns {Promise} + */ + + async sendBatchOpen(names, force, options) { + const unlock = await this.fundLock.lock(); + try { + return await this._sendBatchOpen(names, force, options); + } finally { + unlock(); + } + } + /** * Create and finalize an open * MTX without a lock. @@ -1807,7 +1898,11 @@ class Wallet extends EventEmitter { */ async _createOpen(name, force, options) { - const { mtx, errors, isAllError } = await this._createBatchOpen(Array.of(name), force, options); + const { + mtx, + errors, + isAllError + } = await this._createBatchOpen(Array.of(name), force, options); if (isAllError) { throw new Error(errors[0].error); } else { @@ -1844,6 +1939,9 @@ class Wallet extends EventEmitter { async _sendOpen(name, force, options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createOpen(name, force, options); + + checkAbort(options && options.signal); + return this.sendMTX(mtx, passphrase); } @@ -1987,6 +2085,64 @@ class Wallet extends EventEmitter { return output; } + /** + * Create batch bid + * @param {Array} bids + * @param {Object} options + * @returns {Promise} + */ + + async createBatchBid(bids, options) { + const unlock = await this.fundLock.lock(); + try { + return await this._createBid(bids, options); + } finally { + unlock(); + } + } + + /** + * Send batch bids without lock. + * @param {Array} bids + * @param {Object} options + * @returns {Promise} + */ + + async _sendBatchBid(bids, options) { + const passphrase = options ? options.passphrase : null; + const { + mtx, + errorMessages + } = await this._createBid(bids, options); + + checkAbort(options && options.signal); + + const tx = await this.sendMTX(mtx, passphrase); + + return { + tx, + mtx, + errorMessages + }; + } + + /** + * Send batch bids with lock. + * @param {Array} bids + * @param {Object} options + * @returns {Promise} + */ + + async sendBatchBid(bids, options) { + const unlock = await this.fundLock.lock(); + + try { + return await this._sendBatchBid(bids, options); + } finally { + unlock(); + } + } + /** * Create and finalize a bid * MTX without a lock. @@ -1994,7 +2150,7 @@ class Wallet extends EventEmitter { * @param {Number} value * @param {Number} lockup * @param {Object} options - * @returns {MTX} + * @returns {Promise} */ async _createBid(bids, options) { @@ -2012,7 +2168,7 @@ class Wallet extends EventEmitter { * @param {Number} value * @param {Number} lockup * @param {Object} options - * @returns {MTX} + * @returns {Promise} */ async createBid(name, value, lockup, options) { @@ -2025,26 +2181,21 @@ class Wallet extends EventEmitter { } } - async createBatchBid(bids, options) { - const unlock = await this.fundLock.lock(); - try { - return await this._createBid(bids, options); - } finally { - unlock(); - } - } - /** * Create and send a bid MTX. * @param {String} name * @param {Number} value * @param {Number} lockup * @param {Object} options + * @returns {Promise} */ async _sendBid(name, value, lockup, options) { const passphrase = options ? options.passphrase : null; const { mtx } = await this._createBid([{ name, value, lockup }], options); + + checkAbort(options && options.signal); + return this.sendMTX(mtx, passphrase); } @@ -2054,6 +2205,7 @@ class Wallet extends EventEmitter { * @param {Number} value * @param {Number} lockup * @param {Object} options + * @returns {Promise} */ async sendBid(name, value, lockup, options) { @@ -2308,7 +2460,7 @@ class Wallet extends EventEmitter { * MTX without a lock. * @param {Array} names * @param {Object} options - * @returns {MTX} + * @returns {Promise} */ async _createBatchReveal(names, options) { @@ -2324,7 +2476,7 @@ class Wallet extends EventEmitter { * MTX with a lock. * @param {Array} names * @param {Object} options - * @returns {MTX} + * @returns {Promise} */ async createBatchReveal(names, options) { @@ -2336,12 +2488,56 @@ class Wallet extends EventEmitter { } } + /** + * Create and finalize a batch reveal + * without a lock. + * @param {Array} names + * @param {Object} options + * @returns {Promise} + */ + + async _sendBatchReveal(names, options) { + const passphrase = options ? options.passphrase : null; + + const { + mtx, + errorMessages + } = await this._createBatchReveal(names, options); + + checkAbort(options && options.signal); + + const tx = await this.sendMTX(mtx, passphrase); + + return { + tx, + mtx, + errorMessages + }; + } + + /** + * Create and finalize a batch reveal + * with a lock. + * @param {Array} names + * @param {Object} options + * @returns {Promise} + */ + + async sendBatchReveal(names, options) { + const unlock = await this.fundLock.lock(); + try { + return await this._sendBatchReveal(names, options); + } finally { + unlock(); + } + } + /** * Create and finalize a reveal * MTX with a lock. * @param {String} name * @param {Object} options - * @returns {MTX} + * @returns {Promise} */ async createReveal(name, options) { @@ -2358,11 +2554,15 @@ class Wallet extends EventEmitter { * Create and send a reveal MTX. * @param {String} name * @param {Object} options + * @returns {Promise} */ async _sendReveal(name, options) { const passphrase = options ? options.passphrase : null; const { mtx } = await this._createBatchReveal([name], options); + + checkAbort(options && options.signal); + return this.sendMTX(mtx, passphrase); } @@ -2370,6 +2570,7 @@ class Wallet extends EventEmitter { * Create and send a bid MTX. * @param {String} name * @param {Object} options + * @returns {Promise} */ async sendReveal(name, options) { @@ -2383,7 +2584,7 @@ class Wallet extends EventEmitter { /** * Make a reveal MTX. - * @returns {MTX} + * @returns {Promise} */ async makeRevealAll() { @@ -2412,7 +2613,7 @@ class Wallet extends EventEmitter { continue; const { hash, index } = prevout; - const coin = await this.getCoin(hash, index); + const coin = await this.getUnspentCoin(hash, index); if (!coin) continue; @@ -2484,6 +2685,9 @@ class Wallet extends EventEmitter { async _sendRevealAll(options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createRevealAll(options); + + checkAbort(options && options.signal); + return this.sendMTX(mtx, passphrase); } @@ -2562,8 +2766,6 @@ class Wallet extends EventEmitter { if (coin.height < ns.height) continue; - mtx.addOutpoint(prevout); - const output = new Output(); output.address = coin.address; output.value = coin.value; @@ -2571,6 +2773,7 @@ class Wallet extends EventEmitter { output.covenant.pushHash(nameHash); output.covenant.pushU32(ns.height); + mtx.addOutpoint(prevout); mtx.outputs.push(output); } @@ -2740,6 +2943,51 @@ class Wallet extends EventEmitter { } } + /** + * Create and finalize a redeem + * without a lock. + * @param {Array} names + * - {name: string, data: Object} + * @param {Object} options + * @returns {Promise} + */ + + async _sendBatchFinish(names, options) { + const passphrase = options ? options.passphrase : null; + const { + mtx, + errorMessages + } = await this._createBatchFinish(names, options); + + checkAbort(options && options.signal); + + const tx = await this.sendMTX(mtx, passphrase); + + return { + tx, + mtx, + errorMessages + }; + } + + /** + * Create and finalize a redeem + * with a lock. + * @param {Array} names + * - {name: string, data: Object} + * @param {Object} options + * @returns {Promise} + */ + + async sendBatchFinish(names, options) { + const unlock = await this.fundLock.lock(); + try { + return await this._sendBatchFinish(names, options); + } finally { + unlock(); + } + } + /** * Create and finalize a redeem * MTX without a lock. @@ -2782,6 +3030,9 @@ class Wallet extends EventEmitter { async _sendRedeem(name, options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createRedeem(name, options); + + checkAbort(options && options.signal); + return this.sendMTX(mtx, passphrase); } @@ -2835,7 +3086,7 @@ class Wallet extends EventEmitter { if (prevout.equals(ns.owner)) continue; - const coin = await this.getCoin(hash, index); + const coin = await this.getUnspentCoin(hash, index); if (!coin) continue; @@ -2900,6 +3151,9 @@ class Wallet extends EventEmitter { async _sendRedeemAll(options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createRedeemAll(options); + + checkAbort(options && options.signal); + return this.sendMTX(mtx, passphrase); } @@ -3303,6 +3557,9 @@ class Wallet extends EventEmitter { async _sendUpdate(name, resource, options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createUpdate(name, resource, options); + + checkAbort(options && options.signal); + return this.sendMTX(mtx, passphrase); } @@ -3317,6 +3574,9 @@ class Wallet extends EventEmitter { async _sendRawUpdate(name, resourceHex, options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createRawUpdate(name, resourceHex, options); + + checkAbort(options && options.signal); + return this.sendMTX(mtx, passphrase); } @@ -3488,6 +3748,46 @@ class Wallet extends EventEmitter { } } + /** + * Create and finalize multiple renewals + * without lock. + * @param {String[]} names + * @param {Object} options + * @returns {Promise} + */ + + async _sendBatchRenewal(names, options) { + const passphrase = options ? options.passphrase : null; + const {mtx, errors} = await this._createBatchRenewal(names, options); + + checkAbort(options && options.signal); + + const tx = await this.sendMTX(mtx, passphrase); + + return { + tx, + mtx, + errors + }; + } + + /** + * Create and finalize multiple renewals + * with lock. + * @param {String[]} names + * @param {Object} options + * @returns {Promise} + */ + + async sendBatchRenewal(names, options) { + const unlock = await this.fundLock.lock(); + try { + return await this._sendBatchRenewal(names, options); + } finally { + unlock(); + } + } + /** * Create and finalize a renewal * MTX with a lock. @@ -3528,6 +3828,8 @@ class Wallet extends EventEmitter { throw new Error(error0.errorMessage); } + checkAbort(options && options.signal); + return this.sendMTX(mtx, passphrase); } @@ -3669,6 +3971,9 @@ class Wallet extends EventEmitter { address, options ); + + checkAbort(options && options.signal); + return this.sendMTX(mtx, passphrase); } @@ -3798,6 +4103,9 @@ class Wallet extends EventEmitter { async _sendCancel(name, options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createCancel(name, options); + + checkAbort(options && options.signal); + return this.sendMTX(mtx, passphrase); } @@ -3942,6 +4250,9 @@ class Wallet extends EventEmitter { async _sendFinalize(name, options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createFinalize(name, options); + + checkAbort(options && options.signal); + return this.sendMTX(mtx, passphrase); } @@ -4073,6 +4384,9 @@ class Wallet extends EventEmitter { async _sendRevoke(name, options) { const passphrase = options ? options.passphrase : null; const mtx = await this._createRevoke(name, options); + + checkAbort(options && options.signal); + return this.sendMTX(mtx, passphrase); } @@ -4244,10 +4558,10 @@ class Wallet extends EventEmitter { * @returns {Promise} - Returns {@link TX}. */ - async send(options, passphrase) { + async send(options) { const unlock = await this.fundLock.lock(); try { - return await this._send(options, passphrase); + return await this._send(options); } finally { unlock(); } @@ -4261,41 +4575,22 @@ class Wallet extends EventEmitter { * @returns {Promise} - Returns {@link TX}. */ - async _send(options, passphrase) { + async _send(options) { + const passphrase = options ? options.passphrase : null; const mtx = await this.createTX(options, true); - return this.sendMTX(mtx, passphrase); - } - - /** - * Sign and send a (templated) mutable transaction. - * @param {MTX} mtx - * @param {String} passphrase - */ - async sendMTX(mtx, passphrase) { - return this._sendMTX(mtx, passphrase, false); - } + checkAbort(options && options.signal); - /** - * Sign and send a (templated) mutable transaction. - * This call is blocking, either succeeds or fails - * with an exception - * @param {MTX} mtx - * @param {String} passphrase - */ - - async sendMTXBlocking(mtx, passphrase) { - return this._sendMTX(mtx, passphrase, true); + return this.sendMTX(mtx, passphrase); } /** * Sign and send a (templated) mutable transaction. * @param {MTX} mtx * @param {String} passphrase - * @param {Boolean} isSendBlocking */ - async _sendMTX(mtx, passphrase, isSendBlocking) { + async sendMTX(mtx, passphrase) { await this.sign(mtx, passphrase); if (!mtx.isSigned()) @@ -4327,12 +4622,12 @@ class Wallet extends EventEmitter { } } + await this.wdb.addTX(tx); + this.logger.debug('Sending wallet tx (%s): %x', this.id, tx.hash()); // send to mempool, if succeeds proceed and store - await this.wdb.send(tx, isSendBlocking); - - await this.wdb.addTX(tx); + await this.wdb.send(tx); return tx; } @@ -4892,6 +5187,22 @@ class Wallet extends EventEmitter { return this.txdb.getCoin(hash, index); } + /** + * Get an unspent coin from the wallet. + * @param {Hash} hash + * @param {Number} index + * @returns {Promise} - Returns {@link Coin}. + */ + + async getUnspentCoin(hash, index) { + const credit = await this.txdb.getCredit(hash, index); + + if (!credit || credit.spent) + return null; + + return credit.coin; + } + /** * Get a transaction from the wallet. * @param {Hash} hash @@ -5553,6 +5864,18 @@ class Wallet extends EventEmitter { } } +/* + * Helpers + */ + +function checkAbort(signal) { + if (!signal) + return; + + if (signal.aborted) + throw new common.AbortError('Operation was aborted'); +} + /* * Expose */ diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index b1a38425b..88b8f6f55 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -680,12 +680,11 @@ class WalletDB extends EventEmitter { /** * Broadcast a transaction via chain server. * @param {TX} tx - * @param {boolean} isSendBlocking * @returns {Promise} */ - async send(tx, isSendBlocking) { - return this.client.send(tx, isSendBlocking); + async send(tx) { + return this.client.send(tx); } /** @@ -2298,7 +2297,7 @@ class WalletDB extends EventEmitter { return 0; } - let total = 0; + const walletTxs = []; try { // We set the state as confirming so that @@ -2308,7 +2307,7 @@ class WalletDB extends EventEmitter { this.confirming = true; for (const tx of txs) { if (await this._addTX(tx, tip)) - total += 1; + walletTxs.push(tx); } // Sync the state to the new tip. @@ -2317,9 +2316,9 @@ class WalletDB extends EventEmitter { this.confirming = false; } - if (total > 0) { + if (walletTxs.length > 0) { this.logger.info('Connected WalletDB block %x (tx=%d).', - tip.hash, total); + tip.hash, walletTxs.length); } if (this.filterUpdated && this.state.height > 0) { @@ -2329,7 +2328,9 @@ class WalletDB extends EventEmitter { return this._addBlock(entry, txs); } - return total; + this.emit('block connect', entry, walletTxs); + + return walletTxs.length; } /** diff --git a/test/wallet-http-test.js b/test/wallet-http-test.js index f8c0df048..a71051ab5 100644 --- a/test/wallet-http-test.js +++ b/test/wallet-http-test.js @@ -24,6 +24,7 @@ const { types } = rules; const secp256k1 = require('bcrypto/lib/secp256k1'); const network = Network.get('regtest'); const assert = require('bsert'); +const {BufferSet} = require('buffer-map'); const common = require('./util/common'); const TIMEOUT = 100; @@ -55,6 +56,7 @@ const wclientTimeout = new WalletClient({ timeout: TIMEOUT }); +const {wdb} = node.require('walletdb'); const wallet = wclient.wallet('primary'); const wallet2 = wclient.wallet('secondary'); const walletTimeout = wclientTimeout.wallet('primary'); @@ -272,7 +274,6 @@ describe('Wallet HTTP', function () { const prePending = await wallet.getPending('default'); - const wdb = node.plugins.walletdb.wdb; const primary = await wdb.get('primary'); delayMethodOnce(primary, 'createTX', TIMEOUT_METHOD); @@ -323,10 +324,9 @@ describe('Wallet HTTP', function () { it('should not broadcast an open on client close', async () => { const prePending = await wallet.getPending('default'); - const wdb = node.plugins.walletdb.wdb; const primary = await wdb.get('primary'); - delayMethodOnce(primary, 'createOpen', TIMEOUT_METHOD); + delayMethodOnce(primary, 'sendOpen', TIMEOUT_METHOD); let err; try { @@ -513,10 +513,9 @@ describe('Wallet HTTP', function () { await mineBlocks(treeInterval + 1, cbAddress); const prePending = await wallet.getPending('default'); - const wdb = node.plugins.walletdb.wdb; const primary = await wdb.get('primary'); - delayMethodOnce(primary, 'createBid', TIMEOUT_METHOD); + delayMethodOnce(primary, 'sendBid', TIMEOUT_METHOD); let err; try { @@ -548,7 +547,7 @@ describe('Wallet HTTP', function () { const address = Address.fromString(cbAddress, network.type); const nameHash = rules.hashName(name); - const primary = node.plugins.walletdb.wdb.primary; + const primary = wdb.primary; const nonce = await primary.generateNonce(nameHash, address, bid); const blind = rules.blind(bid, nonce); @@ -573,7 +572,7 @@ describe('Wallet HTTP', function () { const address = Address.fromString(cbAddress, network.type); const nameHash = rules.hashName(name); - const primary = node.plugins.walletdb.wdb.primary; + const primary = wdb.primary; const nonce = await primary.generateNonce(nameHash, address, bid); const blind = rules.blind(bid, nonce); @@ -808,10 +807,9 @@ describe('Wallet HTTP', function () { const prePending = await wallet.getPending('default'); - const wdb = node.plugins.walletdb.wdb; const primary = await wdb.get('primary'); - delayMethodOnce(primary, 'createReveal', TIMEOUT_METHOD); + delayMethodOnce(primary, 'sendReveal', TIMEOUT_METHOD); let err; try { @@ -1133,10 +1131,9 @@ describe('Wallet HTTP', function () { await mineBlocks(revealPeriod + 1, cbAddress); const prePending = await wallet.getPending('default'); - const wdb = node.plugins.walletdb.wdb; const primary = await wdb.get('primary'); - delayMethodOnce(primary, 'createRedeem', TIMEOUT_METHOD); + delayMethodOnce(primary, 'sendRedeem', TIMEOUT_METHOD); let err; try { @@ -1232,10 +1229,9 @@ describe('Wallet HTTP', function () { await mineBlocks(revealPeriod + 1, cbAddress); const prePending = await wallet.getPending('default'); - const wdb = node.plugins.walletdb.wdb; const primary = await wdb.get('primary'); - delayMethodOnce(primary, 'createUpdate', TIMEOUT_METHOD); + delayMethodOnce(primary, 'sendUpdate', TIMEOUT_METHOD); let err; try { @@ -1344,10 +1340,9 @@ describe('Wallet HTTP', function () { await mineBlocks(treeInterval + 1, cbAddress); const prePending = await wallet.getPending('default'); - const wdb = node.plugins.walletdb.wdb; const primary = await wdb.get('primary'); - delayMethodOnce(primary, 'createRenewal', TIMEOUT_METHOD); + delayMethodOnce(primary, 'sendRenewal', TIMEOUT_METHOD); let err; try { @@ -1434,10 +1429,9 @@ describe('Wallet HTTP', function () { const { receiveAddress } = await wallet.getAccount(accountTwo); const prePending = await wallet.getPending('default'); - const wdb = node.plugins.walletdb.wdb; const primary = await wdb.get('primary'); - delayMethodOnce(primary, 'createTransfer', TIMEOUT_METHOD); + delayMethodOnce(primary, 'sendTransfer', TIMEOUT_METHOD); let err; try { @@ -1547,10 +1541,9 @@ describe('Wallet HTTP', function () { await mineBlocks(transferLockup + 1, cbAddress); const prePending = await wallet.getPending('default'); - const wdb = node.plugins.walletdb.wdb; const primary = await wdb.get('primary'); - delayMethodOnce(primary, 'createFinalize', TIMEOUT_METHOD); + delayMethodOnce(primary, 'sendFinalize', TIMEOUT_METHOD); let err; try { @@ -1663,10 +1656,9 @@ describe('Wallet HTTP', function () { await mineBlocks(transferLockup + 1, cbAddress); const prePending = await wallet.getPending('default'); - const wdb = node.plugins.walletdb.wdb; const primary = await wdb.get('primary'); - delayMethodOnce(primary, 'createCancel', TIMEOUT_METHOD); + delayMethodOnce(primary, 'sendCancel', TIMEOUT_METHOD); let err; try { @@ -1756,10 +1748,9 @@ describe('Wallet HTTP', function () { await mineBlocks(treeInterval + 1, cbAddress); const prePending = await wallet.getPending('default'); - const wdb = node.plugins.walletdb.wdb; const primary = await wdb.get('primary'); - delayMethodOnce(primary, 'createRevoke', TIMEOUT_METHOD); + delayMethodOnce(primary, 'sendRevoke', TIMEOUT_METHOD); let err; try { @@ -1794,6 +1785,477 @@ describe('Wallet HTTP', function () { const receiveAddr = (await staticAddressWallet.createAddress('default')).toString(network); assert.equal(changeAddr, receiveAddr); }); + + describe('HTTP tx races (Integration)', function() { + const WNAME1 = 'racetest-1'; + const WNAME2 = 'racetest-2'; + const rcwallet1 = wclient.wallet(WNAME1); + const rcwallet2 = wclient.wallet(WNAME2); + const FUND_VALUE = 1e6; + const HARD_FEE = 1e4; + const NAMES = []; + const PASSPHRASE1 = 'racetest-passphrase-1'; + const PASSPHRASE2 = 'racetest-passphrase-2'; + + let w1addr; + + const fundNcoins = async (recvWallet, n, value = FUND_VALUE) => { + assert(typeof n === 'number'); + for (let i = 0; i < n; i++) { + const addr = (await recvWallet.createAddress('default')).address; + + await wallet.send({ + hardFee: HARD_FEE, + outputs: [{ + address: addr, + value: value + }] + }); + } + + const blockConnects = common.forEvent(wdb, 'block connect', 1); + await mineBlocks(1, w1addr); + await blockConnects; + }; + + const checkDoubleSpends = (txs) => { + const spentCoins = new BufferSet(); + + for (const tx of txs) { + for (const input of tx.inputs) { + const key = input.prevout.toKey(); + + if (spentCoins.has(key)) + throw new Error(`Input ${input.prevout.format()} is already spent.`); + + spentCoins.add(key); + } + } + }; + + const wMineBlocks = async (n = 1) => { + const forConnect = common.forEvent(wdb, 'block connect', n); + await mineBlocks(n, w1addr); + await forConnect; + }; + + before(async () => { + w1addr = (await wallet.createAddress('default')).address; + const winfo1 = await wclient.createWallet(WNAME1, { + passphrase: PASSPHRASE1 + }); + + const winfo2 = await wclient.createWallet(WNAME2, { + passphrase: PASSPHRASE2 + }); + + assert(winfo1); + assert(winfo2); + + // Fund primary wallet. + await wMineBlocks(5); + }); + + beforeEach(async () => { + await rcwallet1.lock(); + await rcwallet2.lock(); + }); + + it('should fund 3 new transactions', async () => { + const promises = []; + + await fundNcoins(rcwallet1, 3); + + const forMemTX = common.forEvent(node.mempool, 'tx', 3); + + for (let i = 0; i < 3; i++) { + promises.push(rcwallet1.send({ + passphrase: PASSPHRASE1, + subtractFee: true, + hardFee: HARD_FEE, + outputs: [{ + address: w1addr, + value: FUND_VALUE + }] + })); + } + + const results = await Promise.all(promises); + const txs = results.map(details => MTX.fromHex(details.tx)); + checkDoubleSpends(txs); + + await forMemTX; + await wMineBlocks(1); + + const balance = await rcwallet1.getBalance(); + + assert.strictEqual(balance.confirmed, 0); + assert.strictEqual(balance.unconfirmed, 0); + assert.strictEqual(balance.coin, 0); + }); + + it('should open 3 name auctions', async () => { + await fundNcoins(rcwallet1, 3); + + for (let i = 0; i < 3; i++) + NAMES.push(rules.grindName(10, node.chain.tip.height, network)); + + const promises = []; + + const forMemTX = common.forEvent(node.mempool, 'tx', 4); + + for (let i = 0; i < 3; i++) { + promises.push(rcwallet1.createOpen({ + name: NAMES[i], + passphrase: PASSPHRASE1, + hardFee: HARD_FEE + })); + } + + const results = await Promise.all(promises); + const txs = results.map(result => MTX.fromHex(result.hex)); + checkDoubleSpends(txs); + + // spend all money for now. + await rcwallet1.send({ + subtractFee: true, + outputs: [{ + value: (FUND_VALUE - HARD_FEE) * 3, + address: w1addr + }] + }); + + await forMemTX; + await wMineBlocks(1); + + const balance = await rcwallet1.getBalance(); + // 3 opens (0 value) + assert.strictEqual(balance.coin, 3); + assert.strictEqual(balance.confirmed, 0); + }); + + it('should bid 3 times', async () => { + const promises = []; + + // 2 blocks. + await fundNcoins(rcwallet1, 3, HARD_FEE * 2); + await fundNcoins(rcwallet2, 6, HARD_FEE * 2); + + // this is 2 blocks ahead, but does not matter for this test. + await wMineBlocks(network.names.treeInterval + 1); + + const forMemTX = common.forEvent(node.mempool, 'tx', 3 + 3 * 2); + + for (let i = 0; i < 3; i++) { + // make sure we use ALL coins, no NONE left. + // winner. + promises.push(rcwallet1.createBid({ + name: NAMES[i], + bid: HARD_FEE, + lockup: HARD_FEE, + passphrase: PASSPHRASE1, + hardFee: HARD_FEE + })); + + // We want redeemer to not have enough funds + // to redeem the money back and has to use + // extra funds for it. + // + // ALSO We want to have enough redeems to + // do redeemAll and redeem. + for (let j = 0; j < 2; j++) { + promises.push(rcwallet2.createBid({ + name: NAMES[i], + bid: HARD_FEE - 1, + lockup: HARD_FEE - 1, + passphrase: PASSPHRASE2, + hardFee: HARD_FEE + })); + } + } + + const results = await Promise.all(promises); + const txs = results.map(result => MTX.fromHex(result.hex)); + checkDoubleSpends(txs); + + await forMemTX; + + await wMineBlocks(1); + const balance1 = await rcwallet1.getBalance(); + const balance2 = await rcwallet2.getBalance(); + + // 3 opens and 3 bids (nothing extra) + assert.strictEqual(balance1.coin, 6); + assert.strictEqual(balance1.confirmed, HARD_FEE * 3); + + // 3 bids (nothing extra) + assert.strictEqual(balance2.coin, 6); + assert.strictEqual(balance2.confirmed, (HARD_FEE - 1) * 6); + }); + + it('should reveal 3 times and reveal all', async () => { + // Now we don't have fees to reveal. Fund these fees. + fundNcoins(rcwallet1, 3, HARD_FEE); + fundNcoins(rcwallet2, 1, HARD_FEE); + + const promises = []; + + await wMineBlocks(network.names.biddingPeriod); + + const forMemTX = common.forEvent(node.mempool, 'tx', 4); + + for (let i = 0; i < 3; i++) { + promises.push(rcwallet1.createReveal({ + name: NAMES[i], + passphrase: PASSPHRASE1, + hardFee: HARD_FEE + })); + } + + // do reveal all + promises.push(rcwallet2.createReveal({ + passphrase: PASSPHRASE2, + hardFee: HARD_FEE + })); + + const results = await Promise.all(promises); + const txs = results.map(r => MTX.fromHex(r.hex)); + checkDoubleSpends(txs); + await forMemTX; + + await wMineBlocks(1); + + const balance1 = await rcwallet1.getBalance(); + + // 3 opens and 3 reveals + assert.strictEqual(balance1.coin, 6); + assert.strictEqual(balance1.confirmed, HARD_FEE * 3); + + const balance2 = await rcwallet2.getBalance(); + + // 6 reveals + assert.strictEqual(balance2.coin, 6); + assert.strictEqual(balance2.confirmed, (HARD_FEE - 1) * 6); + await wMineBlocks(network.names.revealPeriod); + }); + + it('should register 3 times', async () => { + const promises = []; + + await fundNcoins(rcwallet1, 3, HARD_FEE); + + const forMemTX = common.forEvent(node.mempool, 'tx', 3); + + // We don't have funds to fund anything. + for (let i = 0; i < 3; i++) { + promises.push(rcwallet1.createUpdate({ + name: NAMES[i], + passphrase: PASSPHRASE1, + hardFee: HARD_FEE, + data: { + records: [ + { + type: 'TXT', + txt: ['foobar'] + } + ] + } + })); + } + + const results = await Promise.all(promises); + const txs = results.map(r => MTX.fromHex(r.hex)); + checkDoubleSpends(txs); + await forMemTX; + + await wMineBlocks(1); + }); + + it('should redeem 3 times and redeem all', async () => { + const promises = []; + + await fundNcoins(rcwallet2, 3, HARD_FEE); + + const forMemTX = common.forEvent(node.mempool, 'tx', 3); + + for (let i = 0; i < 2; i++) { + promises.push(rcwallet2.createRedeem({ + name: NAMES[i], + passphrase: PASSPHRASE2, + hardFee: HARD_FEE + })); + } + + promises.push(rcwallet2.createRedeem({ + hardFee: HARD_FEE + })); + + const results = await Promise.all(promises); + const txs = results.map(r => MTX.fromHex(r.hex)); + checkDoubleSpends(txs); + await forMemTX; + }); + + it('should renew 3 names', async () => { + const promises = []; + + await wMineBlocks(network.names.treeInterval); + await fundNcoins(rcwallet1, 3, HARD_FEE); + + const forMemTX = common.forEvent(node.mempool, 'tx', 3); + + for (let i = 0; i < 3; i++) { + promises.push(rcwallet1.createRenewal({ + name: NAMES[i], + passphrase: PASSPHRASE1, + hardFee: HARD_FEE + })); + } + + const results = await Promise.all(promises); + const txs = results.map(r => MTX.fromHex(r.hex)); + checkDoubleSpends(txs); + await forMemTX; + + await wMineBlocks(1); + }); + + it('should transfer 3 names', async () => { + const promises = []; + + await fundNcoins(rcwallet1, 3, HARD_FEE); + + const forMemTX = common.forEvent(node.mempool, 'tx', 3); + + const addrs = [ + (await rcwallet2.createAddress('default')).address, + (await rcwallet2.createAddress('default')).address, + (await rcwallet2.createAddress('default')).address + ]; + + for (let i = 0; i < 3; i++) { + promises.push(rcwallet1.createTransfer({ + name: NAMES[i], + address: addrs[i], + passphrase: PASSPHRASE1, + hardFee: HARD_FEE + })); + } + + const results = await Promise.all(promises); + const txs = results.map(r => MTX.fromHex(r.hex)); + checkDoubleSpends(txs); + await forMemTX; + await wMineBlocks(1); + }); + + it('should cancel 3 names', async () => { + const promises = []; + + await fundNcoins(rcwallet1, 3, HARD_FEE); + + const forMemTX = common.forEvent(node.mempool, 'tx', 3); + + for (let i = 0; i < 3; i++) { + promises.push(rcwallet1.createCancel({ + name: NAMES[i], + passphrase: PASSPHRASE1, + hardFee: HARD_FEE + })); + } + + const results = await Promise.all(promises); + const txs = results.map(r => MTX.fromHex(r.hex)); + checkDoubleSpends(txs); + await forMemTX; + await wMineBlocks(1); + }); + + it('should finalize 3 names', async () => { + await fundNcoins(rcwallet1, 6, HARD_FEE); + + let forMemTX = common.forEvent(node.mempool, 'tx', 3); + + const addrs = [ + (await rcwallet2.createAddress('default')).address, + (await rcwallet2.createAddress('default')).address, + (await rcwallet2.createAddress('default')).address + ]; + + for (let i = 0; i < 3; i++) { + await rcwallet1.createTransfer({ + name: NAMES[i], + address: addrs[i], + passphrase: PASSPHRASE1, + hardFee: HARD_FEE + }); + } + + await forMemTX; + await wMineBlocks(network.names.transferLockup); + + // Now we finalize all. + const promises = []; + + forMemTX = common.forEvent(node.mempool, 'tx', 3); + + for (let i = 0; i < 3; i++) { + promises.push(rcwallet1.createFinalize({ + name: NAMES[i], + passphrase: PASSPHRASE1, + hardFee: HARD_FEE + })); + } + + const results = await Promise.all(promises); + const txs = results.map(r => MTX.fromHex(r.hex)); + checkDoubleSpends(txs); + await forMemTX; + + await wMineBlocks(1); + }); + + it('should revoke 3 names', async () => { + // send them back + await fundNcoins(rcwallet2, 6, HARD_FEE); + + let forMemTX = common.forEvent(node.mempool, 'tx', 3); + + const addrs = [ + (await rcwallet1.createAddress('default')).address, + (await rcwallet1.createAddress('default')).address, + (await rcwallet1.createAddress('default')).address + ]; + + for (let i = 0; i < 3; i++) { + await rcwallet2.createTransfer({ + name: NAMES[i], + address: addrs[i], + passphrase: PASSPHRASE2, + hardFee: HARD_FEE + }); + } + + await forMemTX; + await wMineBlocks(network.names.transferLockup); + + forMemTX = common.forEvent(node.mempool, 'tx', 3); + const promises = []; + + for (let i = 0; i < 3; i++) { + promises.push(rcwallet2.createRevoke({ + name: NAMES[i], + passphrase: PASSPHRASE2, + hardFee: HARD_FEE + })); + } + + const results = await Promise.all(promises); + const txs = results.map(r => MTX.fromHex(r.hex)); + checkDoubleSpends(txs); + await forMemTX; + }); + }); }); async function sleep(time) {