diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index efa0c3e2a..532b9d109 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -2366,7 +2366,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; @@ -2761,7 +2761,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; @@ -3204,14 +3204,19 @@ class Wallet extends EventEmitter { throw new Error(`Auction not found: "${name}".`); const { hash, index } = ns.owner; - const coin = await this.getCoin(hash, index); + const credit = await this.getCredit(hash, index); - if (!coin) + if (!credit) throw new Error(`Wallet did not win the auction: "${name}".`); + if (credit.spent) + throw new Error(`Credit is already pending for: "${name}".`); + if (ns.isExpired(height, network)) throw new Error(`Name has expired: "${name}"!`); + const coin = credit.coin; + // Is local? if (coin.height < ns.height) throw new Error(`Wallet did not win the auction: "${name}".`); @@ -3262,10 +3267,11 @@ class Wallet extends EventEmitter { * @private * @param {String} name * @param {String?} resourceHex + * @param {Number} [acct] * @returns {MTX} */ - async _makeRawRegister(name, resourceHex) { + async _makeRawRegister(name, resourceHex, acct) { assert(typeof name === 'string'); assert(!resourceHex || typeof resourceHex === 'string'); @@ -3282,14 +3288,22 @@ class Wallet extends EventEmitter { throw new Error(`Auction not found: "${name}".`); const { hash, index } = ns.owner; - const coin = await this.getCoin(hash, index); + const credit = await this.getCredit(hash, index); - if (!coin) + if (!credit) throw new Error(`Wallet did not win the auction: "${name}".`); + if (credit.spent) + throw new Error(`Credit is already pending for: "${name}".`); + + if (acct != null && !await this.txdb.hasCoinByAccount(acct, hash, index)) + throw new Error(`Account does not own: "${name}".`); + if (ns.isExpired(height, network)) throw new Error(`Name has expired: "${name}"!`); + const coin = credit.coin; + // Is local? if (coin.height < ns.height) throw new Error(`Wallet did not win the auction: "${name}".`); @@ -3365,14 +3379,19 @@ class Wallet extends EventEmitter { throw new Error(`Auction not found: "${name}".`); const { hash, index } = ns.owner; - const coin = await this.getCoin(hash, index); + const credit = await this.getCredit(hash, index); - if (!coin) + if (!credit) throw new Error(`Wallet does not own: "${name}".`); + if (credit.spent) + throw new Error(`Credit is already pending for: "${name}".`); + if (acct != null && !await this.txdb.hasCoinByAccount(acct, hash, index)) throw new Error(`Account does not own: "${name}".`); + const coin = credit.coin; + if (coin.covenant.isReveal() || coin.covenant.isClaim()) return this._makeRegister(name, resource); @@ -3419,10 +3438,11 @@ class Wallet extends EventEmitter { * Make a raw update MTX. * @param {String} name * @param {String} resourceHex + * @param {Number} [acct] * @returns {MTX} */ - async makeRawUpdate(name, resourceHex) { + async makeRawUpdate(name, resourceHex, acct) { assert(typeof name === 'string'); assert(typeof resourceHex === 'string'); @@ -3439,13 +3459,21 @@ class Wallet extends EventEmitter { throw new Error(`Auction not found: "${name}".`); const { hash, index } = ns.owner; - const coin = await this.getCoin(hash, index); + const credit = await this.getCredit(hash, index); - if (!coin) + if (!credit) throw new Error(`Wallet does not own: "${name}".`); + if (credit.spent) + throw new Error(`Credit is already pending for: "${name}".`); + + if (acct != null && !await this.txdb.hasCoinByAccount(acct, hash, index)) + throw new Error(`Account does not own: "${name}".`); + + const coin = credit.coin; + if (coin.covenant.isReveal() || coin.covenant.isClaim()) - return this._makeRawRegister(name, resourceHex); + return this._makeRawRegister(name, resourceHex, acct); if (ns.isExpired(height, network)) throw new Error(`Name has expired: "${name}"!`); @@ -3512,7 +3540,8 @@ class Wallet extends EventEmitter { */ async _createRawUpdate(name, resourceHex, options) { - const mtx = await this.makeRawUpdate(name, resourceHex); + const acct = options ? options.account : null; + const mtx = await this.makeRawUpdate(name, resourceHex, acct); await this.fill(mtx, options); return this.finalize(mtx, options); } @@ -3651,14 +3680,19 @@ class Wallet extends EventEmitter { throw new Error(`Auction not found: "${name}".`); const { hash, index } = ns.owner; - const coin = await this.getCoin(hash, index); + const credit = await this.getCredit(hash, index); - if (!coin) + if (!credit) throw new Error(`Wallet does not own: "${name}".`); + if (credit.spent) + throw new Error(`Credit is already pending for: "${name}".`); + if (ns.isExpired(height, network)) throw new Error(`Name has expired: "${name}"!`); + const coin = credit.coin; + // Is local? if (coin.height < ns.height) throw new Error(`Wallet does not own: "${name}".`); @@ -3895,14 +3929,19 @@ class Wallet extends EventEmitter { throw new Error(`Auction not found: "${name}".`); const { hash, index } = ns.owner; - const coin = await this.getCoin(hash, index); + const credit = await this.getCredit(hash, index); - if (!coin) + if (!credit) throw new Error(`Wallet does not own: "${name}".`); + if (credit.spent) + throw new Error(`Credit is already pending for: "${name}".`); + if (ns.isExpired(height, network)) throw new Error(`Name has expired: "${name}"!`); + const coin = credit.coin; + // Is local? if (coin.height < ns.height) throw new Error(`Wallet does not own: "${name}".`); @@ -3915,6 +3954,9 @@ class Wallet extends EventEmitter { if (state !== states.CLOSED) throw new Error(`Auction is not yet closed: "${name}".`); + if (coin.covenant.isTransfer()) + throw new Error(`Name is already being transferred: "${name}".`); + if (!coin.covenant.isRegister() && !coin.covenant.isUpdate() && !coin.covenant.isRenew() @@ -4278,14 +4320,19 @@ class Wallet extends EventEmitter { throw new Error(`Auction not found: "${name}".`); const { hash, index } = ns.owner; - const coin = await this.getCoin(hash, index); + const credit = await this.getCredit(hash, index); - if (!coin) + if (!credit) throw new Error(`Wallet does not own: "${name}".`); + if (credit.spent) + throw new Error(`Credit is already pending for: "${name}".`); + if (ns.isExpired(height, network)) throw new Error(`Name has expired: "${name}"!`); + const coin = credit.coin; + // Is local? if (coin.height < ns.height) throw new Error(`Wallet does not own: "${name}".`); @@ -4530,14 +4577,19 @@ class Wallet extends EventEmitter { throw new Error(`Auction not found: "${name}".`); const { hash, index } = ns.owner; - const coin = await this.getCoin(hash, index); + const credit = await this.getCredit(hash, index); - if (!coin) + if (!credit) throw new Error(`Wallet does not own: "${name}".`); if (acct != null && !await this.txdb.hasCoinByAccount(acct, hash, index)) throw new Error(`Account does not own: "${name}".`); + if (credit.spent) + throw new Error(`Credit is already pending for: "${name}".`); + + const coin = credit.coin; + // Is local? if (coin.height < ns.height) throw new Error(`Wallet does not own: "${name}".`); @@ -5433,6 +5485,17 @@ class Wallet extends EventEmitter { return credit.coin; } + /** + * Get credit from the wallet. + * @param {Hash} hash + * @param {Number} index + * @returns {Promise} + */ + + getCredit(hash, index) { + return this.txdb.getCredit(hash, index); + } + /** * Get a transaction from the wallet. * @param {Hash} hash diff --git a/test/wallet-auction-test.js b/test/wallet-auction-test.js index eccfe2d0f..b4e9fecd9 100644 --- a/test/wallet-auction-test.js +++ b/test/wallet-auction-test.js @@ -12,6 +12,7 @@ const Address = require('../lib/primitives/address'); const Output = require('../lib/primitives/output'); const Coin = require('../lib/primitives/coin'); const MTX = require('../lib/primitives/mtx'); +const {Resource} = require('../lib/dns/resource'); const {forEvent} = require('./util/common'); const network = Network.get('regtest'); @@ -19,7 +20,8 @@ const NAME1 = rules.grindName(10, 2, network); const { treeInterval, biddingPeriod, - revealPeriod + revealPeriod, + transferLockup } = network.names; const workers = new WorkerPool({ @@ -46,7 +48,7 @@ const wdb = new WalletDB({ }); describe('Wallet Auction', function() { - let wallet; + let wallet, wallet2; before(async () => { // Open @@ -56,6 +58,7 @@ describe('Wallet Auction', function() { // Set up wallet wallet = await wdb.create(); + wallet2 = await wdb.create(); chain.on('connect', async (entry, block) => { await wdb.addBlock(entry, block.txs); }); @@ -347,6 +350,283 @@ describe('Wallet Auction', function() { }); }); + // This affects linked ones mostly as they are guaranteed + // to reselect the same inputs: REVEAL, REDEEM, REGISTER, UPDATE, + // RENEW, TRANSFER, FINALIZE and REVOKE, + describe('Duplicate Pending Requests', function() { + const name1 = rules.grindName(10, 2, network); + const name2 = rules.grindName(10, 2, network); + const expectedError = name => `Credit is already pending for: "${name}".`; + + const mineBlock = async (txs = []) => { + const job = await cpu.createJob(); + + for (const tx of txs) + job.pushTX(tx); + + job.refresh(); + + const block = await job.mineAsync(); + assert(await chain.add(block)); + }; + + const mineBlocks = async (n) => { + for (let i = 0; i < n; i++) + await mineBlock(); + }; + + it('should get to the reveal', async () => { + const open1 = await wallet.sendOpen(name1); + const open2 = await wallet.sendOpen(name2); + + await mineBlock([open1, open2]); + await mineBlocks(treeInterval); + + const bid11 = await wallet.sendBid(name1, 1000, 2000); + const bid12 = await wallet.sendBid(name1, 1500, 2000); + const bid21 = await wallet.sendBid(name2, 1000, 2000); + const bid22 = await wallet.sendBid(name2, 1500, 2000); + + await mineBlock([bid11, bid12, bid21, bid22]); + await mineBlocks(biddingPeriod); + }); + + it('should REVEAL and fail duplicate reveal', async () => { + const reveal1 = await wallet.sendReveal(name1); + assert(reveal1); + const reveal2 = await wallet.sendReveal(name2); + assert(reveal2); + + let err; + try { + await wallet.sendReveal(name1); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, `No bids to reveal: "${name1}".`); + + await mineBlock([reveal1, reveal2]); + + err = null; + + try { + await wallet.sendReveal(name2); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, `No bids to reveal: "${name2}".`); + + await mineBlocks(revealPeriod); + }); + + it('should REDEEM and fail duplicate redeem', async () => { + const redeem1 = await wallet.sendRedeem(name1); + assert(redeem1); + const redeem2 = await wallet.sendRedeem(name2); + assert(redeem2); + + let err; + try { + await wallet.sendRedeem(name1); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, `No reveals to redeem: "${name1}".`); + + await mineBlock([redeem1, redeem2]); + + err = null; + + try { + await wallet.sendRedeem(name2); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, `No reveals to redeem: "${name2}".`); + }); + + it('should REGISTER and fail duplicate register', async () => { + const register = await wallet.sendUpdate(name1, Resource.fromString('name1.1')); + assert(register); + + let err; + try { + await wallet.sendUpdate(name1, Resource.fromString('name1.2')); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, expectedError(name1)); + + await mineBlock([register]); + }); + + it('should RAWREGISTER and fail duplicate register', async () => { + const resourceHex = 'deadbeef'; + const register = await wallet.sendRawUpdate(name2, resourceHex); + assert(register); + + let err; + try { + await wallet.sendRawUpdate(name2, resourceHex); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, expectedError(name2)); + + await mineBlock([register]); + + // This becomes update. + const update = await wallet.sendRawUpdate(name2, resourceHex); + assert(update); + + await mineBlock([update]); + }); + + it('should UPDATE and fail duplicate UPDATE', async () => { + const update = await wallet.sendUpdate(name1, Resource.fromString('name1.2')); + assert(update); + + let err = null; + try { + await wallet.sendUpdate(name1, Resource.fromString('name1.3')); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, expectedError(name1)); + + await mineBlock([update]); + }); + + it('should RAWUPDATE and fail duplicate UPDATE', async () => { + const resourceHex = 'deadbeef'; + const update = await wallet.sendRawUpdate(name1, resourceHex); + assert(update); + + let err = null; + try { + await wallet.sendRawUpdate(name1, resourceHex); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, expectedError(name1)); + + await mineBlock([update]); + }); + + it('should RENEW and fail duplicate RENEW', async () => { + await mineBlocks(treeInterval + 1); + const renew = await wallet.sendRenewal(name1); + assert(renew); + const renew2 = await wallet.sendRenewal(name2); + assert(renew2); + + let err = null; + try { + await wallet.sendRenewal(name1); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, expectedError(name1)); + + await mineBlock([renew, renew2]); + + err = null; + try { + await wallet.sendRenewal(name2); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, `Must wait to renew: "${name2}".`); + + await mineBlocks(treeInterval + 1); + }); + + it('should TRANSFER and fail duplicate TRANSFER', async () => { + const recv = await wallet2.receiveAddress(); + const transfer = await wallet.sendTransfer(name1, recv); + assert(transfer); + + let err = null; + try { + await wallet.sendTransfer(name1, recv); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, expectedError(name1)); + + await mineBlock([transfer]); + + let err2 = null; + try { + await wallet.sendTransfer(name1, recv); + } catch (e) { + err2 = e; + } + + assert(err2); + assert.strictEqual(err2.message, `Name is already being transferred: "${name1}".`); + + await mineBlocks(transferLockup); + }); + + it('should FINALIZE and fail duplicate FINALIZE', async () => { + const finalize = await wallet.sendFinalize(name1); + assert(finalize); + + let err = null; + try { + await wallet.sendFinalize(name1); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, expectedError(name1)); + + await mineBlock([finalize]); + }); + + it('should REVOKE and fail duplicate REVOKE', async () => { + const revoke = await wallet.sendRevoke(name2); + assert(revoke); + + let err = null; + try { + await wallet.sendRevoke(name2); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.message, expectedError(name2)); + + await mineBlock([revoke]); + await mineBlocks(10); + }); + }); + describe('Wallet ICANNLOCKUP', function() { const BAK_CLAIM_PERIOD = network.names.claimPeriod; const BAK_ALEXA_PERIOD = network.names.alexaLockupPeriod;