From 7d8edf679b09412523f9c24debb25e828457a065 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Thu, 1 Aug 2019 10:47:34 -0700 Subject: [PATCH 1/6] wallet: check account ownership of bid TX when making reveal TX Adds new method hasCoinByAccount(acct, hash, index) to txdb which returns true if the coin (hash, index) is owned by account (number). Adds this check to wallet.makeReveal() to prevent BIDs from other wallet accounts being included as inputs to a new REVEAL TX from a specific account. --- lib/wallet/txdb.js | 14 ++++++++++++++ lib/wallet/wallet.js | 14 ++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index d23372328..a22a1c80b 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -2250,6 +2250,20 @@ class TXDB { }); } + /** + * Test whether an account owns a coin. + * @param {Number} acct + * @param {Hash} hash + * @param {Index} number + * @returns {Promise} - Returns Boolean. + */ + + hasCoinByAccount(acct, hash, index) { + assert(typeof acct === 'number'); + + return this.bucket.has(layout.C.encode(acct, hash, index)); + } + /** * Get hashes of all transactions in the database. * @param {Number} acct diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index c25e0b2a8..331278693 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -1747,12 +1747,18 @@ class Wallet extends EventEmitter { /** * Make a reveal MTX. * @param {String} name + * @param {String|Number} acct * @returns {MTX} */ - async makeReveal(name) { + async makeReveal(name, acct) { assert(typeof name === 'string'); + if (acct != null) { + assert((acct >>> 0) === acct || typeof acct === 'string'); + acct = await this.getAccountIndex(acct); + } + if (!rules.verifyName(name)) throw new Error('Invalid name.'); @@ -1789,6 +1795,9 @@ class Wallet extends EventEmitter { if (!coin) continue; + if (acct != null && !await this.txdb.hasCoinByAccount(acct, hash, index)) + continue; + // Is local? if (coin.height < ns.height) continue; @@ -1828,7 +1837,8 @@ class Wallet extends EventEmitter { */ async _createReveal(name, options) { - const mtx = await this.makeReveal(name); + const acct = options ? options.account : null; + const mtx = await this.makeReveal(name, acct); await this.fill(mtx, options); return this.finalize(mtx, options); } From a4755e6a1d51fe705409e4db4e95137bf1c3df1e Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Tue, 6 Aug 2019 16:33:24 -0700 Subject: [PATCH 2/6] wallet: check account ownership of name before making update TX also: make renew and make redeem --- lib/wallet/wallet.js | 44 ++++- test/wallet-accounts-auction-test.js | 259 +++++++++++++++++++++++++++ 2 files changed, 296 insertions(+), 7 deletions(-) create mode 100644 test/wallet-accounts-auction-test.js diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 331278693..2a8254e7d 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -2010,15 +2010,21 @@ class Wallet extends EventEmitter { /** * Make a redeem MTX. * @param {String} name + * @param {String|Number} acct * @returns {MTX} */ - async makeRedeem(name) { + async makeRedeem(name, acct) { assert(typeof name === 'string'); if (!rules.verifyName(name)) throw new Error('Invalid name.'); + if (acct != null) { + assert((acct >>> 0) === acct || typeof acct === 'string'); + acct = await this.getAccountIndex(acct); + } + const rawName = Buffer.from(name, 'ascii'); const nameHash = rules.hashName(rawName); const ns = await this.getNameState(nameHash); @@ -2046,6 +2052,7 @@ class Wallet extends EventEmitter { if (!own) continue; + // Winner can not redeem if (prevout.equals(ns.owner)) continue; @@ -2054,6 +2061,9 @@ class Wallet extends EventEmitter { if (!coin) continue; + if (acct != null && !await this.txdb.hasCoinByAccount(acct, hash, index)) + continue; + // Is local? if (coin.height < ns.height) continue; @@ -2085,7 +2095,8 @@ class Wallet extends EventEmitter { */ async _createRedeem(name, options) { - const mtx = await this.makeRedeem(name); + const acct = options ? options.account : null; + const mtx = await this.makeRedeem(name, acct); await this.fill(mtx, options); return this.finalize(mtx, options); } @@ -2335,16 +2346,22 @@ class Wallet extends EventEmitter { * Make an update MTX. * @param {String} name * @param {Resource} resource + * @param {String|Number} acct * @returns {MTX} */ - async makeUpdate(name, resource) { + async makeUpdate(name, resource, acct) { assert(typeof name === 'string'); assert(resource instanceof Resource); if (!rules.verifyName(name)) throw new Error('Invalid name.'); + if (acct != null) { + assert((acct >>> 0) === acct || typeof acct === 'string'); + acct = await this.getAccountIndex(acct); + } + const rawName = Buffer.from(name, 'ascii'); const nameHash = rules.hashName(rawName); const ns = await this.getNameState(nameHash); @@ -2360,6 +2377,9 @@ class Wallet extends EventEmitter { if (!coin) 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 (coin.covenant.isReveal() || coin.covenant.isClaim()) return this._makeRegister(name, resource); @@ -2412,7 +2432,8 @@ class Wallet extends EventEmitter { */ async _createUpdate(name, resource, options) { - const mtx = await this.makeUpdate(name, resource); + const acct = options ? options.account : null; + const mtx = await this.makeUpdate(name, resource, acct); await this.fill(mtx, options); return this.finalize(mtx, options); } @@ -2470,16 +2491,21 @@ class Wallet extends EventEmitter { * Make a renewal MTX. * @private * @param {String} name - * @param {Resource?} resource + * @param {String|Number} acct * @returns {MTX} */ - async makeRenewal(name) { + async makeRenewal(name, acct) { assert(typeof name === 'string'); if (!rules.verifyName(name)) throw new Error('Invalid name.'); + if (acct != null) { + assert((acct >>> 0) === acct || typeof acct === 'string'); + acct = await this.getAccountIndex(acct); + } + const rawName = Buffer.from(name, 'ascii'); const nameHash = rules.hashName(rawName); const ns = await this.getNameState(nameHash); @@ -2502,6 +2528,9 @@ class Wallet extends EventEmitter { if (coin.height < ns.height) 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}".`); + const state = ns.state(height, network); if (state !== states.CLOSED) @@ -2541,7 +2570,8 @@ class Wallet extends EventEmitter { */ async _createRenewal(name, options) { - const mtx = await this.makeRenewal(name); + const acct = options ? options.account : null; + const mtx = await this.makeRenewal(name, acct); await this.fill(mtx, options); return this.finalize(mtx, options); } diff --git a/test/wallet-accounts-auction-test.js b/test/wallet-accounts-auction-test.js new file mode 100644 index 000000000..b4edb297f --- /dev/null +++ b/test/wallet-accounts-auction-test.js @@ -0,0 +1,259 @@ +/* eslint-env mocha */ +/* eslint prefer-arrow-callback: "off" */ + +'use strict'; + +const assert = require('bsert'); +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 {WalletClient} = require('hs-client'); + +const network = Network.get('regtest'); + +const node = new FullNode({ + memory: true, + network: 'regtest', + plugins: [require('../lib/wallet/plugin')] +}); + +// Prevent mempool from sending duplicate TXs back to the walletDB and txdb. +// This will prevent a race condition when we need to remove spent (but +// unconfirmed) outputs from the wallet so they can be reused in other tests. +node.mempool.emit = () => {}; + +const wclient = new WalletClient({ + port: network.walletPort +}); + +const {wdb} = node.require('walletdb'); + +const name = rules.grindName(5, 1, network); +let wallet, alice, bob, aliceReceive, bobReceive; + +async function mineBlocks(n, addr) { + addr = addr ? addr : new Address().toString('regtest'); + for (let i = 0; i < n; i++) { + const block = await node.miner.mineBlock(null, addr); + await node.chain.add(block); + } +} + +describe('Multiple accounts participating in same auction', function() { + before(async () => { + await node.open(); + await wclient.open(); + + wallet = await wdb.create(); + + // We'll use an account number for alice and a string for bob + // to ensure that both types work as options. + alice = await wallet.getAccount(0); + bob = await wallet.createAccount({name: 'bob'}); + + aliceReceive = await alice.receiveAddress(); + bobReceive = await bob.receiveAddress(); + }); + + after(async () => { + await wclient.close(); + await node.close(); + }); + + it('should fund both accounts', async () => { + await mineBlocks(2, aliceReceive); + await mineBlocks(2, bobReceive); + + // Wallet rescan is an effective way to ensure that + // wallet and chain are synced before proceeding. + await wdb.rescan(0); + + const aliceBal = await wallet.getBalance(0); + const bobBal = await wallet.getBalance('bob'); + assert(aliceBal.confirmed === 2000 * 2 * 1e6); + assert(bobBal.confirmed === 2000 * 2 * 1e6); + }); + + it('should open an auction and proceed to REVEAL phase', async () => { + await wallet.sendOpen(name, false, {account: 0}); + await mineBlocks(network.names.treeInterval + 2); + let ns = await node.chain.db.getNameStateByName(name); + assert(ns.isBidding(node.chain.height, network)); + + await wdb.rescan(0); + + await wallet.sendBid(name, 100000, 200000, {account: 0}); + await wallet.sendBid(name, 50000, 200000, {account: 'bob'}); + await mineBlocks(network.names.biddingPeriod); + ns = await node.chain.db.getNameStateByName(name); + assert(ns.isReveal(node.chain.height, network)); + + await wdb.rescan(0); + + const walletBids = await wallet.getBidsByName(name); + assert.strictEqual(walletBids.length, 2); + + for (const bid of walletBids) + assert(bid.own); + + assert.strictEqual(node.mempool.map.size, 0); + }); + + describe('REVEAL', function() { + it('should send one REVEAL per account', async () => { + const tx1 = await wallet.sendReveal(name, {account: 0}); + assert(tx1); + + const tx2 = await wallet.sendReveal(name, {account: 'bob'}); + assert(tx2); + + // Reset for next test + await wallet.abandon(tx1.hash()); + await wallet.abandon(tx2.hash()); + + assert.strictEqual(node.mempool.map.size, 2); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + + it('should send one REVEAL for all accounts in one tx', async () => { + const tx = await wallet.sendRevealAll(); + assert(tx); + + // Reset for next test + await wallet.abandon(tx.hash()); + + assert.strictEqual(node.mempool.map.size, 1); + await mineBlocks(1); + assert.strictEqual(node.mempool.map.size, 0); + }); + }); + + describe('UPDATE', function() { + const aliceResource = Resource.Resource.fromJSON({ + records: [ + { + type: 'TXT', + txt: ['ALICE'] + } + ]}); + const bobResource = Resource.Resource.fromJSON({ + records: [ + { + type: 'TXT', + txt: ['BOB'] + } + ]}); + + it('should advance auction to REGISTER phase', async () => { + await mineBlocks(network.names.revealPeriod); + const ns = await node.chain.db.getNameStateByName(name); + assert(ns.isClosed(node.chain.height, network)); + + await wdb.rescan(0); + + // Alice is the winner + const {hash, index} = ns.owner; + assert(await wallet.txdb.hasCoinByAccount(0, hash, index)); + + // ...not Bob (sanity check) + assert(!await wallet.txdb.hasCoinByAccount(1, hash, index)); + }); + + it('should reject REGISTER given wrong account', async () => { + await assert.rejects(async () => { + await wallet.sendUpdate(name, bobResource, {account: 'bob'}); + }, { + name: 'Error', + message: `Account does not own: "${name}".` + }); + }); + + it('should send REGISTER given correct account', async () => { + const tx = await wallet.sendUpdate(name, aliceResource, {account: 0}); + assert(tx); + + await wallet.abandon(tx.hash()); + + assert.strictEqual(node.mempool.map.size, 1); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + + it('should send REGISTER from correct account automatically', async () => { + const tx = await wallet.sendUpdate(name, aliceResource); + assert(tx); + + await mineBlocks(1); + }); + }); + + describe('REDEEM', function() { + it('should reject REDEEM given wrong account', async () => { + await assert.rejects(async () => { + await wallet.sendRedeem(name, {account: 0}); + }, { + name: 'Error', + message: 'No reveals to redeem.' + }); + }); + + it('should send REDEEM from correct account', async () => { + const tx = await wallet.sendRedeem(name, {account: 'bob'}); + assert(tx); + + await wallet.abandon(tx.hash()); + + assert.strictEqual(node.mempool.map.size, 1); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + + it('should send REDEEM from correct account automatically', async () => { + const tx = await wallet.sendRedeem(name); + assert(tx); + + await mineBlocks(1); + }); + }); + + describe('RENEW', function() { + it('should advance chain to allow renewal', async () => { + await mineBlocks(network.names.treeInterval); + await wdb.rescan(0); + }); + + it('should reject RENEW from wrong account', async () => { + await assert.rejects(async () => { + await wallet.sendRenewal(name, {account: 'bob'}); + }, { + name: 'Error', + message: `Account does not own: "${name}".` + }); + }); + + it('should send RENEW from correct account', async () => { + const tx = await wallet.sendRenewal(name, {account: 0}); + assert(tx); + + await wallet.abandon(tx.hash()); + + assert.strictEqual(node.mempool.map.size, 1); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + + it('should send RENEW from correct account automatically', async () => { + const tx = await wallet.sendRenewal(name); + assert(tx); + + await wallet.abandon(tx.hash()); + + assert.strictEqual(node.mempool.map.size, 1); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + }); +}); From e326f946e6892be7ffe47d52f73b53f5b64358b0 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 20 Mar 2020 12:30:40 -0400 Subject: [PATCH 3/6] wallet: check account ownership of name before making transfer TX also: make finalize --- lib/wallet/wallet.js | 28 ++++++++-- test/wallet-accounts-auction-test.js | 77 ++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 2a8254e7d..3f364d6c6 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -2626,16 +2626,22 @@ class Wallet extends EventEmitter { * Make a transfer MTX. * @param {String} name * @param {Address} address + * @param {String|Number} acct * @returns {MTX} */ - async makeTransfer(name, address) { + async makeTransfer(name, address, acct) { assert(typeof name === 'string'); assert(address instanceof Address); if (!rules.verifyName(name)) throw new Error('Invalid name.'); + if (acct != null) { + assert((acct >>> 0) === acct || typeof acct === 'string'); + acct = await this.getAccountIndex(acct); + } + const rawName = Buffer.from(name, 'ascii'); const nameHash = rules.hashName(rawName); const ns = await this.getNameState(nameHash); @@ -2658,6 +2664,9 @@ class Wallet extends EventEmitter { if (coin.height < ns.height) 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}".`); + const state = ns.state(height, network); if (state !== states.CLOSED) @@ -2696,7 +2705,8 @@ class Wallet extends EventEmitter { */ async _createTransfer(name, address, options) { - const mtx = await this.makeTransfer(name, address); + const acct = options ? options.account : null; + const mtx = await this.makeTransfer(name, address, acct); await this.fill(mtx, options); return this.finalize(mtx, options); } @@ -2876,15 +2886,21 @@ class Wallet extends EventEmitter { * Make a transfer-finalizing MTX. * @private * @param {String} name + * @param {String|Number} acct * @returns {MTX} */ - async makeFinalize(name) { + async makeFinalize(name, acct) { assert(typeof name === 'string'); if (!rules.verifyName(name)) throw new Error('Invalid name.'); + if (acct != null) { + assert((acct >>> 0) === acct || typeof acct === 'string'); + acct = await this.getAccountIndex(acct); + } + const rawName = Buffer.from(name, 'ascii'); const nameHash = rules.hashName(rawName); const ns = await this.getNameState(nameHash); @@ -2907,6 +2923,9 @@ class Wallet extends EventEmitter { if (coin.height < ns.height) 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}".`); + const state = ns.state(height, network); if (state !== states.CLOSED) @@ -2955,7 +2974,8 @@ class Wallet extends EventEmitter { */ async _createFinalize(name, options) { - const mtx = await this.makeFinalize(name); + const acct = options ? options.account : null; + const mtx = await this.makeFinalize(name, acct); await this.fill(mtx, options); return this.finalize(mtx, options); } diff --git a/test/wallet-accounts-auction-test.js b/test/wallet-accounts-auction-test.js index b4edb297f..baa61e878 100644 --- a/test/wallet-accounts-auction-test.js +++ b/test/wallet-accounts-auction-test.js @@ -256,4 +256,81 @@ describe('Multiple accounts participating in same auction', function() { assert.strictEqual(node.mempool.map.size, 0); }); }); + + describe('TRANSFER', function() { + // Alice will transfer to Bob + let toAddr; + + before(async () => { + toAddr = await bob.receiveAddress(); + }); + + it('should reject TRANSFER from wrong account', async () => { + await assert.rejects(async () => { + await wallet.sendTransfer(name, toAddr, {account: 'bob'}); + }, { + name: 'Error', + message: `Account does not own: "${name}".` + }); + }); + + it('should send TRANSFER from correct account', async () => { + const tx = await wallet.sendTransfer(name, toAddr, {account: 0}); + assert(tx); + + await wallet.abandon(tx.hash()); + + assert.strictEqual(node.mempool.map.size, 1); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + + it('should send TRANSFER from correct account automatically', async () => { + const tx = await wallet.sendTransfer(name, toAddr); + assert(tx); + + await mineBlocks(1); + }); + }); + + describe('FINALIZE', function() { + it('should advance chain until FINALIZE is allowed', async () => { + await mineBlocks(network.names.transferLockup); + const ns = await node.chain.db.getNameStateByName(name); + assert(ns.isClosed(node.chain.height, network)); + + await wdb.rescan(0); + }); + + it('should reject FINALIZE from wrong account', async () => { + await assert.rejects(async () => { + await wallet.sendFinalize(name, {account: 'bob'}); + }, { + name: 'Error', + message: `Account does not own: "${name}".` + }); + }); + + it('should send FINALIZE from correct account', async () => { + const tx = await wallet.sendFinalize(name, {account: 0}); + assert(tx); + + await wallet.abandon(tx.hash()); + + assert.strictEqual(node.mempool.map.size, 1); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + + it('should send FINALIZE from correct account automatically', async () => { + const tx = await wallet.sendFinalize(name); + assert(tx); + + await wallet.abandon(tx.hash()); + + assert.strictEqual(node.mempool.map.size, 1); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + }); }); From 8204e66e911faddb79c7747525a31dbb4129a174 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 20 Mar 2020 13:18:06 -0400 Subject: [PATCH 4/6] wallet: check account ownership of name before making cancel TX --- lib/wallet/wallet.js | 14 ++++++++++-- test/wallet-accounts-auction-test.js | 33 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 3f364d6c6..b2ed019cb 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -2768,15 +2768,21 @@ class Wallet extends EventEmitter { * Make a transfer-cancelling MTX. * @private * @param {String} name + * @param {String|Number} acct * @returns {MTX} */ - async makeCancel(name) { + async makeCancel(name, acct) { assert(typeof name === 'string'); if (!rules.verifyName(name)) throw new Error('Invalid name.'); + if (acct != null) { + assert((acct >>> 0) === acct || typeof acct === 'string'); + acct = await this.getAccountIndex(acct); + } + const rawName = Buffer.from(name, 'ascii'); const nameHash = rules.hashName(rawName); const ns = await this.getNameState(nameHash); @@ -2799,6 +2805,9 @@ class Wallet extends EventEmitter { if (coin.height < ns.height) 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}".`); + const state = ns.state(height, network); if (state !== states.CLOSED) @@ -2831,7 +2840,8 @@ class Wallet extends EventEmitter { */ async _createCancel(name, options) { - const mtx = await this.makeCancel(name); + const acct = options ? options.account : null; + const mtx = await this.makeCancel(name, acct); await this.fill(mtx, options); return this.finalize(mtx, options); } diff --git a/test/wallet-accounts-auction-test.js b/test/wallet-accounts-auction-test.js index baa61e878..125616f3a 100644 --- a/test/wallet-accounts-auction-test.js +++ b/test/wallet-accounts-auction-test.js @@ -333,4 +333,37 @@ describe('Multiple accounts participating in same auction', function() { assert.strictEqual(node.mempool.map.size, 0); }); }); + + describe('CANCEL', function() { + it('should reject CANCEL from wrong account', async () => { + await assert.rejects(async () => { + await wallet.sendCancel(name, {account: 'bob'}); + }, { + name: 'Error', + message: `Account does not own: "${name}".` + }); + }); + + it('should send CANCEL from correct account', async () => { + const tx = await wallet.sendCancel(name, {account: 0}); + assert(tx); + + await wallet.abandon(tx.hash()); + + assert.strictEqual(node.mempool.map.size, 1); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + + it('should send CANCEL from correct account automatically', async () => { + const tx = await wallet.sendCancel(name); + assert(tx); + + await wallet.abandon(tx.hash()); + + assert.strictEqual(node.mempool.map.size, 1); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + }); }); From 2db37fb85cf186b420d5553405d308c591e9abf3 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 20 Mar 2020 13:22:49 -0400 Subject: [PATCH 5/6] wallet: check account ownership of name before making revoke TX --- lib/wallet/wallet.js | 16 +++++++++++--- test/wallet-accounts-auction-test.js | 33 ++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index b2ed019cb..d60a76bb4 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -3039,15 +3039,21 @@ class Wallet extends EventEmitter { /** * Make a revoke MTX. * @param {String} name + * @param {String|Number} acct * @returns {MTX} */ - async makeRevoke(name) { + async makeRevoke(name, acct) { assert(typeof name === 'string'); if (!rules.verifyName(name)) throw new Error('Invalid name.'); + if (acct != null) { + assert((acct >>> 0) === acct || typeof acct === 'string'); + acct = await this.getAccountIndex(acct); + } + const rawName = Buffer.from(name, 'ascii'); const nameHash = rules.hashName(rawName); const ns = await this.getNameState(nameHash); @@ -3063,6 +3069,9 @@ class Wallet extends EventEmitter { if (!coin) 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}".`); + // Is local? if (coin.height < ns.height) throw new Error(`Wallet does not own: "${name}".`); @@ -3106,7 +3115,8 @@ class Wallet extends EventEmitter { */ async _createRevoke(name, options) { - const mtx = await this.makeRevoke(name); + const acct = options ? options.account : null; + const mtx = await this.makeRevoke(name, acct); await this.fill(mtx, options); return this.finalize(mtx, options); } @@ -3137,7 +3147,7 @@ class Wallet extends EventEmitter { async _sendRevoke(name, options) { const passphrase = options ? options.passphrase : null; - const mtx = await this._createRevoke(name); + const mtx = await this._createRevoke(name, options); return this.sendMTX(mtx, passphrase); } diff --git a/test/wallet-accounts-auction-test.js b/test/wallet-accounts-auction-test.js index 125616f3a..34515512b 100644 --- a/test/wallet-accounts-auction-test.js +++ b/test/wallet-accounts-auction-test.js @@ -366,4 +366,37 @@ describe('Multiple accounts participating in same auction', function() { assert.strictEqual(node.mempool.map.size, 0); }); }); + + describe('REVOKE', function() { + it('should reject REVOKE from wrong account', async () => { + await assert.rejects(async () => { + await wallet.sendRevoke(name, {account: 'bob'}); + }, { + name: 'Error', + message: `Account does not own: "${name}".` + }); + }); + + it('should send REVOKE from correct account', async () => { + const tx = await wallet.sendRevoke(name, {account: 0}); + assert(tx); + + await wallet.abandon(tx.hash()); + + assert.strictEqual(node.mempool.map.size, 1); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + + it('should send REVOKE from correct account automatically', async () => { + const tx = await wallet.sendRevoke(name); + assert(tx); + + await wallet.abandon(tx.hash()); + + assert.strictEqual(node.mempool.map.size, 1); + await node.mempool.reset(); + assert.strictEqual(node.mempool.map.size, 0); + }); + }); }); From 7fc234506a44a14c7bcfc4c92a77a440041cd98c Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 20 Mar 2020 14:14:12 -0400 Subject: [PATCH 6/6] wallet: JSDocs: acct param is optional when making covenants --- lib/wallet/wallet.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index d60a76bb4..cf78b81ac 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -1747,7 +1747,7 @@ class Wallet extends EventEmitter { /** * Make a reveal MTX. * @param {String} name - * @param {String|Number} acct + * @param {(Number|String)?} acct * @returns {MTX} */ @@ -2010,7 +2010,7 @@ class Wallet extends EventEmitter { /** * Make a redeem MTX. * @param {String} name - * @param {String|Number} acct + * @param {(Number|String)?} acct * @returns {MTX} */ @@ -2346,7 +2346,7 @@ class Wallet extends EventEmitter { * Make an update MTX. * @param {String} name * @param {Resource} resource - * @param {String|Number} acct + * @param {(Number|String)?} acct * @returns {MTX} */ @@ -2491,7 +2491,7 @@ class Wallet extends EventEmitter { * Make a renewal MTX. * @private * @param {String} name - * @param {String|Number} acct + * @param {(Number|String)?} acct * @returns {MTX} */ @@ -2626,7 +2626,7 @@ class Wallet extends EventEmitter { * Make a transfer MTX. * @param {String} name * @param {Address} address - * @param {String|Number} acct + * @param {(Number|String)?} acct * @returns {MTX} */ @@ -2768,7 +2768,7 @@ class Wallet extends EventEmitter { * Make a transfer-cancelling MTX. * @private * @param {String} name - * @param {String|Number} acct + * @param {(Number|String)?} acct * @returns {MTX} */ @@ -2896,7 +2896,7 @@ class Wallet extends EventEmitter { * Make a transfer-finalizing MTX. * @private * @param {String} name - * @param {String|Number} acct + * @param {(Number|String)?} acct * @returns {MTX} */ @@ -3039,7 +3039,7 @@ class Wallet extends EventEmitter { /** * Make a revoke MTX. * @param {String} name - * @param {String|Number} acct + * @param {(Number|String)?} acct * @returns {MTX} */