diff --git a/js/controllers/send.js b/js/controllers/send.js index a46fec5d1..0e61066a1 100644 --- a/js/controllers/send.js +++ b/js/controllers/send.js @@ -25,7 +25,7 @@ angular.module('copayApp.controllers').controller('SendController', $scope.isRateAvailable = false; $scope.rateService = rateService; $scope.showScanner = false; - + $scope.myId = w.getMyCopayerId(); rateService.whenAvailable(function() { $scope.isRateAvailable = true; @@ -369,6 +369,8 @@ angular.module('copayApp.controllers').controller('SendController', notification.success('Success', 'Transaction proposal created'); else if (status == copay.Wallet.TX_SIGNED) notification.success('Success', 'Transaction proposal was signed'); + else if (status == copay.Wallet.TX_SIGNED_AND_BROADCASTED) + notification.success('Success', 'Transaction signed and broadcasted!'); else notification.error('Error', 'Unknown error occured'); }; @@ -378,7 +380,7 @@ angular.module('copayApp.controllers').controller('SendController', $scope.error = $scope.success = null; $scope.loading = true; $rootScope.txAlertCount = 0; - w.broadcastTx(ntxid, function(err, txid, status) { + w.issueTx(ntxid, function(err, txid, status) { $scope.notifyStatus(status); if (cb) return cb(); else $scope.loadTxs(); diff --git a/js/models/BuilderMockV0.js b/js/models/BuilderMockV0.js deleted file mode 100644 index be3926d10..000000000 --- a/js/models/BuilderMockV0.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - - -var bitcore = require('bitcore'); -var Transaction = bitcore.Transaction; - -function BuilderMockV0 (data) { - this.vanilla = data; - this.tx = new Transaction(); - this.tx.parse(new Buffer(data.tx, 'hex')); -}; - -BuilderMockV0.prototype.build = function() { - return this.tx; -}; - - -BuilderMockV0.prototype.getSelectedUnspent = function() { - return []; -}; - -BuilderMockV0.prototype.toObj = function() { - return this.vanilla; -}; - -module.exports = BuilderMockV0; diff --git a/js/models/TxProposal.js b/js/models/TxProposal.js index 32afe115b..036dd5b63 100644 --- a/js/models/TxProposal.js +++ b/js/models/TxProposal.js @@ -1,15 +1,16 @@ 'use strict'; -var bitcore = require('bitcore'); var _ = require('lodash'); +var preconditions = require('preconditions').singleton(); + +var bitcore = require('bitcore'); var util = bitcore.util; var Transaction = bitcore.Transaction; -var BuilderMockV0 = require('./BuilderMockV0');; var TransactionBuilder = bitcore.TransactionBuilder; var Script = bitcore.Script; var Key = bitcore.Key; -var buffertools = bitcore.buffertools; -var preconditions = require('preconditions').instance(); + +var log = require('../log'); var TX_MAX_SIZE_KB = 50; var VERSION = 1; @@ -26,22 +27,24 @@ function TxProposal(opts) { this.version = opts.version; this.builder = opts.builder; this.createdTs = opts.createdTs; - this._inputSigners = []; - // CopayerIds - this.creator = opts.creator; + // Copayer Actions ( copayerId: timeStamp ) this.signedBy = opts.signedBy || {}; this.seenBy = opts.seenBy || {}; this.rejectedBy = opts.rejectedBy || {}; + this.sentTs = opts.sentTs || null; this.sentTxid = opts.sentTxid || null; this.comment = opts.comment || null; this.readonly = opts.readonly || null; this.merchant = opts.merchant || null; - this.paymentAckMemo = null; + this.paymentAckMemo = opts.paymentAckMemo || null; this.paymentProtocolURL = opts.paymentProtocolURL || null; - if (opts.creator) { + this.resetCache(); + + // New Tx Proposal + if (_.isEmpty(this.seenBy) && opts.creator) { var now = Date.now(); var me = {}; me[opts.creator] = now; @@ -55,8 +58,6 @@ function TxProposal(opts) { throw new Error('Could not sign generated tx'); } } - - this._sync(); } TxProposal.prototype._checkPayPro = function() { @@ -94,13 +95,32 @@ TxProposal.prototype.isFullySigned = function() { return this.builder && this.builder.isFullySigned(); }; + +TxProposal.prototype.getMySignatures = function() { + preconditions.checkState(this._mySignatures, 'Still no signatures from us'); + return _.clone(this._mySignatures); +}; + +TxProposal.prototype._setMySignatures = function(signaturesBefore) { + var mySigs = []; + _.each(this.getSignatures(), function(signatures, index) { + var diff = _.difference(signatures, signaturesBefore[index]); + preconditions.checkState(diff.length == 1, 'more that one signature added!'); + mySigs.push(diff[0].toString('hex')); + }) + this._mySignatures = mySigs; + return; +}; + TxProposal.prototype.sign = function(keys, signerId) { var before = this.countSignatures(); + var signaturesBefore = this.getSignatures(); this.builder.sign(keys); var signaturesAdded = this.countSignatures() > before; - if (signaturesAdded){ + if (signaturesAdded) { this.signedBy[signerId] = Date.now(); + this._setMySignatures(signaturesBefore); } return signaturesAdded; }; @@ -111,6 +131,7 @@ TxProposal.prototype._check = function() { throw new Error('Invalid tx proposal'); } + // Should be able to build var tx = this.builder.build(); var txSize = tx.getSize(); @@ -153,52 +174,207 @@ TxProposal.prototype.addMerchantData = function(merchantData) { this._checkPayPro(); }; +TxProposal.prototype.getSignatures = function() { + var ins = this.builder.build().ins; + var sigs = _.map(ins, function(value) { + var script = new bitcore.Script(value.s); + var nchunks = script.chunks.length; + return _.map(script.chunks.slice(1, nchunks - 1), function(buffer) { + return buffer.toString('hex'); + }); + }); + + return sigs; +}; + TxProposal.prototype.rejectCount = function() { return _.size(this.rejectedBy); }; -TxProposal.prototype.isPending = function(maxRejectCount) { - preconditions.checkArgument(typeof maxRejectCount != 'undefined'); - if (this.rejectCount() > maxRejectCount || this.sentTxid) +TxProposal.prototype.isFinallyRejected = function(maxRejectCount) { + return this.rejectCount() > maxRejectCount; +}; + +TxProposal.prototype.isPending = function(maxRejectCount) { + preconditions.checkArgument(_.isNumber(maxRejectCount)); + + if (this.isFinallyRejected(maxRejectCount) || this.sentTxid) return false; return true; }; +TxProposal.prototype._setSigned = function(copayerId) { -TxProposal.prototype._updateSignedBy = function() { - this._inputSigners = []; + // Sign powns rejected + if (this.rejectedBy[copayerId]) { + log.info("WARN: a previously rejected transaction was signed by:", copayerId); + delete this.rejectedBy[copayerId]; + } - var tx = this.builder.build(); - for (var i in tx.ins) { - var scriptSig = new Script(tx.ins[i].s); - var signatureCount = scriptSig.countSignatures(); + this.signedBy[copayerId] = Date.now(); - var info = TxProposal._infoFromRedeemScript(scriptSig); - var txSigHash = tx.hashForSignature(info.script, parseInt(i), Transaction.SIGHASH_ALL); - var signersPubKey = TxProposal._verifySignatures(info.keys, scriptSig, txSigHash); - if (signersPubKey.length !== signatureCount) - throw new Error('Invalid signature'); + return this; +}; - this._inputSigners[i] = signersPubKey; + + +/** + * + * @desc verify signatures of ONE copayer, using an array of signatures for each input + * + * @param {string[]} signatures, of the same copayer, one for each input + * @return {string[]} array for signing pubkeys for each input + */ +TxProposal.prototype._addSignatureAndVerify = function(signatures) { + var self = this; + + var ret = []; + var tx = self.builder.build(); + + var newScriptSigs = []; + _.each(tx.ins, function(input, index) { + var scriptSig = new Script(input.s); + + var info = TxProposal.infoFromRedeemScript(scriptSig); + var txSigHash = tx.hashForSignature(info.script, parseInt(index), Transaction.SIGHASH_ALL); + var keys = TxProposal.formatKeys(info.keys); + var sig = new Buffer(signatures[index], 'hex'); + + var hashType = sig[sig.length - 1]; + if (hashType !== Transaction.SIGHASH_ALL) + throw new Error('BADSIG: Invalid signature: Bad hash type'); + + var sigRaw = new Buffer(sig.slice(0, sig.length - 1)); + var signingPubKeyHex = self._verifyOneSignature(keys, sigRaw, txSigHash); + if (!signingPubKeyHex) + throw new Error('BADSIG: Invalid signatures: invalid for input:' + index); + + // now insert it + var keysHex = _.pluck(keys, 'keyHex'); + var prio = _.indexOf(keysHex, signingPubKeyHex); + preconditions.checkState(prio >= 0); + + var currentKeys = self.getSignersPubKeys()[index]; + + if (_.indexOf(currentKeys, signingPubKeyHex) >= 0) + throw new Error('BADSIG: Already have this signature'); + + var currentPrios = _.map(currentKeys, function(key) { + var prio = _.indexOf(keysHex, key); + preconditions.checkState(prio >= 0); + return prio; + }); + + var insertAt = 0; + while ( !_.isUndefined(currentPrios[insertAt]) && prio > currentPrios[insertAt] ) + insertAt++; + + // Insert it! (1 is OP_0!) + scriptSig.chunks.splice(1 + insertAt, 0, sig); + scriptSig.updateBuffer(); + + + newScriptSigs.push(scriptSig.buffer); + }); + preconditions.checkState(newScriptSigs.length === tx.ins.length); + + // If we reach here, all signatures are OK, let's update the TX. + _.each(tx.ins, function(input, index) { + input.s = newScriptSigs[index]; + + // TODO just to keep TransactionBuilder + self.builder.inputsSigned++; + }); + this.resetCache(); +}; + +TxProposal.prototype.resetCache = function() { + this.cache = { + pubkeysForScript: {}, }; }; -TxProposal.prototype._sync = function() { - this._check(); - this._updateSignedBy(); - return this; -} +/** + * addSignature + * + * @param {string[]} signatures from *ONE* copayer, one signature for each TX input. + * @return {boolean} true = signatures added + */ +TxProposal.prototype.addSignature = function(copayerId, signatures) { + preconditions.checkArgument(_.isArray(signatures)); + + if (this.isFullySigned()) + return false; + + var tx = this.builder.build(); + preconditions.checkArgument(signatures.length === tx.ins.length, 'Wrong number of signatures given'); + + this._addSignatureAndVerify(signatures); + this._setSigned(copayerId); + + return false; +}; + +/** + * + * getSignersPubKey + * @desc get Pubkeys of signers, for each input. this is CPU intensive + * + * @return {string[][]} array of hashes for signing pubkeys for each input + */ +TxProposal.prototype.getSignersPubKeys = function(forceUpdate) { + var self = this; + + + var signersPubKey = []; + + if (!self.cache.signersPubKey || forceUpdate) { + + log.debug('PERFORMANCE WARN: Verifying *all* TX signatures:', self.getId()); + + var tx = self.builder.build(); + _.each(tx.ins, function(input, index) { + + if (!self.cache.pubkeysForScript[input.s]) { + var scriptSig = new Script(input.s); + var signatureCount = scriptSig.countSignatures(); + + var info = TxProposal.infoFromRedeemScript(scriptSig); + var txSigHash = tx.hashForSignature(info.script, parseInt(index), Transaction.SIGHASH_ALL); + var inputSignersPubKey = self.verifySignatures(info.keys, scriptSig, txSigHash); + + // Does scriptSig has strings that are not signatures? + if (inputSignersPubKey.length !== signatureCount) + throw new Error('Invalid signature'); + + self.cache.pubkeysForScript[input.s] = inputSignersPubKey; + } + + signersPubKey[index] = self.cache.pubkeysForScript[input.s]; + }); + self.cache.signersPubKey = signersPubKey; + } else { + log.debug('Using signatures verification cache') + } + + return self.cache.signersPubKey; +}; TxProposal.prototype.getId = function() { preconditions.checkState(this.builder); - return this.builder.build().getNormalizedHash().toString('hex'); + + if (!this.ntxid) { + this.ntxid = this.builder.build().getNormalizedHash().toString('hex'); + } + return this.ntxid; }; TxProposal.prototype.toObj = function() { var o = JSON.parse(JSON.stringify(this)); delete o['builder']; + delete o['cache']; o.builderObj = this.builder.toObj(); return o; }; @@ -216,46 +392,43 @@ TxProposal.fromObj = function(o, forceOpts) { preconditions.checkArgument(o.builderObj); delete o['builder']; forceOpts = forceOpts || {}; - - - if (forceOpts) { - o.builderObj.opts = o.builderObj.opts || {}; - } + o.builderObj.opts = o.builderObj.opts || {}; // force opts is requested. - for (var k in forceOpts) { - o.builderObj.opts[k] = forceOpts[k]; - } - // Handle undef options + _.each(forceOpts, function(value, key) { + o.builderObj.opts[key] = value; + }); + + // Handle undef fee options if (_.isUndefined(forceOpts.fee) && _.isUndefined(forceOpts.feeSat)) { - if (o.builderObj.opts) { - o.builderObj.opts.fee = undefined; - o.builderObj.opts.feeSat = undefined; - } + o.builderObj.opts.fee = undefined; + o.builderObj.opts.feeSat = undefined; } try { o.builder = TransactionBuilder.fromObj(o.builderObj); } catch (e) { - - // backwards (V0) compatatibility fix. - if (!o.version) { - o.builder = new BuilderMockV0(o.builderObj); - o.readonly = 1; - }; + throw new Error(e); + return null; } return new TxProposal(o); }; TxProposal.fromUntrustedObj = function(o, forceOpts) { - return TxProposal.fromObj(TxProposal._trim(o), forceOpts); + var trimmed = TxProposal._trim(o); + var txp = TxProposal.fromObj(trimmed, forceOpts); + if (!txp) + throw new Error('Invalid Transaction'); + + txp._check(); + return txp; }; TxProposal.prototype.toObjTrim = function() { return TxProposal._trim(this.toObj()); }; -TxProposal._formatKeys = function(keys) { +TxProposal.formatKeys = function(keys) { var ret = []; for (var i in keys) { if (!Buffer.isBuffer(keys[i])) @@ -271,31 +444,66 @@ TxProposal._formatKeys = function(keys) { return ret; }; -TxProposal._verifySignatures = function(inKeys, scriptSig, txSigHash) { + +/** + * @desc Verify a single signature, for a given hash, tested against a given list of public keys. + * @param keys + * @param sigRaw + * @param txSigHash + * @return {string?} on valid signature, return the signing public key hex representation + */ +TxProposal.prototype._verifyOneSignature = function(keys, sigRaw, txSigHash) { + preconditions.checkArgument(Buffer.isBuffer(txSigHash)); + preconditions.checkArgument(Buffer.isBuffer(sigRaw)); + preconditions.checkArgument(_.isArray(keys)); + preconditions.checkArgument(keys[0].keyObj); + + var signingKey = _.find(keys, function(key) { + var ret = false; + try { + ret = key.keyObj.verifySignatureSync(txSigHash, sigRaw); + } catch (e) {}; + return ret; + }); + + return signingKey ? signingKey.keyHex : null; +}; + + +/** + * @desc verify transaction signatures + * + * @param inKeys + * @param scriptSig + * @param txSigHash + * @return {string[]} signing pubkeys, in order of apperance + */ +TxProposal.prototype.verifySignatures = function(inKeys, scriptSig, txSigHash) { preconditions.checkArgument(Buffer.isBuffer(txSigHash)); preconditions.checkArgument(inKeys); preconditions.checkState(Buffer.isBuffer(inKeys[0])); + var self = this; if (scriptSig.chunks[0] !== 0) throw new Error('Invalid scriptSig'); - var keys = TxProposal._formatKeys(inKeys); + var keys = TxProposal.formatKeys(inKeys); var ret = []; for (var i = 1; i <= scriptSig.countSignatures(); i++) { var chunk = scriptSig.chunks[i]; + log.debug('\t Verifying CHUNK:', i); var sigRaw = new Buffer(chunk.slice(0, chunk.length - 1)); - for (var j in keys) { - var k = keys[j]; - if (k.keyObj.verifySignatureSync(txSigHash, sigRaw)) { - ret.push(k.keyHex); - break; - } - } + + var signingPubKeyHex = self._verifyOneSignature(keys, sigRaw, txSigHash); + if (!signingPubKeyHex) + throw new Error('Found a signature that is invalid'); + + ret.push(signingPubKeyHex); } return ret; }; -TxProposal._infoFromRedeemScript = function(s) { +TxProposal.infoFromRedeemScript = function(s) { var redeemScript = new Script(s.chunks[s.chunks.length - 1]); if (!redeemScript) throw new Error('Bad scriptSig (no redeemscript)'); @@ -340,17 +548,7 @@ TxProposal.prototype.getSent = function() { return this.sentTs; } -TxProposal.prototype._allSignatures = function() { - var ret = {}; - for (var i in this._inputSigners) - for (var j in this._inputSigners[i]) - ret[this._inputSigners[i][j]] = true; - - return ret; -}; - - -TxProposal.prototype.setCopayers = function(senderId, keyMap, readOnlyPeers) { +TxProposal.prototype.setCopayers = function(pubkeyToCopayerMap) { var newCopayer = {}, oldCopayers = {}, newSignedBy = {}, @@ -375,9 +573,9 @@ TxProposal.prototype.setCopayers = function(senderId, keyMap, readOnlyPeers) { } - var iSig = this._inputSigners[0]; + var iSig = this.getSignersPubKeys(); for (var i in iSig) { - var copayerId = keyMap[iSig[i]]; + var copayerId = pubkeyToCopayerMap[iSig[i]]; if (!copayerId) throw new Error('Found unknown signature') @@ -390,24 +588,19 @@ TxProposal.prototype.setCopayers = function(senderId, keyMap, readOnlyPeers) { } } - // Seems unncessary to check this: - // if (!newCopayer[senderId] && !readOnlyPeers[senderId]) - // throw new Error('TX must have a (new) senders 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 + // from senderId, and must be signed by senderId * DISABLED* + // if (isNew) { this.creator = Object.keys(newCopayer)[0]; this.seenBy[this.creator] = this.createdTs = Date.now(); } - //Ended. Update this. - for (var i in newCopayer) { - this.signedBy[i] = newCopayer[i]; - } + //Ended. Update this + _.extend(this.signedBy, newCopayer); // signedBy has preference over rejectedBy for (var i in this.signedBy) { @@ -417,21 +610,6 @@ TxProposal.prototype.setCopayers = function(senderId, keyMap, readOnlyPeers) { return Object.keys(newCopayer); }; -// merge will not merge any metadata. -TxProposal.prototype.merge = function(incoming) { - preconditions.checkArgument(_.isFunction(incoming._sync)); - incoming._sync(); - - // Note that all inputs must have the same number of signatures, so checking - // one (0) is OK. - var before = this._inputSigners[0].length; - this.builder.merge(incoming.builder); - this._sync(); - - var after = this._inputSigners[0].length; - return after !== before; -}; - //This should be on bitcore / Transaction TxProposal.prototype.countSignatures = function() { var tx = this.builder.build(); diff --git a/js/models/TxProposals.js b/js/models/TxProposals.js index a1b890f7d..477f276a2 100644 --- a/js/models/TxProposals.js +++ b/js/models/TxProposals.js @@ -1,16 +1,16 @@ 'use strict'; -var BuilderMockV0 = require('./BuilderMockV0');; +var preconditions = require('preconditions').singleton(); + var bitcore = require('bitcore'); var util = bitcore.util; var Transaction = bitcore.Transaction; -var BuilderMockV0 = require('./BuilderMockV0');; -var TxProposal = require('./TxProposal');; var Script = bitcore.Script; var Key = bitcore.Key; var buffertools = bitcore.buffertools; -var preconditions = require('preconditions').instance(); + var log = require('../log'); +var TxProposal = require('./TxProposal');; function TxProposals(opts) { opts = opts || {}; @@ -51,9 +51,9 @@ TxProposals.prototype.getNtxidsSince = function(sinceTs) { preconditions.checkArgument(sinceTs); var ret = []; - for(var ii in this.txps){ + for (var ii in this.txps) { var txp = this.txps[ii]; - if (txp.createdTs >= sinceTs) + if (txp.createdTs >= sinceTs) ret.push(ii); } return ret; @@ -96,101 +96,41 @@ TxProposals.prototype.toObj = function() { }; -TxProposals.prototype.merge = function(inObj, builderOpts) { - var incomingTx = TxProposal.fromUntrustedObj(inObj, builderOpts); - - var myTxps = this.txps; - var ntxid = incomingTx.getId(); - var ret = { - ntxid: ntxid - }; - - if (myTxps[ntxid]) { - - // Merge an existing txProposal - ret.hasChanged = myTxps[ntxid].merge(incomingTx); - - - } else { - // Create a new one - ret.new = ret.hasChanged = 1; - this.txps[ntxid] = incomingTx; - } - - ret.txp = this.txps[ntxid]; - return ret; -}; // Add a LOCALLY CREATED (trusted) tx proposal TxProposals.prototype.add = function(txp) { - txp._sync(); var ntxid = txp.getId(); this.txps[ntxid] = txp; return ntxid; }; +TxProposals.prototype.exist = function(ntxid) { + return this.txps[ntxid] ? true : false; +}; + + TxProposals.prototype.get = function(ntxid) { var ret = this.txps[ntxid]; if (!ret) - throw new Error('Unknown TXP: '+ntxid); + throw new Error('Unknown TXP: ' + ntxid); return ret; }; -TxProposals.prototype.getTxProposal = function(ntxid, copayers) { - var txp = this.get(ntxid); - - var i = JSON.parse(JSON.stringify(txp)); - i.builder = txp.builder; - i.ntxid = ntxid; - i.peerActions = {}; - - if (copayers) { - for (var j = 0; j < copayers.length; j++) { - var p = copayers[j]; - i.peerActions[p] = {}; - } - } - - for (var p in txp.seenBy) { - i.peerActions[p] = { - seen: txp.seenBy[p] - }; - } - for (var p in txp.signedBy) { - i.peerActions[p] = i.peerActions[p] || {}; - i.peerActions[p].sign = txp.signedBy[p]; - } - var r = 0; - for (var p in txp.rejectedBy) { - i.peerActions[p] = i.peerActions[p] || {}; - i.peerActions[p].rejected = txp.rejectedBy[p]; - r++; - } - i.rejectCount = r; - - var c = txp.creator; - i.peerActions[c] = i.peerActions[c] || {}; - i.peerActions[c].create = txp.createdTs; - return i; -}; - - //returns the unspent txid-vout used in PENDING Txs TxProposals.prototype.getUsedUnspent = function(maxRejectCount) { var ret = {}; - for (var i in this.txps) { - if (!this.txps[i].isPending(maxRejectCount)) - continue; + var self = this; - var u = this.txps[i].builder.getSelectedUnspent(); - var p = this.getTxProposal(i); + _.each(this.txps, function(txp) { + if (!txp.isPending(maxRejectCount)) + return - for (var j in u) { - ret[u[j].txid + ',' + u[j].vout] = 1; - } - } + _.each(txp.builder.getSelectedUnspent(), function(u) { + ret[u.txid + ',' + u.vout] = 1; + }); + }); return ret; }; diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 66ceb9c80..0a951c934 100644 --- a/js/models/Wallet.js +++ b/js/models/Wallet.js @@ -122,6 +122,7 @@ inherits(Wallet, events.EventEmitter); Wallet.TX_BROADCASTED = 'txBroadcasted'; Wallet.TX_PROPOSAL_SENT = 'txProposalSent'; Wallet.TX_SIGNED = 'txSigned'; +Wallet.TX_SIGNED_AND_BROADCASTED = 'txSignedAndBroadcasted'; Wallet.prototype.emitAndKeepAlive = function(args) { var args = Array.prototype.slice.call(arguments); @@ -131,7 +132,7 @@ Wallet.prototype.emitAndKeepAlive = function(args) { }; /** - * @desc Fixed & Forced TransactionBuilder options, for genereration transactions. + * @desc Fixed & Forced TransactionBuilder options, for genererating transactions. * * @static * @property lockTime null @@ -338,76 +339,37 @@ Wallet.prototype._onPublicKeyRing = function(senderId, data) { /** * @desc - * Demultiplexes calls to update TxProposal updates - * - * @param {string} senderId - the copayer that sent this event - * @param {Object} m - the data received - * @emits txProposalEvent - */ -Wallet.prototype._processProposalEvents = function(senderId, m) { - var ev; - if (m) { - if (m.new) { - ev = { - type: 'new', - cId: senderId - } - } else if (m.newCopayer && m.newCopayer.length) { - ev = { - type: 'signed', - cId: m.newCopayer[0] - }; - } else { - log.error('unknown tx proposal event:', m) - } - } - if (ev) - this.emitAndKeepAlive('txProposalEvent', ev); -}; - - -/* OTDO - events.push({ -type: 'signed', -cId: k, -txId: ntxid -}); -*/ -/** - * @desc - * Retrieves a keymap from from a transaction proposal set extracts a maps from + * Retrieves a keymap from a transaction proposal set extracts a maps from * public key to cosignerId for each signed input of the transaction proposal. * * @param {TxProposals} txp - the transaction proposals - * @return {Object} + * @return {Object} [pubkey] -> copayerId */ -Wallet.prototype._getKeyMap = function(txp) { +Wallet.prototype._getPubkeyToCopayerMap = function(txp) { preconditions.checkArgument(txp); - var inSig0, keyMapAll = {}; + var inSig0, keyMapAll = {}, + self = this; - for (var i in txp._inputSigners) { - var keyMap = this.publicKeyRing.copayersForPubkeys(txp._inputSigners[i], txp.inputChainPaths); + var signersPubKeys = txp.getSignersPubKeys(); + _.each(signersPubKeys, function(inputSignersPubKey, i) { + var keyMap = self.publicKeyRing.copayersForPubkeys(inputSignersPubKey, txp.inputChainPaths); - if (_.size(keyMap) !== _.size(txp._inputSigners[i])) + if (_.size(keyMap) !== _.size(inputSignersPubKey)) throw new Error('Signature does not match known copayers'); - for (var j in keyMap) { - keyMapAll[j] = keyMap[j]; - } + _.extend(keyMapAll, keyMap); // From here -> only to check that all inputs have the same sigs - var inSigArr = []; - _.each(keyMap, function(value, key) { - inSigArr.push(value); - }); + var inSigArr = _.values(keyMap); var inSig = JSON.stringify(inSigArr.sort()); - if (i === '0') { + + if (!inSig0) { inSig0 = inSig; - continue; + } else { + if (inSig !== inSig0) + throw new Error('found inputs with different signatures'); } - if (inSig !== inSig0) - throw new Error('found inputs with different signatures'); - } + }); return keyMapAll; }; @@ -442,11 +404,10 @@ Wallet.prototype._checkIfTxIsSent = function(ntxid, cb) { * and send `seen` messages to peers if aplicable. * @param ntxid */ -Wallet.prototype._setTxProposalSeen = function(ntxid) { - var txp = this.txProposals.get(ntxid); +Wallet.prototype._setTxProposalSeen = function(txp) { if (!txp.getSeen(this.getMyCopayerId())) { txp.setSeen(this.getMyCopayerId()); - this.sendSeen(ntxid); + this.sendSeen(txp.getId()); } }; @@ -458,17 +419,13 @@ Wallet.prototype._setTxProposalSeen = function(ntxid) { * @param ntxid * @param {transactionCallback} cb */ -Wallet.prototype._updateTxProposalSent = function(ntxid, cb) { +Wallet.prototype._updateTxProposalSent = function(txp, cb) { var self = this; - var txp = this.txProposals.get(ntxid); - - this._checkIfTxIsSent(ntxid, function(err, txid) { + this._checkIfTxIsSent(txp.getId(), function(err, txid) { if (err) return cb(err); if (txid) { - if (!txp.getSent()) { - txp.setSent(txid); - } + txp.setSent(txid); self.emitAndKeepAlive('txProposalsUpdated'); } if (cb) @@ -486,13 +443,10 @@ Wallet.prototype._updateTxProposalSent = function(ntxid, cb) { * @param mergeInfo Proposals merge information, as returned by TxProposals.merge * @return {fetchPaymentRequestCallback} */ -Wallet.prototype._processTxProposalPayPro = function(mergeInfo, cb) { +Wallet.prototype._processTxProposalPayPro = function(txp, cb) { var self = this; - var txp = mergeInfo.txp; - var isNew = mergeInfo.new; - var ntxid = mergeInfo.ntxid; - if (!isNew || !txp.paymentProtocolURL) + if (!txp.paymentProtocolURL) return cb(); log.info('Received a Payment Protocol TX Proposal'); @@ -513,35 +467,36 @@ Wallet.prototype._processTxProposalPayPro = function(mergeInfo, cb) { }; /** - * _processIncomingTxProposal - * - * @desc Process an incoming transaction proposal. Runs safety and sanity checks on it. + * @desc Process an NEW incoming transaction proposal. Runs safety and sanity checks on it. * * @param mergeInfo Proposals merge information, as returned by TxProposals.merge * @return {errCallback} */ -Wallet.prototype._processIncomingTxProposal = function(mergeInfo, cb) { - if (!mergeInfo) return cb(); +Wallet.prototype._processIncomingNewTxProposal = function(txp, cb) { var self = this; - self._processTxProposalPayPro(mergeInfo, function(err) { + var ntxid = txp.getId(); + self._processTxProposalPayPro(txp, function(err) { if (err) return cb(err); - self._setTxProposalSeen(mergeInfo.ntxid); + self._setTxProposalSeen(txp); - var tx = mergeInfo.txp.builder.build(); - if (tx.isComplete()) - self._updateTxProposalSent(mergeInfo.ntxid); - else { - self.emitAndKeepAlive('txProposalsUpdated'); - } + var tx = txp.builder.build(); + if (tx.isComplete() && !txp.getSent()) + self._updateTxProposalSent(txp); return cb(); }); }; + +/* only for stubbing */ +Wallet.prototype._txProposalFromUntrustedObj = function(data, opts) { + return TxProposal.fromUntrustedObj(data, opts); +}; + /** * @desc - * Handles a 'TXPROPOSAL' network message + * Handles a NEW 'TXPROPOSAL' network message * * @param {string} senderId - the id of the sender * @param {Object} data - the data received @@ -549,37 +504,56 @@ Wallet.prototype._processIncomingTxProposal = function(mergeInfo, cb) { * @emits txProposalsUpdated */ Wallet.prototype._onTxProposal = function(senderId, data) { + preconditions.checkArgument(data.txProposal); var self = this; - var m; try { - m = self.txProposals.merge(data.txProposal, Wallet.builderOpts); - var keyMap = self._getKeyMap(m.txp); - m.newCopayer = m.txp.setCopayers(senderId, keyMap); + var incomingTx = self._txProposalFromUntrustedObj(data.txProposal, Wallet.builderOpts); + var incomingNtxid = incomingTx.getId(); } catch (e) { - log.error('Corrupt TX proposal received from:', senderId, e.toString()); - if (m && m.ntxid) - self.txProposals.deleteOne(m.ntxid); - m = null; + log.warn(e); + return; } - if (m) { + if (this.txProposals.exist(incomingNtxid)) { + log.warn('Ignoring existing tx Proposal:' + incomingNtxid); + return; + } - self._processIncomingTxProposal(m, function(err) { + self._processIncomingNewTxProposal(incomingTx, function(err) { + if (err) { + log.warn('Corrupt TX proposal received from:', senderId, err.toString()); + return; + } - if (err) { - log.error('Corrupt TX proposal received from:', senderId, err.toString()); - if (m && m.ntxid) - self.txProposals.deleteOne(m.ntxid); - m = null; - } else { - if (m && m.hasChanged) - self.sendTxProposal(m.ntxid); - } - self._processProposalEvents(senderId, m); + var pubkeyToCopayerMap = self._getPubkeyToCopayerMap(incomingTx); + incomingTx.setCopayers(pubkeyToCopayerMap); + + self.txProposals.add(incomingTx); + self.emitAndKeepAlive('txProposalEvent', { + type: 'new', + cId: senderId, }); - } + }); +}; + + +Wallet.prototype._onSignature = function(senderId, data) { + var self = this; + try { + var localTx = this.txProposals.get(data.ntxid); + } catch (e) { + log.info('Ignoring signature for unknown tx Proposal:' + data.ntxid); + return; + }; + localTx.addSignature(senderId, data.signatures); + self.issueTxIfComplete(data.ntxid, function(err, txid) { + self.emitAndKeepAlive('txProposalEvent', { + type: txid ? 'signedAndBroadcasted' : 'signed', + cId: senderId, + }); + }); }; /** @@ -753,6 +727,9 @@ Wallet.prototype._onData = function(senderId, data, ts) { case 'txProposal': this._onTxProposal(senderId, data); break; + case 'signature': + this._onSignature(senderId, data); + break; case 'indexes': this._onIndexes(senderId, data); break; @@ -1278,6 +1255,27 @@ Wallet.prototype.sendReject = function(ntxid) { }); }; + +/** + * @desc Send a signature for a TX Proposal + * @param {string} ntxid + */ +Wallet.prototype.sendSignature = function(ntxid) { + preconditions.checkArgument(ntxid); + + var txp = this.txProposals.get(ntxid); + var signatures = txp.getMySignatures(); + preconditions.checkState(signatures && signatures.length); + + this._sendToPeers(null, { + type: 'signature', + ntxid: ntxid, + signatures: signatures, + walletId: this.id, + }); +}; + + /** * @desc Notify other peers that a wallet has been backed up and it's ready to be used * @param {string[]} [recipients] - the pubkeys of the recipients @@ -1396,45 +1394,28 @@ Wallet.prototype.generateAddress = function(isChange, cb) { }; /** - * @desc Retrieve all the Transaction proposals (see {@link TxProposals}) - * @return {Object[]} each object returned represents a transaction proposal, with two additional - * booleans: signedByUs and rejectedByUs. An optional third boolean signals - * whether the transaction was finally rejected (finallyRejected set to true). - */ -Wallet.prototype.getTxProposals = function() { - var ret = []; - var self = this; - var copayers = self.getRegisteredCopayerIds(); - var myId = self.getMyCopayerId(); - - _.each(self.txProposals.txps, function(txp, ntxid){ - txp.signedByUs = txp.signedBy[myId] ? true : false; - txp.rejectedByUs = txp.rejectedBy[self.getMyCopayerId()] ? true : false; - txp.finallyRejected = self.totalCopayers - txp.rejectCount < self.requiredCopayers; - txp.isPending = !txp.finallyRejected && !txp.sentTxid; - - if (!txp.readonly || txp.finallyRejected || txp.sentTs) { - ret.push(txp); - } - }); - return ret; -}; - -/** + * TODO: get this out of here * @desc get list of actions (see {@link getPendingTxProposals}) */ -Wallet.prototype._getActionList = function(actions) { - if (!actions) return; - var peers = Object.keys(actions).map(function(i) { - return { - cId: i, - actions: actions[i] - } - }); +Wallet.prototype._getActionList = function(txp) { + preconditions.checkArgument(txp); - return peers.sort(function(a, b) { - return !!b.actions.create - !!a.actions.create; + var self = this; + var peers = []; + + _.each(self.getRegisteredCopayerIds(), function(copayerId) { + var actions = { + rejected: txp.rejectedBy[copayerId], + sign: txp.signedBy[copayerId], + seen: txp.seenBy[copayerId], + create: (txp.creator === copayerId) ? txp.createdTs : null, + }; + peers.push({ + cId: copayerId, + actions: actions, + }); }); + return peers; }; /** @@ -1446,15 +1427,22 @@ Wallet.prototype.getPendingTxProposals = function() { var ret = []; ret.txs = []; var pendingForUs = 0; - var txps = this.getTxProposals(); + var txps = this.txProposals.txps; + var maxRejectCount = this.maxRejectCount(); var satToUnit = 1 / this.settings.unitToSatoshi; - _.each(_.where(txps, 'isPending'), function(txp) { + _.each(txps, function(inTxp, ntxid) { + if (!inTxp.isPending(maxRejectCount)) + return; + + var txp = _.clone(inTxp); + txp.ntxid = ntxid; + pendingForUs++; var addresses = {}; var outs = JSON.parse(txp.builder.vanilla.outs); outs.forEach(function(o) { - if (!self.publicKeyRing.addressToPath[o.Straddress]) { + if (!self.addressIsOwn(o.address)) { if (!addresses[o.address]) addresses[o.address] = 0; addresses[o.address] += (o.amountSatStr || Math.round(o.amount * bitcore.util.COIN)); }; @@ -1469,7 +1457,7 @@ Wallet.prototype.getPendingTxProposals = function() { // extra fields txp.fee = txp.builder.feeSat * satToUnit; txp.missingSignatures = txp.builder.build().countInputMissingSignatures(0); - txp.actionList = self._getActionList(txp.peerActions); + txp.actionList = self._getActionList(txp); ret.txs.push(txp); }); @@ -1537,6 +1525,16 @@ Wallet.prototype.sign = function(ntxid) { return true; }; +Wallet.prototype.issueTxIfComplete = function(ntxid, cb) { + var txp = this.txProposals.get(ntxid); + var tx = txp.builder.build(); + if (tx.isComplete()) { + this.issueTx(ntxid, cb); + } else { + return cb(); + } +}; + /** * @@ -1551,13 +1549,13 @@ Wallet.prototype.sign = function(ntxid) { */ Wallet.prototype.signAndSend = function(ntxid, cb) { if (this.sign(ntxid)) { - var txp = this.txProposals.get(ntxid); - if (txp.isFullySigned()) { - return this.broadcastTx(ntxid, cb); - } else { - this.sendTxProposal(ntxid); - return cb(null, ntxid, Wallet.TX_SIGNED); - } + this.sendSignature(ntxid); + this.issueTxIfComplete(ntxid, function(err, txid, status) { + if (!txid) + return cb(null, ntxid, Wallet.TX_SIGNED); + else + return cb(null, ntxid, Wallet.TX_SIGNED_AND_BROADCASTED); + }); } else { return cb(new Error('Could not sign the proposal')); } @@ -1572,13 +1570,12 @@ Wallet.prototype.signAndSend = function(ntxid, cb) { * @param cb * @return {undefined} */ -Wallet.prototype._doBroadcastTx = function(ntxid, cb) { +Wallet.prototype.broadcastToBitcoinNetwork = function(ntxid, cb) { var self = this; var txp = this.txProposals.get(ntxid); - var tx = txp.builder.build(); - if (!tx.isComplete()) - throw new Error('Tx is not complete. Can not broadcast'); + var tx = txp.builder.build(); + preconditions.checkState(tx.isComplete(), 'tx is not complete'); var txHex = tx.serialize().toString('hex'); @@ -1595,7 +1592,7 @@ Wallet.prototype._doBroadcastTx = function(ntxid, cb) { return cb(err, txid); }); } else { - log.info('Wallet:' + self.getName() + ' broadcasted a TX. BITCOIND txid:', txid); + log.info('Wallet:' + self.getName() + ' broadcasted a TX! TXID:', txid); return cb(null, txid); } }); @@ -1610,10 +1607,10 @@ Wallet.prototype._doBroadcastTx = function(ntxid, cb) { * @param {string} txid - the transaction id on the blockchain * @param {signCallback} cb */ -Wallet.prototype.broadcastTx = function(ntxid, cb) { +Wallet.prototype.issueTx = function(ntxid, cb) { var self = this; - self._doBroadcastTx(ntxid, function(err, txid) { + self.broadcastToBitcoinNetwork(ntxid, function(err, txid) { if (err) return cb(err); preconditions.checkState(txid); @@ -1630,8 +1627,6 @@ Wallet.prototype.broadcastTx = function(ntxid, cb) { self.onPayProPaymentAck(ntxid, data); }); } - - self.sendTxProposal(ntxid); self.emitAndKeepAlive('txProposalsUpdated'); return cb(null, txid, Wallet.TX_BROADCASTED); }); @@ -2133,58 +2128,6 @@ Wallet.prototype.getUnspent = function(cb) { }); }; -// TODO. not used. -Wallet.prototype.removeTxWithSpentInputs = function(cb) { - var self = this; - - cb = cb || function() {}; - - if (!_.some(self.getTxProposals(), { - isPending: true - })) - return cb(); - - var proposalsChanged = false; - this.blockchain.getUnspent(this.getAddressesStr(), function(err, unspentList) { - if (err) return cb(err); - - var txps = _.where(self.getTxProposals(), { - isPending: true - }); - if (txps.length === 0) return cb(); - - var inputs = _.flatten(_.map(txps, function(txp) { - return _.map(txp.builder.utxos, function(utxo) { - return { - ntxid: txp.ntxid, - txid: utxo.txid, - vout: utxo.vout, - }; - }); - })); - - _.each(unspentList, function(unspent) { - _.each(inputs, function(input) { - input.unspent = input.unspent || (input.txid === unspent.txid && input.vout === unspent.vout); - }); - }); - - _.each(inputs, function(input) { - if (!input.unspent) { - proposalsChanged = true; - self.txProposals.deleteOne(input.ntxid); - } - }); - - if (proposalsChanged) { - self.emitAndKeepAlive('txProposalsUpdated'); - } - - return cb(); - }); - -}; - /** * spend * @@ -2252,11 +2195,10 @@ Wallet.prototype.spend = function(opts, cb) { log.debug('TXP Added: ', ntxid); - console.log('[Wallet.js.2233]'); //TODO self.sendIndexes(); // Needs only one signature? Broadcast it! if (!self.requiresMultipleSignatures()) { - self.broadcastTx(ntxid, cb); + self.issueTx(ntxid, cb); } else { self.sendTxProposal(ntxid); self.emitAndKeepAlive('txProposalsUpdated'); @@ -2361,6 +2303,7 @@ Wallet.prototype._createTxProposal = function(toAddress, amountSat, comment, utx signWith: keys, }); + console.log('[Wallet.js.2303]'); //TODO return txp; }; @@ -2590,7 +2533,7 @@ Wallet.prototype.getTransactionHistory = function(opts, cb) { opts = opts || {}; var addresses = self.getAddressesInfo(); - var proposals = self.getTxProposals(); + var proposals = self.txProposals.txps; var satToUnit = 1 / self.settings.unitToSatoshi; var indexedProposals = _.indexBy(proposals, 'sentTxid'); @@ -2689,18 +2632,11 @@ Wallet.prototype.getTransactionHistory = function(opts, cb) { if (proposal) { // TODO refactor tx.comment = proposal.comment; - tx.sentTs = proposal.sentTs; tx.merchant = proposal.merchant; tx.peerActions = proposal.peerActions; - tx.finallyRejected = proposal.finallyRejected; tx.merchant = proposal.merchant; tx.paymentAckMemo = proposal.paymentAckMemo; - tx.peerActions = proposal.peerActions; - tx.finallyRejected = proposal.finallyRejected; - - if (tx.peerActions) { - tx.actionList = self._getActionList(tx.peerActions); - } + tx.actionList = self._getActionList(proposal); } }; diff --git a/js/services/controllerUtils.js b/js/services/controllerUtils.js index 6d4106f1d..935bc1ee1 100644 --- a/js/services/controllerUtils.js +++ b/js/services/controllerUtils.js @@ -154,6 +154,10 @@ angular.module('copayApp.services') notification.info('[' + name + '] Transaction Signed', $filter('translate')('A transaction was signed by') + ' ' + user); break; + case 'signedAndBroadcasted': + notification.info('[' + name + '] Transaction Approved', + $filter('translate')('A transaction was signed and broadcasted by') + ' ' + user); + break; case 'rejected': notification.info('[' + name + '] Transaction Rejected', $filter('translate')('A transaction was rejected by') + ' ' + user); diff --git a/test/Identity.js b/test/Identity.js index 804680ca7..0e939f916 100644 --- a/test/Identity.js +++ b/test/Identity.js @@ -129,7 +129,8 @@ describe('Identity model', function() { should.not.exist(err); should.exist(iden); should.exist(iden.wallets); - Identity.prototype.store.calledOnce.should.be.true; + iden.store.calledOnce.should.be.true; + iden.store.restore(); }); }); }); @@ -169,10 +170,16 @@ describe('Identity model', function() { args = createIdentity(); args.params.noWallets = true; var old = Identity.prototype.createWallet; + sinon.stub(Identity.prototype, 'store').yields(null); Identity.create(args.params, function(err, res) { iden = res; }); }); + + afterEach(function() { + iden.store.restore(); + }); + it('should be able to create wallets with given pk', function(done) { var priv = 'tprv8ZgxMBicQKsPdEqHcA7RjJTayxA3gSSqeRTttS1JjVbgmNDZdSk9EHZK5pc52GY5xFmwcakmUeKWUDzGoMLGAhrfr5b3MovMUZUTPqisL2m'; args.storage.setItem = sinon.stub(); @@ -220,7 +227,7 @@ describe('Identity model', function() { args.storage.getItem.onFirstCall().callsArgWith(1, null, '{"wallet": "fakeData"}'); var backup = Wallet.fromUntrustedObj; args.params.noWallets = true; - + sinon.stub(Identity.prototype, 'store').yields(null); sinon.stub().returns(args.wallet); var opts = { @@ -232,6 +239,7 @@ describe('Identity model', function() { should.not.exist(err); opts.importWallet.calledOnce.should.equal(true); should.exist(wallet); + iden.store.restore(); done(); }); }); @@ -246,6 +254,7 @@ describe('Identity model', function() { var backup = Wallet.fromUntrustedObj; args.params.noWallets = true; sinon.stub().returns(args.wallet); + sinon.stub(Identity.prototype, 'store').yields(null); var fakeCrypto = { kdf: sinon.stub().returns('passphrase'), @@ -263,6 +272,7 @@ describe('Identity model', function() { should.not.exist(err); fakeCrypto.decrypt.getCall(0).args[0].should.equal('password'); fakeCrypto.decrypt.getCall(0).args[1].should.equal(123); + iden.store.restore(); done(); }); }); @@ -311,12 +321,14 @@ describe('Identity model', function() { args = createIdentity(); args.params.Async = net = sinon.stub(); + sinon.stub(Identity.prototype, 'store').yields(null); net.cleanUp = sinon.spy(); net.on = sinon.stub(); net.start = sinon.spy(); var old = Identity.prototype.createWallet; Identity.create(args.params, function(err, res) { iden = res; + iden.store.restore(); }); }); diff --git a/test/TxProposal.js b/test/TxProposal.js index c1b69a5ac..af81b4d79 100644 --- a/test/TxProposal.js +++ b/test/TxProposal.js @@ -8,19 +8,89 @@ var Script = bitcore.Script; var TransactionBuilder = bitcore.TransactionBuilder; var util = bitcore.util; var networks = bitcore.networks; -var FakeBuilder = requireMock('FakeBuilder'); -var TxProposal = copay.TxProposal; var Buffer = bitcore.Buffer; -var someKeys = ["03b39d61dc9a504b13ae480049c140dcffa23a6cc9c09d12d6d1f332fee5e18ca5", "022929f515c5cf967474322468c3bd945bb6f281225b2c884b465680ef3052c07e"]; +var TxProposal = copay.TxProposal; + describe('TxProposal', function() { - function dummyProposal() { + + // These 2 signed the scripts below + var PUBKEYS = ['03197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d', '03a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e3']; + + + // Signatures of the scripts below + var SIG0 = '304502200708a381dde585ef7fdfaeaeb5da9b451d3e22b01eac8a5e3d03b959e24a7478022100c90e76e423523a54a9e9c43858337ebcef1a539a7fc685c2698dd8648fcf1b9101'; + var SIG1 = '3044022030a77c9613d6ee010717c1abc494668d877e3fa0ae4c520f65cc3b308754c98c02205219d387bcb291bd44805b9468439e4168b02a6a180cdbcc24d84d71d696c1ae01'; + + /* decoded redeemscript + * + "asm" : "3 03197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d 0380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127 0392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed03 03a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e3 03e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e4 5 OP_CHECKMULTISIG", + */ + + // 1,2 signatures 3-5! + var SCRIPTSIG = _.map([ + '0048304502207d8e832bd576c93300e53ab6cbd68641961bec60690c358fd42d8e42b7d7d687022100a1daa89923efdb4c9b615d065058d9e1644f67000694a7d0806759afa7bef19b014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae', + '0048304502200708a381dde585ef7fdfaeaeb5da9b451d3e22b01eac8a5e3d03b959e24a7478022100c90e76e423523a54a9e9c43858337ebcef1a539a7fc685c2698dd8648fcf1b9101473044022030a77c9613d6ee010717c1abc494668d877e3fa0ae4c520f65cc3b308754c98c02205219d387bcb291bd44805b9468439e4168b02a6a180cdbcc24d84d71d696c1ae014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae' + ], function(hex) { + return new Buffer(hex, 'hex'); + }); + + + var someKeys = ["03b39d61dc9a504b13ae480049c140dcffa23a6cc9c09d12d6d1f332fee5e18ca5", "022929f515c5cf967474322468c3bd945bb6f281225b2c884b465680ef3052c07e"]; + + + function dummyBuilder(opts) { + opts = opts || {}; + + var index = opts.nsig ? opts.nsig - 1 : 1; + var script = SCRIPTSIG[index]; + + var aIn = { + s: script + }; + + var tx = {}; + tx.ins = opts.noins ? [] : [opts.nosigs ? {} : aIn]; + + tx.serialize = sinon.stub().returns(new Buffer('1234', 'hex')); + tx.getSize = sinon.stub().returns(1); + tx.getHashType = sinon.stub().returns(opts.hashtype || 1); + tx.getNormalizedHash = sinon.stub().returns('123456'); + tx.hashForSignature = sinon.stub().returns( + new Buffer('31103626e162f1cbfab6b95b08c9f6e78aae128523261cb37f8dfd4783cb09a7', 'hex')); + + + var builder = {}; + + builder.opts = opts.opts || {}; + builder.build = sinon.stub().returns(tx) + builder.toObj = sinon.stub().returns({ + iAmBuilderObj: true, + version: 1, + opts: builder.opts, + }); + builder.isFullySigned = sinon.stub().returns(false); + + builder.vanilla = { + scriptSig: [SCRIPTSIG[1]], + outs: JSON.stringify([{ + address: '2NDJbzwzsmRgD2o5HHXPhuq5g6tkKTjYkd6', + amountSatStr: '123', + }]), + }; + + return builder; + }; + + function dummyProposal(opts) { + opts = opts || {}; + return new TxProposal({ creator: 'creator', createdTs: 1, - builder: new FakeBuilder(), + builder: dummyBuilder(opts), inputChainPaths: ['m/1'], }) }; @@ -47,7 +117,7 @@ describe('TxProposal', function() { var txp = new TxProposal({ creator: 1, createdTs: 1, - builder: new FakeBuilder(), + builder: dummyBuilder(), inputChainPaths: 'm/1', }); should.exist(txp); @@ -59,8 +129,7 @@ describe('TxProposal', function() { }); describe('#getId', function() { it('should return id', function() { - var b = new FakeBuilder(); - var spy = sinon.spy(b.tx, 'getNormalizedHash'); + var b = new dummyBuilder(); var txp = new TxProposal({ creator: 1, createdTs: 1, @@ -68,12 +137,12 @@ describe('TxProposal', function() { inputChainPaths: 'm/1', }); txp.getId().should.equal('123456');; - sinon.assert.callCount(spy, 1); + sinon.assert.callCount(b.build().getNormalizedHash, 1); }); }); describe('#toObj', function() { it('should return an object and remove builder', function() { - var b = new FakeBuilder(); + var b = new dummyBuilder(); var txp = new TxProposal({ creator: 1, createdTs: 1, @@ -87,7 +156,7 @@ describe('TxProposal', function() { should.exist(o.builderObj); }); it('toObjTrim', function() { - var b = new FakeBuilder(); + var b = new dummyBuilder(); var txp = new TxProposal({ creator: 1, createdTs: 1, @@ -104,54 +173,66 @@ describe('TxProposal', function() { }); }); - describe('#fromObj', function() { + describe('#fromUntrustedObj', function() { it('should fail to create from wrong object', function() { - var b = new FakeBuilder(); + var b = new dummyBuilder(); (function() { - var txp = TxProposal.fromObj({ + var txp = TxProposal.fromUntrustedObj({ creator: 1, createdTs: 1, builderObj: b.toObj(), inputChainPaths: ['m/1'], }); - }).should.throw('Invalid'); + }).should.throw('tx is not defined'); }); it('sets force opts', function() { - var b = new FakeBuilder(); - b.opts = { - juan: 1, - pepe: 1, - fee: 1000 - }; - var txp; + + // Create an incomming TX proposal, with certain options... + var b = new dummyBuilder({ + opts: { + juan: 1, + pepe: 1, + fee: 1000 + } + }); + var o = { creator: 1, createdTs: 1, builderObj: b.toObj(), inputChainPaths: ['m/1'], }; - (function() { - txp = TxProposal.fromObj(o, { - pepe: 100 - }); - }).should.throw('Invalid tx proposal: no ins'); + sinon.stub(TxProposal.prototype, '_check').returns(true); + + //Force other options + var txp = TxProposal.fromUntrustedObj(o, { + pepe: 100 + }); + o.builderObj.opts.should.deep.equal({ juan: 1, pepe: 100, feeSat: undefined, fee: undefined }); + + TxProposal.prototype._check.restore(); }); }); + describe('#fromObj', function() { + + }); + + describe('#setSent', function() { it('should set txid and timestamp', function() { var now = Date.now(); var txp = new TxProposal({ creator: 1, createdTs: 1, - builder: new FakeBuilder(), + builder: new dummyBuilder(), inputChainPaths: ['m/1'], }); txp.setSent('3a42'); @@ -166,7 +247,7 @@ describe('TxProposal', function() { var txp = new TxProposal({ creator: 1, createdTs: 1, - builder: new FakeBuilder(), + builder: new dummyBuilder(), inputChainPaths: ['m/1'], }); @@ -180,7 +261,8 @@ describe('TxProposal', function() { }); describe('Signature verification', function() { - var validScriptSig = new bitcore.Script(FakeBuilder.VALID_SCRIPTSIG_BUF); + var validScriptSig1Sig = new bitcore.Script(SCRIPTSIG[0]); + var validScriptSig = new bitcore.Script(SCRIPTSIG[1]); var pubkeys = [ '03197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d', @@ -194,33 +276,43 @@ describe('TxProposal', function() { var keyBuf = someKeys.map(function(hex) { return new Buffer(hex, 'hex'); }); - it('#_formatKeys', function() { + it('#formatKeys', function() { (function() { - TxProposal._formatKeys(someKeys); + TxProposal.formatKeys(someKeys); }).should.throw('buffers'); - var res = TxProposal._formatKeys(keyBuf); + var res = TxProposal.formatKeys(keyBuf); }); it('#_verifyScriptSig arg checks', function() { + var txp = dummyProposal(); (function() { - TxProposal._verifySignatures( + txp.verifySignatures( keyBuf, new bitcore.Script(new Buffer('112233', 'hex')), new Buffer('1a', 'hex')); }).should.throw('script'); }); it('#_verifyScriptSig, no signatures', function() { - var ret = TxProposal._verifySignatures(keyBuf, validScriptSig, new Buffer(32)); - ret.length.should.equal(0); + var txp = dummyProposal(); + (function() { + txp.verifySignatures(keyBuf, validScriptSig, new Buffer(32)); + }).should.throw('invalid'); + }); + it('#_verifyScriptSig, one signature', function() { + // Data taken from bitcore's TransactionBuilder test + var txp = dummyProposal(); + var tx = dummyProposal().builder.build(); + var ret = txp.verifySignatures(pubkeys, validScriptSig1Sig, tx.hashForSignature()); + ret.should.deep.equal(['03197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d']); }); it('#_verifyScriptSig, two signatures', function() { // Data taken from bitcore's TransactionBuilder test var txp = dummyProposal(); var tx = dummyProposal().builder.build(); - var ret = TxProposal._verifySignatures(pubkeys, validScriptSig, tx.hashForSignature()); + var ret = txp.verifySignatures(pubkeys, validScriptSig, tx.hashForSignature()); ret.should.deep.equal(['03197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d', '03a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e3']); }); - it('#_infoFromRedeemScript', function() { - var info = TxProposal._infoFromRedeemScript(validScriptSig); + it('#infoFromRedeemScript', function() { + var info = TxProposal.infoFromRedeemScript(validScriptSig); var keys = info.keys; keys.length.should.equal(5); for (var i in keys) { @@ -228,54 +320,107 @@ describe('TxProposal', function() { } Buffer.isBuffer(info.script.getBuffer()).should.equal(true); }); - it('#_updateSignedBy', function() { + it('#getSignersPubKeys', function() { var txp = dummyProposal(); - txp._inputSigners.should.deep.equal([ - ['03197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d', '03a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e3'] - ]); + var pubkeys = txp.getSignersPubKeys(); + pubkeys.should.deep.equal([PUBKEYS]); + }); + + + + describe('#getSignatures', function() { + it('should get signatures', function() { + var txp = dummyProposal(); + var sigs = txp.getSignatures(); + sigs.length.should.equal(1); + sigs[0].length.should.equal(2); + sigs[0][0].should.equal(SIG0); + sigs[0][1].should.equal(SIG1); + }); + }); + describe('#addSignature', function() { + it('should add signatures maintaing pubkeys order', function() { + var txp = dummyProposal({ + nsig:1 + }); + txp.getSignersPubKeys()[0].length.should.equal(1); + + txp.addSignature('pepe', [SIG1]); + txp.getSignersPubKeys()[0].length.should.equal(2); + + var keys = txp.getSignersPubKeys()[0]; + var keysSorted = _.clone(keys).sort(); + keysSorted.should.deep.equal(keys); + + }); + + + + it('should fail with invalid signatures', function() { + var txp = dummyProposal({ + nsig:1 + }); + txp.getSignersPubKeys()[0].length.should.equal(1); + + (function(){ + txp.addSignature('pepe', ['002030a77c9613d6ee010717c1abc494668d877e3fa0ae4c520f65cc3b308754c98c02205219d387bcb291bd44805b9468439e4168b02a6a180cdbcc24d84d71d696c1ae01']); + }).should.throw('BADSIG'); + }); + + it('should fail adding the same signature twice', function() { + var txp = dummyProposal({ + nsig:1 + }); + txp.getSignersPubKeys()[0].length.should.equal(1); + + txp.addSignature('pepe', [SIG1]); + (function(){ + txp.addSignature('pepe', [SIG1]); + }).should.throw('BADSIG'); + }); }); describe('#_check', function() { - var txp = dummyProposal(); - var backup = txp.builder.tx.ins; - it('OK', function() { - txp._check(); + dummyProposal({})._check(); }); it('FAIL ins', function() { - txp.builder.tx.ins = []; (function() { - txp._check(); + dummyProposal({ + noins: true, + })._check(); }).should.throw('no ins'); - txp.builder.tx.ins = backup; }); it('FAIL signhash SINGLE', function() { - sinon.stub(txp.builder.tx, 'getHashType').returns(Transaction.SIGHASH_SINGLE); + var txp = dummyProposal({ + hashtype: Transaction.SIGHASH_SINGLE + }); (function() { txp._check(); }).should.throw('signatures'); - txp.builder.tx.getHashType.restore(); }); it('FAIL signhash NONE', function() { - sinon.stub(txp.builder.tx, 'getHashType').returns(Transaction.SIGHASH_NONE); + var txp = dummyProposal({ + hashtype: Transaction.SIGHASH_NONE, + }); (function() { txp._check(); }).should.throw('signatures'); - txp.builder.tx.getHashType.restore(); }); it('FAIL signhash ANYONECANPAY', function() { - sinon.stub(txp.builder.tx, 'getHashType').returns(Transaction.SIGHASH_ANYONECANPAY); + var txp = dummyProposal({ + hashtype: Transaction.SIGHASH_ANYONECANPAY, + }); (function() { txp._check(); }).should.throw('signatures'); - txp.builder.tx.getHashType.restore(); }); it('FAIL no signatures', function() { - var backup = txp.builder.tx.ins[0].s; - txp.builder.tx.ins[0].s = undefined; + var txp = dummyProposal({ + nosigs: true, + }); (function() { txp._check(); }).should.throw('no signatures'); - txp.builder.tx.ins[0].s = backup; }); }); @@ -366,21 +511,19 @@ describe('TxProposal', function() { }); - describe('#merge', function() { - var txp = dummyProposal(); - var backup = txp.builder.tx.ins; + describe.skip('#merge', function() { it('with self', function() { + var txp = dummyProposal(); var hasChanged = txp.merge(txp); hasChanged.should.equal(false); }); it('with less signatures', function() { + var txp = dummyProposal(); + var txp1Sig = dummyProposal({ + nsig:1 + }); var backup = txp.builder.vanilla.scriptSig[0]; - txp.builder.merge = function() { - // Only one signatures. - this.vanilla.scriptSig = ['0048304502207d8e832bd576c93300e53ab6cbd68641961bec60690c358fd42d8e42b7d7d687022100a1daa89923efdb4c9b615d065058d9e1644f67000694a7d0806759afa7bef19b014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae']; - this.tx.ins[0].s = new Buffer(this.vanilla.scriptSig[0], 'hex'); - }; var hasChanged = txp.merge(txp); hasChanged.should.equal(true); @@ -388,7 +531,6 @@ describe('TxProposal', function() { txp.builder.tx.ins[0].s = new Buffer(backup, 'hex'); }); - it('with more signatures', function() { txp.builder.merge = function() { // 3 signatures. @@ -407,7 +549,7 @@ describe('TxProposal', function() { }; delete txp['creator']; (function() { - txp.setCopayers('juan', { + txp.setCopayers({ pk1: 'pepe' }) }).should.throw('no creator'); @@ -422,7 +564,7 @@ describe('TxProposal', function() { ['pkX'] ]; (function() { - txp.setCopayers('juan', { + txp.setCopayers({ pk1: 'pepe' }) }).should.throw('creator'); @@ -439,7 +581,7 @@ describe('TxProposal', function() { ['pk0', 'pkX'] ]; (function() { - txp.setCopayers('juan', { + txp.setCopayers({ pk1: 'pepe' }) }).should.throw('unknown sig'); @@ -456,7 +598,7 @@ describe('TxProposal', function() { 'creator': Date.now() }; (function() { - txp.setCopayers('juan', { + txp.setCopayers({ pk0: 'creator', pk1: 'pepe', pk2: 'john' @@ -468,13 +610,12 @@ describe('TxProposal', function() { it("should set signedBy (trivial case)", function() { var txp = dummyProposal(); var ts = Date.now(); - txp._inputSigners = [ - ['pk1', 'pk0'] - ]; + + sinon.stub(txp, 'getSignersPubKeys').returns(['pk1', 'pk0']); txp.signedBy = { 'creator': Date.now() }; - txp.setCopayers('pepe', { + txp.setCopayers({ pk0: 'creator', pk1: 'pepe', pk2: 'john' @@ -486,13 +627,11 @@ describe('TxProposal', function() { it("should assign creator", function() { var txp = dummyProposal(); var ts = Date.now(); - txp._inputSigners = [ - ['pk0'] - ]; + sinon.stub(txp, 'getSignersPubKeys').returns(['pk0']); txp.signedBy = {}; delete txp['creator']; delete txp['creatorTs']; - txp.setCopayers('creator', { + txp.setCopayers({ pk0: 'creator', pk1: 'pepe', pk2: 'john' @@ -508,33 +647,27 @@ describe('TxProposal', function() { txp.signedBy = {}; delete txp['creator']; delete txp['creatorTs']; - txp._inputSigners = [ - ['pk0', 'pk1'] - ]; + sinon.stub(txp, 'getSignersPubKeys').returns(['pk0', 'pk1']); (function() { - txp.setCopayers( - 'creator', { - pk0: 'creator', - pk1: 'pepe', - pk2: 'john' - }, { - 'creator2': 1 - } - ); + txp.setCopayers({ + pk0: 'creator', + pk1: 'pepe', + pk2: 'john' + }, { + 'creator2': 1 + }); }).should.throw('only 1'); }) it("if signed, should not change ts", function() { var txp = dummyProposal(); var ts = Date.now(); - txp._inputSigners = [ - ['pk0', 'pk1'] - ]; + sinon.stub(txp, 'getSignersPubKeys').returns(['pk0', 'pk1']); txp.creator = 'creator'; txp.signedBy = { 'creator': 1 }; - txp.setCopayers('pepe', { + txp.setCopayers({ pk0: 'creator', pk1: 'pepe', pk2: 'john' diff --git a/test/TxProposals.js b/test/TxProposals.js index 2bafd03b8..13a2837e5 100644 --- a/test/TxProposals.js +++ b/test/TxProposals.js @@ -9,14 +9,15 @@ var TransactionBuilder = bitcore.TransactionBuilder; var util = bitcore.util; var networks = bitcore.networks; -var FakeBuilder = requireMock('FakeBuilder'); var TxProposal = copay.TxProposal; var TxProposals = copay.TxProposals; var dummyProposal = new TxProposal({ creator: 1, createdTs: 1, - builder: new FakeBuilder(), + builder: { + toObj: sinon.stub().returns({}), + }, inputChainPaths: ['m/1'], }); diff --git a/test/Wallet.js b/test/Wallet.js index 71dd56044..559793b92 100644 --- a/test/Wallet.js +++ b/test/Wallet.js @@ -3,7 +3,6 @@ var Wallet = copay.Wallet; var PrivateKey = copay.PrivateKey; var Network = requireMock('FakeNetwork'); var Blockchain = requireMock('FakeBlockchain'); -var Builder = requireMock('FakeBuilder'); var TransactionBuilder = bitcore.TransactionBuilder; var Transaction = bitcore.Transaction; var Address = bitcore.Address; @@ -234,7 +233,7 @@ describe('Wallet model', function() { unspentTest[0].scriptPubKey = w.publicKeyRing.getScriptPubKeyHex(1, true); var f = function() { - var ntxid = w._createTxProposal( + w._createTxProposal( '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', '123456789', null, @@ -276,7 +275,7 @@ describe('Wallet model', function() { unspentTest ); - Object.keys(txp._inputSigners).length.should.equal(1); + Object.keys(txp.getSignersPubKeys()).length.should.equal(1); var tx = txp.builder.build(); should.exist(tx); chai.expect(txp.comment).to.be.null; @@ -587,47 +586,6 @@ describe('Wallet model', function() { } }); - it('handle network txProposals correctly', function() { - var w = createW(); - var txp = { - 'txProposal': { - inputChainPaths: ['m/1'], - builderObj: { - version: 1, - outs: [{ - address: '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', - amountSatStr: '123456789' - }], - utxos: [{ - address: '2N6fdPg2QL7V36XKe7a8wkkA5HCy7fNYmZF', - scriptPubKey: 'a91493372782bab70f4eefdefefea8ece0df44f9596887', - txid: '2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1', - vout: 1, - amount: 10, - confirmations: 7 - }], - opts: { - remainderOut: { - address: '2N7BLvdrxJ4YzDtb3hfgt6CMY5rrw5kNT1H' - } - }, - scriptSig: ['00493046022100b8249a4fc326c4c33882e9d5468a1c6faa01e8c6cef0a24970122e804abdd860022100dbf6ee3b07d3aad8f73997e62ad20654a08aa63a7609792d02f3d5d088e69ad9014cad5321027445ab3a935dce7aee1dadb0d103ed6147a0f83deb80474a04538b2c5bc4d5092102ab32ba51402a139873aeb919c738f5a945f3956f8f8c6ba296677bd29e85d7e821036f119b72e09f76c11ebe2cf754d64eac2cb42c9e623455d54aaa89d70c11f9c82103bcbd3f8ab2c849ea9eae434733cee8b75120d26233def56011b3682ca12081d72103f37f81dc534163b9f73ecf36b91e6c3fb8ae370c24618f91bb1d972e86ceeee255ae'], - hashToScriptMap: { - '2N6fdPg2QL7V36XKe7a8wkkA5HCy7fNYmZF': '5321027445ab3a935dce7aee1dadb0d103ed6147a0f83deb80474a04538b2c5bc4d5092102ab32ba51402a139873aeb919c738f5a945f3956f8f8c6ba296677bd29e85d7e821036f119b72e09f76c11ebe2cf754d64eac2cb42c9e623455d54aaa89d70c11f9c82103bcbd3f8ab2c849ea9eae434733cee8b75120d26233def56011b3682ca12081d72103f37f81dc534163b9f73ecf36b91e6c3fb8ae370c24618f91bb1d972e86ceeee255ae' - } - } - } - }; - - var stub = sinon.stub(w.publicKeyRing, 'copayersForPubkeys').returns({ - '027445ab3a935dce7aee1dadb0d103ed6147a0f83deb80474a04538b2c5bc4d509': 'pepe' - }); - w._onTxProposal('senderID', txp, true); - Object.keys(w.txProposals.txps).length.should.equal(1); - w.getTxProposals().length.should.equal(1); - //stub.restore(); - }); - var newId = '00bacacafe'; it('handle new connections', function(done) { var w = createW(); @@ -800,13 +758,16 @@ describe('Wallet model', function() { var w = createW2([k2]); var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); + var now = Date.now(); w.spend({ toAddress: toAddress, amountSat: amountSatStr, }, function(err, ntxid) { w.on('txProposalsUpdated', function() { - w.getTxProposals()[0].signedByUs.should.equal(true); - w.getTxProposals()[0].rejectedByUs.should.equal(false); + var txp = w.txProposals.txps[ntxid]; + var myId = w.getMyCopayerId(); + txp.signedBy[myId].should.be.above(now - 1); + should.not.exist(txp.rejectedBy[myId]); done(); }); w.privateKey = k2; @@ -870,8 +831,9 @@ describe('Wallet model', function() { w.blockchain.fixUnspent(utxo); sinon.spy(w, 'sendIndexes'); sinon.spy(w, 'sendTxProposal'); - sinon.spy(w, 'broadcastTx'); + sinon.spy(w, 'issueTx'); sinon.stub(w, 'requiresMultipleSignatures').returns(false); + sinon.stub(w.blockchain, 'broadcast').yields(null, 1234); w.spend({ toAddress: toAddress, amountSat: amountSatStr, @@ -879,9 +841,8 @@ describe('Wallet model', function() { should.not.exist(err); should.exist(id); status.should.equal(Wallet.TX_BROADCASTED); - w.sendTxProposal.calledOnce.should.equal(true); - w.sendIndexes.calledOnce.should.equal(true); - w.broadcastTx.calledOnce.should.equal(true); + w.blockchain.broadcast.calledOnce.should.equal(true); + w.issueTx.calledOnce.should.equal(true); done(); }); }); @@ -893,7 +854,7 @@ describe('Wallet model', function() { sinon.stub(w, 'requiresMultipleSignatures').returns(false); sinon.spy(w, 'sendIndexes'); sinon.spy(w, 'sendTxProposal'); - sinon.stub(w, '_doBroadcastTx').yields('error'); + sinon.stub(w, 'broadcastToBitcoinNetwork').yields('error'); w.spend({ toAddress: toAddress, amountSat: amountSatStr, @@ -938,26 +899,7 @@ describe('Wallet model', function() { }); - describe('#broadcastTx', function() { - it('should fail to send incomplete transaction', function(done) { - var w = createW2(null, 1); - var utxo = createUTXO(w); - var txp = w._createTxProposal(toAddress, amountSatStr + 0, 'hola', utxo); - var ntxid = w.txProposals.add(txp); - - // Assign fake builder - txp.builder = new Builder(); - sinon.stub(txp.builder, 'build').returns({ - isComplete: function() { - return false; - } - }); - (function() { - w.broadcastTx(ntxid); - }).should.throw('Tx is not complete. Can not broadcast'); - done(); - }); - + describe('#issueTx', function() { it('should broadcast a TX', function(done) { var w = createW2(null, 1); var utxo = createUTXO(w); @@ -965,7 +907,7 @@ describe('Wallet model', function() { var ntxid = w.txProposals.add(txp); sinon.stub(w.blockchain, 'broadcast').yields(null, 1234); - w.broadcastTx(ntxid, function(err, txid, status) { + w.issueTx(ntxid, function(err, txid, status) { should.not.exist(err); txid.should.equal(1234); status.should.equal(Wallet.TX_BROADCASTED); @@ -973,7 +915,6 @@ describe('Wallet model', function() { }); }); - it('should send Payment Messages on a PayPro payment', function(done) { var w = createW2(null, 1); var utxo = createUTXO(w); @@ -992,7 +933,7 @@ describe('Wallet model', function() { sinon.stub(w, 'onPayProPaymentAck'); - w.broadcastTx(ntxid, function(err, txid, status) { + w.issueTx(ntxid, function(err, txid, status) { should.not.exist(err); txid.should.equal(1234); status.should.equal(Wallet.TX_BROADCASTED); @@ -1004,6 +945,26 @@ describe('Wallet model', function() { done(); }); }); + + it('should fail to send incomplete transaction', function(done) { + var w = createW2(null, 1); + var utxo = createUTXO(w); + var txp = w._createTxProposal(toAddress, amountSatStr + 0, 'hola', utxo); + var ntxid = w.txProposals.add(txp); + + // Assign fake builder + sinon.stub(txp.builder, 'build').returns({ + serialize: sinon.stub().returns('xxx'), + isComplete: sinon.stub().returns(false), + }); + (function() { + w.issueTx(ntxid); + }).should.throw('tx is not complete'); + done(); + }); + + + }); @@ -1128,7 +1089,7 @@ describe('Wallet model', function() { }); }); - describe('removeTxWithSpentInputs', function() { + describe.skip('removeTxWithSpentInputs', function() { var w; var utxos; beforeEach(function() { @@ -1508,22 +1469,21 @@ describe('Wallet model', function() { }); }); - describe('_getKeymap', function() { + describe('_getPubkeyToCopayerMap', function() { var w = cachedCreateW(); it('should set keymap', function() { - var stub = sinon.stub(w.publicKeyRing, 'copayersForPubkeys', function() { - return { - '123': 'juan' - }; + var stub = sinon.stub(w.publicKeyRing, 'copayersForPubkeys').returns({ + '123': 'juan' }); var txp = { - _inputSigners: [ + getSignersPubKeys: sinon.stub().returns([ ['123'] - ], + ]), inputChainPaths: ['/m/1'], }; - var map = w._getKeyMap(txp); + var map = w._getPubkeyToCopayerMap(txp); + console.log('[Wallet.js.1526:map:]', map); //TODO Object.keys(map).length.should.equal(1); map['123'].should.equal('juan'); stub.restore(); @@ -1534,13 +1494,13 @@ describe('Wallet model', function() { return {}; }); var txp = { - _inputSigners: [ - ['234'] - ], + getSignersPubKeys: sinon.stub().returns([ + ['123'] + ]), inputChainPaths: ['/m/1'], }; (function() { - w._getKeyMap(txp); + w._getPubkeyToCopayerMap(txp); }).should.throw('does not match known copayers'); stub.restore(); }); @@ -1550,14 +1510,14 @@ describe('Wallet model', function() { return {}; }); var txp = { - _inputSigners: [ + getSignersPubKeys: sinon.stub().returns([ ['234', '321'], ['234', '322'] - ], + ]), inputChainPaths: ['/m/1'], }; (function() { - w._getKeyMap(txp); + w._getPubkeyToCopayerMap(txp); }).should.throw('does not match known copayers'); stub.restore(); }); @@ -1570,12 +1530,12 @@ describe('Wallet model', function() { }; }); var txp = { - _inputSigners: [ - ['234', '123'] - ], + getSignersPubKeys: sinon.stub().returns([ + ['234', '321'], + ]), inputChainPaths: ['/m/1'], }; - var map = w._getKeyMap(txp); + var map = w._getPubkeyToCopayerMap(txp); Object.keys(map).length.should.equal(2); map['123'].should.equal('juan'); map['234'].should.equal('pepe'); @@ -1593,14 +1553,16 @@ describe('Wallet model', function() { }; }); var txp = { - _inputSigners: [ + getSignersPubKeys: sinon.stub().returns([ ['234', '123'], ['555'] - ], + ]), + + _inputSigners: [], inputChainPaths: ['/m/1'], }; (function() { - w._getKeyMap(txp); + w._getPubkeyToCopayerMap(txp); }).should.throw('different sig'); stub.restore(); }); @@ -1618,14 +1580,14 @@ describe('Wallet model', function() { }; }); var txp = { - _inputSigners: [ + getSignersPubKeys: sinon.stub().returns([ ['234', '123'], - ['555', '666'] - ], + ['555', '666'], + ]), inputChainPaths: ['/m/1'], }; (function() { - w._getKeyMap(txp); + w._getPubkeyToCopayerMap(txp); }).should.throw('different sig'); stub.restore(); }); @@ -1643,13 +1605,13 @@ describe('Wallet model', function() { }; }); var txp = { - _inputSigners: [ + getSignersPubKeys: sinon.stub().returns([ ['234', '123'], ['555', '666'] - ], + ]), inputChainPaths: ['/m/1'], }; - var gk = w._getKeyMap(txp); + var gk = w._getPubkeyToCopayerMap(txp); gk.should.deep.equal({ '123': 'pedro', '234': 'pepe', @@ -1662,72 +1624,18 @@ describe('Wallet model', function() { describe('_onTxProposal', function() { var w, data, txp; - beforeEach(function() { w = cachedCreateW(); data = { txProposal: { dummy: 1, - }, - }; - sinon.stub(w.txProposals, 'deleteOne'); - }); - - - it('should handle corrupt tx', function() { - w.txProposals.merge = sinon.stub().throws(new Error('test error')); - - sinon.stub(w, 'on'); - w._onTxProposal('senderID', data); - w.on.called.should.equal(false); - }); - - it('should call _processIncomingTxProposal', function(done) { - var args = { - xxx: 'yyy', - new: true, - txp: { - setCopayers: sinon.stub(), - }, - }; - sinon.stub(w.txProposals, 'merge').returns(args); - sinon.stub(w, '_processIncomingTxProposal').yields(null); - sinon.stub(w, '_getKeyMap').returns(null); - - w.on('txProposalEvent', function(e) { - e.type.should.equal('new'); - w._processIncomingTxProposal.getCall(0).args[0].should.deep.equal(args); - done(); - }); - w._onTxProposal('senderID', data); - }); - - it('should handle corrupt tx, case2', function() { - sinon.stub(w.txProposals, 'merge').returns({ - ntxid: '1' - }); - sinon.stub(w, 'on'); - sinon.stub(w, '_getKeyMap').throws(new Error('test error')); - w._onTxProposal('senderID', data); - w.on.called.should.equal(false); - }); - }); - - - describe.skip('_onTxProposal', function() { - var w, data, txp; - - beforeEach(function() { - w = cachedCreateW(); - w._getKeyMap = sinon.stub(); - w.sendSeen = sinon.spy(); - w.sendTxProposal = sinon.spy(); - data = { - txProposal: { - dummy: 1, + builderObj: { + dummy: 1, + }, }, }; txp = { + getId: sinon.stub().returns('ntxid'), getSeen: sinon.stub().returns(false), setSeen: sinon.spy(), setCopayers: sinon.spy(), @@ -1738,159 +1646,132 @@ describe('Wallet model', function() { }, }; - w.txProposals.get = sinon.stub().returns(txp); - - w.txProposals.merge = sinon.stub().returns({ - ntxid: 1, - txp: txp, - new: true, - hasChanged: true, - }); - + sinon.stub(w, '_processIncomingNewTxProposal').yields(null); + sinon.stub(w.txProposals, 'get').returns(null); + sinon.stub(w.txProposals, 'deleteOne'); + sinon.stub(w, '_txProposalFromUntrustedObj').returns(txp); + sinon.stub(w, '_getPubkeyToCopayerMap'); }); - it('should handle new 1', function(done) { - var spy1 = sinon.spy(); - var spy2 = sinon.spy(); - w.on('txProposalEvent', spy1); - w.on('txProposalsUpdated', spy2); + afterEach(function() {}); + + it('should handle corrupt message', function() { + w._txProposalFromUntrustedObj.throws('error'); + w._onTxProposal('senderID', data); + w._processIncomingNewTxProposal.called.should.equal(false); + }); + + it('should ignore localTx', function() { + w.txProposals.get = sinon.stub().returns(txp); + w._txProposalFromUntrustedObj.throws('error'); + w._onTxProposal('senderID', data); + w._processIncomingNewTxProposal.called.should.equal(false); + }); + + it('should accept a new valid TXP', function(done) { + w.txProposals.get = sinon.stub().returns(null); w.on('txProposalEvent', function(e) { e.type.should.equal('new'); - spy1.called.should.be.true; - spy2.called.should.be.true; - txp.setSeen.calledOnce.should.be.true; - w.sendSeen.calledOnce.should.equal(true); - w.sendTxProposal.calledOnce.should.equal(true); + w._processIncomingNewTxProposal.called.should.equal(true); + w._getPubkeyToCopayerMap.called.should.equal(true); done(); - }); - + }) w._onTxProposal('senderID', data); }); - it('should handle signed', function(done) { - var data = { - txProposal: { - dummy: 1, - }, - }; + + it('should ignore is a TXP arrived 2 times', function(done) { + w.txProposals.get = sinon.stub().returns(null); + var secondCall = false; + w.on('txProposalEvent', function(e) { + e.type.should.equal('new'); + w._processIncomingNewTxProposal.calledOnce.should.equal(true); + w._getPubkeyToCopayerMap.called.should.equal(true); + w._onTxProposal('senderID', data); + w._processIncomingNewTxProposal.calledOnce.should.equal(true); + done(); + }) + w._onTxProposal('senderID', data); + }); + + + + it('should handle a real txp correctly', function(done) { + w._txProposalFromUntrustedObj.restore(); + w._getPubkeyToCopayerMap.restore(); var txp = { - getSeen: sinon.stub().returns(true), - setSeen: sinon.spy(), - setCopayers: sinon.stub().returns(['new copayer']), - builder: { - build: sinon.stub().returns({ - isComplete: sinon.stub().returns(false), - }), - }, + 'txProposal': { + inputChainPaths: ['m/1'], + builderObj: { + version: 1, + outs: [{ + address: '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', + amountSatStr: '123456789' + }], + utxos: [{ + address: '2N6fdPg2QL7V36XKe7a8wkkA5HCy7fNYmZF', + scriptPubKey: 'a91493372782bab70f4eefdefefea8ece0df44f9596887', + txid: '2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1', + vout: 1, + amount: 10, + confirmations: 7 + }], + opts: { + remainderOut: { + address: '2N7BLvdrxJ4YzDtb3hfgt6CMY5rrw5kNT1H' + } + }, + scriptSig: ['00493046022100b8249a4fc326c4c33882e9d5468a1c6faa01e8c6cef0a24970122e804abdd860022100dbf6ee3b07d3aad8f73997e62ad20654a08aa63a7609792d02f3d5d088e69ad9014cad5321027445ab3a935dce7aee1dadb0d103ed6147a0f83deb80474a04538b2c5bc4d5092102ab32ba51402a139873aeb919c738f5a945f3956f8f8c6ba296677bd29e85d7e821036f119b72e09f76c11ebe2cf754d64eac2cb42c9e623455d54aaa89d70c11f9c82103bcbd3f8ab2c849ea9eae434733cee8b75120d26233def56011b3682ca12081d72103f37f81dc534163b9f73ecf36b91e6c3fb8ae370c24618f91bb1d972e86ceeee255ae'], + hashToScriptMap: { + '2N6fdPg2QL7V36XKe7a8wkkA5HCy7fNYmZF': '5321027445ab3a935dce7aee1dadb0d103ed6147a0f83deb80474a04538b2c5bc4d5092102ab32ba51402a139873aeb919c738f5a945f3956f8f8c6ba296677bd29e85d7e821036f119b72e09f76c11ebe2cf754d64eac2cb42c9e623455d54aaa89d70c11f9c82103bcbd3f8ab2c849ea9eae434733cee8b75120d26233def56011b3682ca12081d72103f37f81dc534163b9f73ecf36b91e6c3fb8ae370c24618f91bb1d972e86ceeee255ae' + } + } + } }; - w.txProposals.get = sinon.stub().returns(txp); - w.txProposals.merge = sinon.stub().returns({ - ntxid: 1, - txp: txp, - new: false, - hasChanged: true, + var stub = sinon.stub(w.publicKeyRing, 'copayersForPubkeys').returns({ + '027445ab3a935dce7aee1dadb0d103ed6147a0f83deb80474a04538b2c5bc4d509': 'pepe' }); + w.on('txProposalEvent', function(e) { + Object.keys(w.txProposals.txps).length.should.equal(1); + done(); + }); + w._onTxProposal('senderID', txp, true); + }); + }); + + + describe('_onSignature', function() { + var w, data, txp; + beforeEach(function() { + w = cachedCreateW2(); + }); + + afterEach(function() {}); + + it('should handle corrupt message', function() { + w._onSignature('senderID', 'sigs'); + }); + + it('should sign a txp', function(done) { + var utxo = createUTXO(w); + var txp = w._createTxProposal(PP.outs[0].address, PP.outs[0].amountSatStr, 'hola', utxo); + var ntxid = w.txProposals.add(txp); + sinon.stub(w.blockchain, 'broadcast').yields(null, 1234); + data = { + ntxid: ntxid, + signatures: [1], + } + sinon.stub(w.txProposals, 'get').returns(txp); + sinon.stub(txp, '_addSignatureAndVerify').returns(); - var spy1 = sinon.spy(); - var spy2 = sinon.spy(); - w.on('txProposalEvent', spy1); - w.on('txProposalsUpdated', spy2); w.on('txProposalEvent', function(e) { e.type.should.equal('signed'); - spy1.called.should.be.true; - spy2.called.should.be.true; - txp.setSeen.calledOnce.should.be.false; - w.sendSeen.calledOnce.should.be.false; - w.sendTxProposal.calledOnce.should.be.true; - done(); - }); - - w._onTxProposal('senderID', data); + }) + w._onSignature('senderID', data); }); - - it('should only mark as broadcast if found in the blockchain', function(done) { - w.txProposals.get = sinon.stub().returns(txp); - w.txProposals.merge = sinon.stub().returns({ - ntxid: 1, - txp: txp, - new: false, - hasChanged: false, - }); - w._checkSentTx = sinon.stub().yields(false); - w.on('txProposalEvent', function(e) { - txp.setSent.called.should.equal(false); - txp.setSent.calledWith(1).should.equal(false); - w.sendTxProposal.called.should.equal(false); - done(); - }); - - w._onTxProposal('senderID', data); - }); - - it('should not overwrite sent info', function(done) { - var data = { - txProposal: { - dummy: 1, - }, - }; - var txp = { - getSeen: sinon.stub().returns(true), - setCopayers: sinon.stub().returns(['new copayer']), - getSent: sinon.stub().returns(true), - setSent: sinon.spy(), - builder: { - build: sinon.stub().returns({ - isComplete: sinon.stub().returns(true), - }), - }, - }; - - w.txProposals.get = sinon.stub().returns(txp); - w.txProposals.merge = sinon.stub().returns({ - ntxid: 1, - txp: txp, - new: false, - hasChanged: false, - }); - w._checkSentTx = sinon.stub().yields(true); - - w._onTxProposal('senderID', data); - txp.setSent.called.should.be.false; - w.sendTxProposal.called.should.be.false; - done(); - }); - - it('should resend when not complete only if changed', function(done) { - var data = { - txProposal: { - dummy: 1, - }, - }; - var txp = { - getSeen: sinon.stub().returns(true), - setCopayers: sinon.stub().returns(['new copayer']), - builder: { - build: sinon.stub().returns({ - isComplete: sinon.stub().returns(false), - }), - }, - }; - - w.txProposals.get = sinon.stub().returns(txp); - w.txProposals.merge = sinon.stub().returns({ - ntxid: 1, - txp: txp, - new: false, - hasChanged: false, - }); - - w._onTxProposal('senderID', data); - w.sendTxProposal.called.should.be.false; - done(); - }); }); @@ -1993,6 +1874,70 @@ describe('Wallet model', function() { }); + describe('sendMesages', function() { + var w, txp; + beforeEach(function() { + w = createW2(null, 1); + var utxo = createUTXO(w); + txp = w._createTxProposal( + '2MtP8WyiwG7ZdVWM96CVsk2M1N8zyfiVQsY', + '123456789', + null, + utxo + ); + }); + + it('should be able to sendReject', function() { + w.sendReject(txp.getId()); + w.network.send.calledOnce.should.equal(true); + var payload = w.network.send.getCall(0).args[1]; + payload.type.should.equal('reject'); + payload.walletId.should.equal(w.id); + payload.ntxid.should.equal(txp.getId()); + }); + + + it('should be able to sendSend', function() { + w.sendSeen(txp.getId()); + w.network.send.calledOnce.should.equal(true); + var payload = w.network.send.getCall(0).args[1]; + payload.type.should.equal('seen'); + payload.walletId.should.equal(w.id); + payload.ntxid.should.equal(txp.getId()); + }); + + it('should be able to sendTxProposal', function() { + w.txProposals.add(txp); + w.sendTxProposal(txp.getId()); + w.network.send.calledOnce.should.equal(true); + var payload = w.network.send.getCall(0).args[1]; + payload.type.should.equal('txProposal'); + payload.walletId.should.equal(w.id); + payload.txProposal.should.deep.equal(txp.toObjTrim()); + }); + it('should be able to sendAllTxProposals', function() { + w.txProposals.add(txp); + w.sendAllTxProposals(); + w.network.send.calledOnce.should.equal(true); + var payload = w.network.send.getCall(0).args[1]; + payload.type.should.equal('txProposal'); + payload.walletId.should.equal(w.id); + payload.txProposal.should.deep.equal(txp.toObjTrim()); + }); + it('should be able to sendSignature', function() { + w.txProposals.add(txp); + w.sendSignature(txp.getId()); + w.network.send.calledOnce.should.equal(true); + var payload = w.network.send.getCall(0).args[1]; + payload.type.should.equal('signature'); + payload.walletId.should.equal(w.id); + payload.signatures.length.should.equal(1); + var sig = new Buffer(payload.signatures[0], 'hex'); + sig.length.should.be.above(70); + sig.length.should.be.below(74); + }); + }); + describe('#obtainNetworkName', function() { it('should return the networkname', function() { Wallet.obtainNetworkName({ @@ -2438,13 +2383,19 @@ describe('Wallet model', function() { isChange: true, }]); - w.getTxProposals = sinon.stub().returns([{ + w.txProposals.txps = [{ sentTxid: 'id0', comment: 'My comment', + rejectedBy: {}, + signedBy: {}, + seenBy: {}, }, { sentTxid: 'id1', comment: 'Another comment', - }]); + rejectedBy: {}, + signedBy: {}, + seenBy: {}, + }]; w.getTransactionHistory(function(err, res) { res.should.exist; res.items.should.exist; diff --git a/test/mocks/FakeBuilder.js b/test/mocks/FakeBuilder.js deleted file mode 100644 index 8d81531ff..000000000 --- a/test/mocks/FakeBuilder.js +++ /dev/null @@ -1,67 +0,0 @@ -'use scrict'; -var bitcore = bitcore || require('bitcore'); -var Script = bitcore.Script; - -var VALID_SCRIPTSIG_BUF = new Buffer('0048304502200708a381dde585ef7fdfaeaeb5da9b451d3e22b01eac8a5e3d03b959e24a7478022100c90e76e423523a54a9e9c43858337ebcef1a539a7fc685c2698dd8648fcf1b9101473044022030a77c9613d6ee010717c1abc494668d877e3fa0ae4c520f65cc3b308754c98c02205219d387bcb291bd44805b9468439e4168b02a6a180cdbcc24d84d71d696c1ae014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae', 'hex'); - -function Tx() { - this.ins = [{ - s: VALID_SCRIPTSIG_BUF - }]; -}; - - -Tx.prototype.serialize = function() { - return new Buffer('1234','hex'); -}; - - -Tx.prototype.getSize = function() { - return 1; -}; - -Tx.prototype.getHashType = function() { - return 1; -}; - -Tx.prototype.getNormalizedHash = function() { - return '123456'; -}; -Tx.prototype.hashForSignature = function() { - return new Buffer('31103626e162f1cbfab6b95b08c9f6e78aae128523261cb37f8dfd4783cb09a7', 'hex'); -}; - -function FakeBuilder() { - this.test = 1; - this.tx = new Tx(); - this.signhash = 1; - this.inputMap = [{ - address: '2NDJbzwzsmRgD2o5HHXPhuq5g6tkKTjYkd6', - scriptPubKey: new Script(new Buffer('a914dc0623476aefb049066b09b0147a022e6eb8429187', 'hex')), - scriptType: 4, - i: 0 - }]; - - this.vanilla = { - scriptSig: [VALID_SCRIPTSIG_BUF], - outs: JSON.stringify([{ - address: '2NDJbzwzsmRgD2o5HHXPhuq5g6tkKTjYkd6', - amountSatStr: '123', - }]), - - } -} - - -FakeBuilder.prototype.merge = function() {}; - -FakeBuilder.prototype.build = function() { - return this.tx; -}; - - -FakeBuilder.prototype.toObj = function() { - return this; -}; -FakeBuilder.VALID_SCRIPTSIG_BUF = VALID_SCRIPTSIG_BUF; -module.exports = FakeBuilder; diff --git a/util/build.js b/util/build.js index 4b4b1f74d..f3fb38c7b 100644 --- a/util/build.js +++ b/util/build.js @@ -128,9 +128,6 @@ var createBundle = function(opts) { b.require('./test/mocks/FakeNetwork', { expose: './mocks/FakeNetwork' }); - b.require('./test/mocks/FakeBuilder', { - expose: './mocks/FakeBuilder' - }); } if (!opts.debug) { diff --git a/views/includes/transaction.html b/views/includes/transaction.html index dbe6da338..aca03ead0 100644 --- a/views/includes/transaction.html +++ b/views/includes/transaction.html @@ -59,7 +59,7 @@