From 8b55f69d404e9f707240dcbccf1d5a9e22888f8a Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Wed, 27 Aug 2014 16:42:07 -0300 Subject: [PATCH 1/8] Add method removeTxWithSpentInputs to wallet --- js/models/core/TxProposals.js | 7 +++++++ js/models/core/Wallet.js | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/js/models/core/TxProposals.js b/js/models/core/TxProposals.js index 11d9ad3b8..2481b3a72 100644 --- a/js/models/core/TxProposals.js +++ b/js/models/core/TxProposals.js @@ -44,6 +44,13 @@ TxProposals.prototype.getNtxids = function() { return Object.keys(this.txps); }; +TxProposals.prototype.deleteOne = function(ntxid) { + var txp = this.txps[ntxid]; + if (!txp) + throw new Error('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 0a9785516..8de654705 100644 --- a/js/models/core/Wallet.js +++ b/js/models/core/Wallet.js @@ -1525,6 +1525,42 @@ Wallet.prototype.getUnspent = function(cb) { }); }; +Wallet.prototype.removeTxWithSpentInputs = function(cb) { + var self = this; + + var txps = this.getTxProposals(); + + 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(); + }); + +}; Wallet.prototype.createTx = function(toAddress, amountSatStr, comment, opts, cb) { var self = this; From 25064829e06d45bcdacf14f105d080a739a8b0ec Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Wed, 27 Aug 2014 16:42:52 -0300 Subject: [PATCH 2/8] Tests --- test/test.Wallet.js | 49 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/test.Wallet.js b/test/test.Wallet.js index 09dc4afe5..cf8a0e26c 100644 --- a/test/test.Wallet.js +++ b/test/test.Wallet.js @@ -802,6 +802,55 @@ describe('Wallet model', function() { }); }); + describe('removeTxWithSpentInputs', function () { + it('should remove 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 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(); + }); + }); + }); + describe('#send', function() { it('should call this.network.send', function() { var w = cachedCreateW2(); From a07ffee0689947f6b4c262662ce8e5a0c5752f4d Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 28 Aug 2014 11:03:47 -0300 Subject: [PATCH 3/8] Precondition check for TxProposals#deleteOne --- js/models/core/TxProposals.js | 4 +--- test/test.TxProposals.js | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/js/models/core/TxProposals.js b/js/models/core/TxProposals.js index 2481b3a72..9e33d6eab 100644 --- a/js/models/core/TxProposals.js +++ b/js/models/core/TxProposals.js @@ -45,9 +45,7 @@ TxProposals.prototype.getNtxids = function() { }; TxProposals.prototype.deleteOne = function(ntxid) { - var txp = this.txps[ntxid]; - if (!txp) - throw new Error('Unknown TXP: ' + ntxid); + preconditions.checkState(this.txps[ntxid], 'Unknown TXP: ' + ntxid); delete this.txps[ntxid]; }; diff --git a/test/test.TxProposals.js b/test/test.TxProposals.js index a7a25e727..f94435f48 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.only('#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({ From ea394de56a68980d885adc9739bf4bd7ceaacf70 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 28 Aug 2014 11:04:45 -0300 Subject: [PATCH 4/8] Tests --- test/test.TxProposals.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.TxProposals.js b/test/test.TxProposals.js index f94435f48..2cc332e69 100644 --- a/test/test.TxProposals.js +++ b/test/test.TxProposals.js @@ -65,7 +65,7 @@ describe('TxProposals', function() { txps.getNtxids().should.deep.equal(['a','b']); }); }); - describe.only('#deleteOne', function() { + describe('#deleteOne', function() { it('should delete specified ntxid', function() { var txps = new TxProposals(); txps.txps = {a:1, b:2}; From 55aa4e8f68a7362323a1a657ba85fe33911db0ed Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Wed, 3 Sep 2014 11:27:58 -0300 Subject: [PATCH 5/8] Added call from UX --- js/models/core/Wallet.js | 7 ++++--- js/services/controllerUtils.js | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/js/models/core/Wallet.js b/js/models/core/Wallet.js index 8de654705..a4d5e3380 100644 --- a/js/models/core/Wallet.js +++ b/js/models/core/Wallet.js @@ -1528,6 +1528,7 @@ Wallet.prototype.getUnspent = function(cb) { Wallet.prototype.removeTxWithSpentInputs = function(cb) { var self = this; + cb = cb || function () {}; var txps = this.getTxProposals(); var inputs = []; @@ -1540,9 +1541,7 @@ Wallet.prototype.removeTxWithSpentInputs = function(cb) { return; this.blockchain.getUnspent(this.getAddressesStr(), function(err, unspentList) { - if (err) { - return cb(err); - } + if (err) return cb(err); unspentList.forEach(function (unspent) { inputs.forEach(function (input) { @@ -1558,6 +1557,8 @@ Wallet.prototype.removeTxWithSpentInputs = function(cb) { self.emit('txProposalsUpdated'); self.store(); + + cb(null); }); }; diff --git a/js/services/controllerUtils.js b/js/services/controllerUtils.js index ba0c9d2d6..d307d2b63 100644 --- a/js/services/controllerUtils.js +++ b/js/services/controllerUtils.js @@ -166,6 +166,8 @@ angular.module('copayApp.services') if (!w) return root.onErrorDigest(); if (!w.isReady()) return; + w.removeTxWithSpentInputs(); + $rootScope.balanceByAddr = {}; $rootScope.updatingBalance = true; From e9e36020fe0b4ac32bd4701e56629febfb0897b6 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Wed, 3 Sep 2014 18:06:58 -0300 Subject: [PATCH 6/8] Add method to FakeWallet --- test/mocks/FakeWallet.js | 3 +++ 1 file changed, 3 insertions(+) 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; }; From ace301d1f82e2e900dacb7f91acddbf3b9b00879 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 4 Sep 2014 17:37:38 -0300 Subject: [PATCH 7/8] Only remove pending txps --- js/models/core/Wallet.js | 11 ++++++++++- test/test.Wallet.js | 30 +++++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/js/models/core/Wallet.js b/js/models/core/Wallet.js index a4d5e3380..91123d20a 100644 --- a/js/models/core/Wallet.js +++ b/js/models/core/Wallet.js @@ -1529,7 +1529,16 @@ Wallet.prototype.removeTxWithSpentInputs = function(cb) { var self = this; cb = cb || function () {}; - var txps = this.getTxProposals(); + + 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) { diff --git a/test/test.Wallet.js b/test/test.Wallet.js index cf8a0e26c..222296a76 100644 --- a/test/test.Wallet.js +++ b/test/test.Wallet.js @@ -802,8 +802,8 @@ describe('Wallet model', function() { }); }); - describe('removeTxWithSpentInputs', function () { - it('should remove TxProposal with spent inputs', function(done) { + describe.only('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); @@ -825,7 +825,7 @@ describe('Wallet model', function() { }); }); - it('should remove TxProposal with at least 1 spent input', function(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; @@ -849,6 +849,30 @@ describe('Wallet model', function() { 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() { From 48a4e8b555f3aebef961d50cf0280e1fae3ebbc7 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 4 Sep 2014 17:38:09 -0300 Subject: [PATCH 8/8] Test --- test/test.Wallet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.Wallet.js b/test/test.Wallet.js index 222296a76..05c9cc658 100644 --- a/test/test.Wallet.js +++ b/test/test.Wallet.js @@ -802,7 +802,7 @@ describe('Wallet model', function() { }); }); - describe.only('removeTxWithSpentInputs', function () { + describe('removeTxWithSpentInputs', function () { it('should remove pending TxProposal with spent inputs', function(done) { var w = cachedCreateW2(); var utxo = createUTXO(w);