diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index 87b5b5cc6..5bb6f5ee1 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -1283,7 +1283,7 @@ class TXDB { * database. Disconnect inputs. * @private * @param {TXRecord} wtx - * @returns {Promise} + * @returns {Promise} */ async erase(wtx, block) { @@ -1420,7 +1420,7 @@ class TXDB { * remove all of its spenders. * @private * @param {TXRecord} wtx - * @returns {Promise} + * @returns {Promise} */ async removeRecursive(wtx) { @@ -1632,7 +1632,7 @@ class TXDB { * @private * @param {Hash} hash * @param {TX} ref - Reference tx, the tx that double-spent. - * @returns {Promise} - Returns Boolean. + * @returns {Promise} - Returns Boolean. */ async removeConflict(wtx) { @@ -1642,6 +1642,9 @@ class TXDB { const details = await this.removeRecursive(wtx); + if (!details) + return null; + this.logger.warning('Removed conflict: %x.', tx.hash()); // Emit the _removed_ transaction. diff --git a/test/wallet-test.js b/test/wallet-test.js index 615704904..e6108a7db 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -361,16 +361,16 @@ describe('Wallet', function() { await wdb.addTX(t1.toTX()); assert.strictEqual((await wallet.getBalance()).unconfirmed, 50000); - let conflict = false; + let conflict = 0; wallet.on('conflict', () => { - conflict = true; + conflict += 1; }); const t2 = new MTX(); t2.addInput(input); t2.addOutput(new Address(), 5000); await wdb.addTX(t2.toTX()); - assert(conflict); + assert.strictEqual(conflict, 1); assert.strictEqual((await wallet.getBalance()).unconfirmed, 0); }); @@ -389,9 +389,9 @@ describe('Wallet', function() { await wdb.addTX(txa.toTX()); assert.strictEqual((await wallet.getBalance()).unconfirmed, 50000); - let conflict = false; + let conflict = 0; wallet.on('conflict', () => { - conflict = true; + conflict += 1; }); const txb = new MTX(); @@ -400,10 +400,96 @@ describe('Wallet', function() { txb.addOutput(address, 49000); await wdb.addTX(txb.toTX()); - assert(conflict); + assert.strictEqual(conflict, 1); assert.strictEqual((await wallet.getBalance()).unconfirmed, 49000); }); + it('should handle double-spend (with block)', async () => { + const wallet = await wdb.create(); + const address = await wallet.receiveAddress(); + + const hash = random.randomBytes(32); + const input0 = Input.fromOutpoint(new Outpoint(hash, 0)); + const input1 = Input.fromOutpoint(new Outpoint(hash, 1)); + + const txa = new MTX(); + txa.addInput(input0); + txa.addInput(input1); + txa.addOutput(address, 50000); + await wdb.addTX(txa.toTX()); + assert.strictEqual((await wallet.getBalance()).unconfirmed, 50000); + + let conflict = 0; + wallet.on('conflict', () => { + conflict += 1; + }); + + const txb = new MTX(); + txb.addInput(input0); + txb.addInput(input1); + txb.addOutput(address, 49000); + + await wdb.addBlock(nextBlock(wdb), [txb.toTX()]); + assert.strictEqual(conflict, 1); + assert.strictEqual((await wallet.getBalance()).unconfirmed, 49000); + assert.strictEqual((await wallet.getBalance()).confirmed, 49000); + }); + + it('should recover from interrupt when removing conflict', async () => { + const wallet = await wdb.create(); + const address = await wallet.receiveAddress(); + + const hash = random.randomBytes(32); + const input0 = Input.fromOutpoint(new Outpoint(hash, 0)); + const input1 = Input.fromOutpoint(new Outpoint(hash, 1)); + + const txa = new MTX(); + txa.addInput(input0); + txa.addInput(input1); + txa.addOutput(address, 50000); + + await wdb.addTX(txa.toTX()); + assert.strictEqual((await wallet.getBalance()).unconfirmed, 50000); + assert.strictEqual((await wallet.getBalance()).confirmed, 0); + + let conflict = 0; + wallet.on('conflict', () => { + conflict += 1; + }); + + const txb = new MTX(); + txb.addInput(input0); + txb.addInput(input1); + txb.addOutput(address, 49000); + + const removeConflict = wallet.txdb.removeConflict; + + wallet.txdb.removeConflict = async () => { + throw new Error('Unexpected interrupt.'); + }; + + const entry = nextBlock(wdb); + + await assert.rejects(async () => { + await wdb.addBlock(entry, [txb.toTX()]); + }, { + name: 'Error', + message: 'Unexpected interrupt.' + }); + + wallet.txdb.removeConflict = removeConflict; + + assert.strictEqual(conflict, 0); + assert.strictEqual((await wallet.getBalance()).unconfirmed, 50000); + assert.strictEqual((await wallet.getBalance()).confirmed, 0); + + await wdb.addBlock(entry, [txb.toTX()]); + + assert.strictEqual(conflict, 1); + assert.strictEqual((await wallet.getBalance()).unconfirmed, 49000); + assert.strictEqual((await wallet.getBalance()).confirmed, 49000); + }); + it('should handle more missed txs', async () => { const alice = await wdb.create(); const bob = await wdb.create(); @@ -2750,6 +2836,46 @@ describe('Wallet', function() { assert.equal(txCount, 1); assert.equal(confirmedCount, 1); }); + + it('should emit conflict event (multiple inputs)', async () => { + const wallet = await wdb.create({id: 'test2'}); + const address = await wallet.receiveAddress(); + + const wclient = new WalletClient({port: ports.wallet}); + await wclient.open(); + + const cwallet = wclient.wallet(wallet.id, wallet.token); + await cwallet.open(); + + try { + const hash = random.randomBytes(32); + const input0 = Input.fromOutpoint(new Outpoint(hash, 0)); + const input1 = Input.fromOutpoint(new Outpoint(hash, 1)); + + const txa = new MTX(); + txa.addInput(input0); + txa.addInput(input1); + txa.addOutput(address, 50000); + await wdb.addTX(txa.toTX()); + assert.strictEqual((await wallet.getBalance()).unconfirmed, 50000); + + let conflict = 0; + cwallet.on('conflict', () => { + conflict += 1; + }); + + const txb = new MTX(); + txb.addInput(input0); + txb.addInput(input1); + txb.addOutput(address, 49000); + await wdb.addTX(txb.toTX()); + + assert.strictEqual(conflict, 1); + assert.strictEqual((await wallet.getBalance()).unconfirmed, 49000); + } finally { + await wclient.close(); + } + }); }); describe('Wallet Name Claims', function() {