diff --git a/js/models/core/TxProposal.js b/js/models/core/TxProposal.js index 38fd61a43..85420245f 100644 --- a/js/models/core/TxProposal.js +++ b/js/models/core/TxProposal.js @@ -17,8 +17,6 @@ var CORE_FIELDS = ['builderObj', 'inputChainPaths', 'version']; function TxProposal(opts) { preconditions.checkArgument(opts); preconditions.checkArgument(opts.inputChainPaths, 'no inputChainPaths'); - preconditions.checkArgument(opts.creator, 'no creator'); - preconditions.checkArgument(opts.createdTs, 'no createdTs'); preconditions.checkArgument(opts.builder, 'no builder'); preconditions.checkArgument(opts.inputChainPaths, 'no inputChainPaths'); @@ -106,8 +104,7 @@ TxProposal.prototype.toObj = function() { }; -TxProposal.trim = function() { - var o = this.toObj(); +TxProposal._trim = function(o) { var ret = {}; CORE_FIELDS.forEach(function(k) { ret[k] = o[k]; @@ -115,7 +112,6 @@ TxProposal.trim = function() { return ret; }; -// fromObj => from a trusted source TxProposal.fromObj = function(o, forceOpts) { preconditions.checkArgument(o.builderObj); delete o['builder']; @@ -137,6 +133,9 @@ TxProposal.fromObj = function(o, forceOpts) { return new TxProposal(o); }; +TxProposal.fromUntrustedObj = function(o, forceOpts) { + return TxProposal.fromObj(TxProposal._trim(o),forceOpts); +}; TxProposal._formatKeys = function(keys) { @@ -208,7 +207,11 @@ TxProposal.prototype.setSeen = function(copayerId) { }; TxProposal.prototype.setRejected = function(copayerId) { - if (!this.rejectedBy[copayerId] && !this.signedBy) + + if (this.signedBy[copayerId]) + throw new Error('Can not reject a signed TX'); + + if (!this.rejectedBy[copayerId]) this.rejectedBy[copayerId] = Date.now(); }; @@ -230,7 +233,7 @@ TxProposal.prototype._allSignatures = function() { TxProposal.prototype.setCopayers = function(senderId, keyMap, readOnlyPeers) { - var newCopayers = {}, + var newCopayer = {}, oldCopayers = {}, newSignedBy = {}, readOnlyPeers = {}, isNew = 1; for(var k in this.signedBy) { @@ -253,35 +256,36 @@ TxProposal.prototype.setCopayers = function(senderId, keyMap, readOnlyPeers) { if (oldCopayers[copayerId]) { //Already have it. Do nothing } else { - newCopayers[copayerId] = Date.now(); + newCopayer[copayerId] = Date.now(); delete oldCopayers[i]; } } - if (!newCopayers[senderId] && !readOnlyPeers[senderId]) + if (!newCopayer[senderId] && !readOnlyPeers[senderId]) throw new Error('TX must have a (new) senders signature') - if (isNew && Object.keys(newCopayers).length>1) - throw new Error('New TX must have only 1 signature'); + if (Object.keys(newCopayer).length>1) + throw new Error('New TX must have only 1 new signature'); // Handler creator / createdTs. // from senderId, and must be signed by senderId if (isNew) { - this.creator = Object.keys(newCopayers)[0]; + this.creator = Object.keys(newCopayer)[0]; this.createdTs = Date.now(); } //Ended. Update this. - for(var i in newCopayers) { - this.signedBy[i] = newCopayers[i]; + for(var i in newCopayer) { + this.signedBy[i] = newCopayer[i]; } // signedBy has preference over rejectedBy for(var i in this.signedBy) { delete this.rejectedBy[i]; } + console.log('[TxProposal.js.287:newCopayer:]',newCopayer); //TODO - return Object.keys(newCopayers); + return Object.keys(newCopayer); }; // merge will not merge any metadata. diff --git a/js/models/core/TxProposals.js b/js/models/core/TxProposals.js index e12c0b9db..7fe68b7b9 100644 --- a/js/models/core/TxProposals.js +++ b/js/models/core/TxProposals.js @@ -56,13 +56,12 @@ TxProposals.prototype.toObj = function() { }; -TxProposals.prototype.merge = function(inObj, senderId, copayersForPubkeys, builderOpts) { - var safeObj = inObj.trimUntrustedObj(); - var incomingTx = TxProposal.fromObj(safeObj, builderOpts); +TxProposals.prototype.merge = function(inObj, builderOpts) { + var incomingTx = TxProposal.fromUntrustedObj(inObj, builderOpts); incomingTx._sync(); var myTxps = this.txps; - var ntxid = inTxp.getId(); + var ntxid = incomingTx.getId(); var ret = { ntxid: ntxid }; @@ -70,37 +69,39 @@ TxProposals.prototype.merge = function(inObj, senderId, copayersForPubkeys, buil if (myTxps[ntxid]) { // Merge an existing txProposal - ret.hasChanged = myTxps[ntxid].merge(inTxp, allowedPubKeys); + ret.hasChanged = myTxps[ntxid].merge(incomingTx, allowedPubKeys); } else { // Create a new one ret.new = 1; - this.txps[ntxid] = inTxp; + this.txps[ntxid] = incomingTx; } ret.txp = this.txps[ntxid]; return ret; }; -TxProposals.prototype.mergeFromObj = function(txProposalObj, allowedPubKeys, opts) { - var inTxp = TxProposal.fromObj(txProposalObj, opts); - var mergeInfo = this.merge(inTxp, allowedPubKeys); - mergeInfo.inTxp = inTxp; - return mergeInfo; -}; - - // Add a LOCALLY CREATED (trusted) tx proposal TxProposals.prototype.add = function(txp) { - txp.sync(); + txp._sync(); var ntxid = txp.getId(); this.txps[ntxid] = txp; return ntxid; }; + +TxProposals.prototype._getTxp = function(ntxid) { + var ret = this.txps[ntxid]; + if (!ret) + throw new Error('Could not find txp: '+ntxid); + + return ret; +}; + TxProposals.prototype.getTxProposal = function(ntxid, copayers) { - var txp = this.txps[ntxid]; + var txp = this._getTxp(ntxid); + var i = JSON.parse(JSON.stringify(txp)); i.builder = txp.builder; i.ntxid = ntxid; @@ -136,6 +137,17 @@ TxProposals.prototype.getTxProposal = function(ntxid, copayers) { return i; }; + +TxProposals.prototype.reject = function(ntxid, copayerId) { + var txp = this._getTxp(ntxid); + txp.setRejected(copayerId); +}; + +TxProposals.prototype.seen = function(ntxid, copayerId) { + var txp = this._getTxp(ntxid); + txp.setSeen(copayerId); +}; + //returns the unspent txid-vout used in PENDING Txs TxProposals.prototype.getUsedUnspent = function(maxRejectCount) { var ret = {}; diff --git a/js/models/core/Wallet.js b/js/models/core/Wallet.js index 8646d5924..e2e57379f 100644 --- a/js/models/core/Wallet.js +++ b/js/models/core/Wallet.js @@ -130,30 +130,27 @@ Wallet.prototype._handlePublicKeyRing = function(senderId, data, isInbound) { }; -Wallet.prototype._processProposalEvents = function(mergeInfo) { - var ev = []; - if (mergeInfo) { - if (mergeInfo.new) { +Wallet.prototype._processProposalEvents = function(senderId, m) { + var ev; + if (m) { + if (m.new) { ev = { type: 'new', cid: senderId } - } else { - for (var i in mergeInfo.newCopayers) { - var copayerId = mergeInfo.newCopayers[i]; - ev.push({ - type: 'signed', - cid: copayerId - }); - } + } else if(m.newCopayer){ + ev={ + type: 'signed', + cid: m.newCopayer + }; } } else { ev = { type: 'corrupt', cId: senderId, - error: e, }; } + if (ev) this.emit('txProposalEvent', ev); }; @@ -189,13 +186,12 @@ Wallet.prototype._handleTxProposal = function(senderId, data) { var m; try { - m = this.txProposals.mergeObj(senderId, data.txProposal, Wallet.builderOpts); - + m = this.txProposals.merge(data.txProposal, Wallet.builderOpts); var keyMap = this._getKeyMap(m.tpx,senderId); - ret.newCopayers = m.txp.setCopayers(senderId, keyMap); + ret.newCopayer = m.txp.setCopayers(senderId, keyMap); } catch (e) { - this.log('Corrupt TX proposal received', senderId, e); //TODO + this.log('Corrupt TX proposal received', senderId, e); } if (m) { @@ -632,20 +628,12 @@ Wallet.prototype.getTxProposals = function() { Wallet.prototype.reject = function(ntxid) { - var myId = this.getMyCopayerId(); - var txp = this.txProposals.txps[ntxid]; - if (!txp || txp.rejectedBy[myId] || txp.signedBy[myId]) { - throw new Error('Invalid transaction to reject: ' + ntxid); - } - - txp.rejectedBy[myId] = Date.now(); + var txp = this.txProposals.reject(ntxid, this.getMyCopayerId()) ; this.sendReject(ntxid); this.store(); this.emit('txProposalsUpdated'); }; - - Wallet.prototype.sign = function(ntxid, cb) { preconditions.checkState(typeof this.getMyCopayerId() !== 'undefined'); var self = this; @@ -834,9 +822,9 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos var priv = this.privateKey; opts = opts || {}; - preconditions.checkArgument(new Address(toAddress).network().name === this.getNetworkName()); - preconditions.checkState(pkr.isComplete()); - preconditions.checkState(priv); + preconditions.checkArgument(new Address(toAddress).network().name === this.getNetworkName(), 'networkname mismatch'); + preconditions.checkState(pkr.isComplete(), 'pubkey ring incomplete'); + preconditions.checkState(priv,'no private key'); if (comment) preconditions.checkArgument(comment.length <= 100); if (!opts.remainderOut) { diff --git a/test/test.Wallet.js b/test/test.Wallet.js index 91e2a2573..b721afb39 100644 --- a/test/test.Wallet.js +++ b/test/test.Wallet.js @@ -668,18 +668,28 @@ describe('Wallet model', function() { }); }); }); - it('should create & reject transaction', function(done) { + it('should fail to reject a signed transaction', function() { var w = cachedCreateW2(); - w.privateKey = null; var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); w.createTx(toAddress, amountSatStr, null, function(ntxid) { - w.on('txProposalsUpdated', function() { - w.getTxProposals()[0].signedByUs.should.equal(false); - w.getTxProposals()[0].rejectedByUs.should.equal(true); - done(); - }); + (function() {w.reject(ntxid);}).should.throw('reject a signed'); + }); + }); + + it('should create & reject transaction', function(done) { + var w = cachedCreateW2(); + var oldK = w.privateKey; + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + w.createTx(toAddress, amountSatStr, null, function(ntxid) { + var s = sinon.stub(w, 'getMyCopayerId').returns('213'); + Object.keys(w.txProposals._getTxp(ntxid).rejectedBy).length.should.equal(0); w.reject(ntxid); + Object.keys(w.txProposals._getTxp(ntxid).rejectedBy).length.should.equal(1); + w.txProposals._getTxp(ntxid).rejectedBy['213'].should.gt(1); + s.restore(); + done(); }); }); it('should create & sign & send a transaction', function(done) { @@ -1030,8 +1040,9 @@ describe('Wallet model', function() { }); }); - describe('validate txProposals', function() { - var testValidate = function(shouldThrow, result, done) { + describe('_handleTxProposal', function() { + var testValidate = function(response, result, done) { + var w = cachedCreateW(); var spy = sinon.spy(); w.on('txProposalEvent', spy); @@ -1039,26 +1050,31 @@ describe('Wallet model', function() { e.type.should.equal(result); done(); }); - var txp = {dummy:1}; // txp.prototype.getId = function() {return 'aa'}; + var txp = {dummy:1}; var txp = { 'txProposal': txp }; - var merge = sinon.stub(w.txProposals, 'mergeFromObj', function() { - if (shouldThrow) throw new Error(); - return {events: [{type:'new'}]}; + var merge = sinon.stub(w.txProposals, 'merge', function() { + if (response==0) throw new Error(); + return {newCopayer: ['juan'], ntxid:1, new:response==1}; }); - w._handleTxProposal('senderID', txp, true); + w._handleTxProposal('senderID', txp); spy.callCount.should.equal(1); merge.restore(); }; - it('should validate for undefined', function(done) { + it('should handle corrupt', function(done) { var result = 'corrupt'; - testValidate(1, result, done); - }); - it('should validate for SIGHASH_ALL', function(done) { - var result = 'new'; testValidate(0, result, done); }); + it('should handle new', function(done) { + var result = 'new'; + testValidate(1, result, done); + }); + it('should handle signed', function(done) { + var result = 'signed'; + testValidate(2, result, done); + }); + }); }); diff --git a/test/test.performance.js b/test/test.performance.js index d2b2bfb75..9bca46ebc 100644 --- a/test/test.performance.js +++ b/test/test.performance.js @@ -5,6 +5,15 @@ var should = chai.should(); var PrivateKey = require('../js/models/core/PrivateKey'); var PublicKeyRing = require('../js/models/core/PublicKeyRing'); +var getNewEpk = function() { + return new PrivateKey({ + networkName: 'livenet', + }) + .deriveBIP45Branch() + .extendedPublicKeyString(); +} + + describe('Performance tests', function() { describe('PrivateKey', function() { it('should optimize BIP32 private key gen time with cache', function() { @@ -43,7 +52,7 @@ describe('Performance tests', function() { requiredCopayers: M }); for (var i = 0; i < N; i++) { - pkr1.addCopayer(); // add new random ext public key + pkr1.addCopayer(getNewEpk()); // add new random ext public key } var generateN = 5; var generated = [];