diff --git a/js/models/core/TxProposals.js b/js/models/core/TxProposals.js index 11d9ad3b8..9e33d6eab 100644 --- a/js/models/core/TxProposals.js +++ b/js/models/core/TxProposals.js @@ -44,6 +44,11 @@ TxProposals.prototype.getNtxids = function() { return Object.keys(this.txps); }; +TxProposals.prototype.deleteOne = function(ntxid) { + preconditions.checkState(this.txps[ntxid], 'Unknown TXP: ' + ntxid); + delete this.txps[ntxid]; +}; + TxProposals.prototype.deleteAll = function() { this.txps = {}; }; diff --git a/js/models/core/Wallet.js b/js/models/core/Wallet.js index 1856005a7..32fd75c69 100644 --- a/js/models/core/Wallet.js +++ b/js/models/core/Wallet.js @@ -1534,6 +1534,52 @@ Wallet.prototype.getUnspent = function(cb) { }); }; +Wallet.prototype.removeTxWithSpentInputs = function(cb) { + var self = this; + + cb = cb || function () {}; + + var txps = []; + var maxRejectCount = this.maxRejectCount(); + for (var ntxid in this.txProposals.txps) { + var txp = this.txProposals.txps[ntxid]; + txp.ntxid = ntxid; + if (txp.isPending(maxRejectCount)) { + txps.push(txp); + } + } + + var inputs = []; + txps.forEach(function (txp) { + txp.builder.utxos.forEach(function (utxo) { + inputs.push({ ntxid: txp.ntxid, txid: utxo.txid, vout: utxo.vout }); + }); + }); + if (inputs.length === 0) + return; + + this.blockchain.getUnspent(this.getAddressesStr(), function(err, unspentList) { + if (err) return cb(err); + + unspentList.forEach(function (unspent) { + inputs.forEach(function (input) { + input.unspent = input.unspent || (input.txid === unspent.txid && input.vout === unspent.vout); + }); + }); + + inputs.forEach(function (input) { + if (!input.unspent) { + self.txProposals.deleteOne(input.ntxid); + } + }); + + self.emit('txProposalsUpdated'); + self.store(); + + cb(null); + }); + +}; Wallet.prototype.createTx = function(toAddress, amountSatStr, comment, opts, cb) { var self = this; diff --git a/js/services/controllerUtils.js b/js/services/controllerUtils.js index edbb9129b..a5ecf83d2 100644 --- a/js/services/controllerUtils.js +++ b/js/services/controllerUtils.js @@ -165,6 +165,8 @@ angular.module('copayApp.services') if (!w) return root.onErrorDigest(); if (!w.isReady()) return; + w.removeTxWithSpentInputs(); + $rootScope.balanceByAddr = {}; $rootScope.updatingBalance = true; diff --git a/test/mocks/FakeWallet.js b/test/mocks/FakeWallet.js index 2270c4d64..77b2d04a5 100644 --- a/test/mocks/FakeWallet.js +++ b/test/mocks/FakeWallet.js @@ -98,6 +98,9 @@ FakeWallet.prototype.getBalance = function(cb) { return cb(null, this.balance, this.balanceByAddr, this.safeBalance); }; +FakeWallet.prototype.removeTxWithSpentInputs = function (cb) { +}; + FakeWallet.prototype.setEnc = function(enc) { this.enc = enc; }; diff --git a/test/test.TxProposals.js b/test/test.TxProposals.js index a7a25e727..2cc332e69 100644 --- a/test/test.TxProposals.js +++ b/test/test.TxProposals.js @@ -65,6 +65,21 @@ describe('TxProposals', function() { txps.getNtxids().should.deep.equal(['a','b']); }); }); + describe('#deleteOne', function() { + it('should delete specified ntxid', function() { + var txps = new TxProposals(); + txps.txps = {a:1, b:2}; + txps.deleteOne('a'); + txps.getNtxids().should.deep.equal(['b']); + }); + it('should fail on non-existent ntxid', function() { + var txps = new TxProposals(); + txps.txps = {a:1, b:2}; + (function () { + txps.deleteOne('c'); + }).should.throw('Unknown TXP: c'); + }); + }); describe('#toObj', function() { it('should an object', function() { var txps = TxProposals.fromObj({ diff --git a/test/test.Wallet.js b/test/test.Wallet.js index 20dcfd9d9..4d80895a9 100644 --- a/test/test.Wallet.js +++ b/test/test.Wallet.js @@ -809,6 +809,79 @@ describe('Wallet model', function() { }); }); + describe('removeTxWithSpentInputs', function () { + it('should remove pending TxProposal with spent inputs', function(done) { + var w = cachedCreateW2(); + var utxo = createUTXO(w); + chai.expect(w.getTxProposals().length).to.equal(0); + w.blockchain.fixUnspent(utxo); + w.createTx(toAddress, amountSatStr, null, function(ntxid) { + w.sendTxProposal(ntxid); + chai.expect(w.getTxProposals().length).to.equal(1); + + // Inputs are still available, txp still valid + w.removeTxWithSpentInputs(); + chai.expect(w.getTxProposals().length).to.equal(1); + + // Simulate input spent. txp should be removed from txps list + w.blockchain.fixUnspent([]); + w.removeTxWithSpentInputs(); + chai.expect(w.getTxProposals().length).to.equal(0); + + done(); + }); + }); + + it('should remove pending TxProposal with at least 1 spent input', function(done) { + var w = cachedCreateW2(); + var utxo = [createUTXO(w)[0], createUTXO(w)[0]]; + utxo[0].amount = 80000; + utxo[1].amount = 80000; + utxo[1].vout = 1; + chai.expect(w.getTxProposals().length).to.equal(0); + w.blockchain.fixUnspent(utxo); + w.createTx(toAddress, '100000', null, function(ntxid) { + w.sendTxProposal(ntxid); + chai.expect(w.getTxProposals().length).to.equal(1); + + // Inputs are still available, txp still valid + w.removeTxWithSpentInputs(); + chai.expect(w.getTxProposals().length).to.equal(1); + + // Simulate 1 input spent. txp should be removed from txps list + w.blockchain.fixUnspent([utxo[0]]); + w.removeTxWithSpentInputs(); + chai.expect(w.getTxProposals().length).to.equal(0); + + done(); + }); + }); + + it('should not remove complete TxProposal', function(done) { + var w = cachedCreateW2(); + var utxo = createUTXO(w); + chai.expect(w.getTxProposals().length).to.equal(0); + w.blockchain.fixUnspent(utxo); + w.createTx(toAddress, amountSatStr, null, function(ntxid) { + w.sendTxProposal(ntxid); + chai.expect(w.getTxProposals().length).to.equal(1); + + // Inputs are still available, txp still valid + w.removeTxWithSpentInputs(); + chai.expect(w.getTxProposals().length).to.equal(1); + + // Simulate input spent. txp should be removed from txps list + w.blockchain.fixUnspent([]); + var txp = w.txProposals.get(ntxid); + sinon.stub(txp, 'isPending', function () { return false; }) + w.removeTxWithSpentInputs(); + chai.expect(w.getTxProposals().length).to.equal(1); + + done(); + }); + }); + }); + describe('#send', function() { it('should call this.network.send', function() { var w = cachedCreateW2();