diff --git a/js/controllers/send.js b/js/controllers/send.js index 376a0199f..0e61066a1 100644 --- a/js/controllers/send.js +++ b/js/controllers/send.js @@ -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/TxProposal.js b/js/models/TxProposal.js index 865d70dd1..036dd5b63 100644 --- a/js/models/TxProposal.js +++ b/js/models/TxProposal.js @@ -41,8 +41,7 @@ function TxProposal(opts) { this.paymentAckMemo = opts.paymentAckMemo || null; this.paymentProtocolURL = opts.paymentProtocolURL || null; - // not from obj - this._pubkeysForScriptCache = {}; + this.resetCache(); // New Tx Proposal if (_.isEmpty(this.seenBy) && opts.creator) { @@ -206,10 +205,122 @@ TxProposal.prototype.isPending = function(maxRejectCount) { return true; }; +TxProposal.prototype._setSigned = function(copayerId) { + + // Sign powns rejected + if (this.rejectedBy[copayerId]) { + log.info("WARN: a previously rejected transaction was signed by:", copayerId); + delete this.rejectedBy[copayerId]; + } + + this.signedBy[copayerId] = Date.now(); + + return this; +}; + + /** + * + * @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: {}, + }; +}; + +/** + * 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 + * @desc get Pubkeys of signers, for each input. this is CPU intensive * * @return {string[][]} array of hashes for signing pubkeys for each input */ @@ -219,34 +330,36 @@ TxProposal.prototype.getSignersPubKeys = function(forceUpdate) { var signersPubKey = []; - if (!self._signersPubKey || forceUpdate) { + 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._pubkeysForScriptCache[input.s]) { + if (!self.cache.pubkeysForScript[input.s]) { var scriptSig = new Script(input.s); var signatureCount = scriptSig.countSignatures(); - var info = TxProposal._infoFromRedeemScript(scriptSig); + var info = TxProposal.infoFromRedeemScript(scriptSig); var txSigHash = tx.hashForSignature(info.script, parseInt(index), Transaction.SIGHASH_ALL); - var inputSignersPubKey = TxProposal._verifySignatures(info.keys, scriptSig, txSigHash); + 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._pubkeysForScriptCache[input.s] = inputSignersPubKey; + self.cache.pubkeysForScript[input.s] = inputSignersPubKey; } - signersPubKey[index] = self._pubkeysForScriptCache[input.s]; + signersPubKey[index] = self.cache.pubkeysForScript[input.s]; }); - self._signersPubKey = signersPubKey; + self.cache.signersPubKey = signersPubKey; + } else { + log.debug('Using signatures verification cache') } - return self._signersPubKey; + return self.cache.signersPubKey; }; TxProposal.prototype.getId = function() { @@ -261,7 +374,7 @@ TxProposal.prototype.getId = function() { TxProposal.prototype.toObj = function() { var o = JSON.parse(JSON.stringify(this)); delete o['builder']; - delete o['_pubkeysForScriptCache']; + delete o['cache']; o.builderObj = this.builder.toObj(); return o; }; @@ -279,8 +392,6 @@ TxProposal.fromObj = function(o, forceOpts) { preconditions.checkArgument(o.builderObj); delete o['builder']; forceOpts = forceOpts || {}; - var builderClass = forceOpts.transactionBuilderClass || TransactionBuilder; - o.builderObj.opts = o.builderObj.opts || {}; // force opts is requested. @@ -295,7 +406,7 @@ TxProposal.fromObj = function(o, forceOpts) { } try { - o.builder = builderClass.fromObj(o.builderObj); + o.builder = TransactionBuilder.fromObj(o.builderObj); } catch (e) { throw new Error(e); return null; @@ -317,7 +428,7 @@ 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])) @@ -333,33 +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)); - log.debug('\t Verifying CHUNK:', i); - 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)'); @@ -455,10 +599,8 @@ TxProposal.prototype.setCopayers = function(pubkeyToCopayerMap) { 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) { diff --git a/js/models/TxProposals.js b/js/models/TxProposals.js index 44e038cce..477f276a2 100644 --- a/js/models/TxProposals.js +++ b/js/models/TxProposals.js @@ -105,6 +105,11 @@ TxProposals.prototype.add = function(txp) { }; +TxProposals.prototype.exist = function(ntxid) { + return this.txps[ntxid] ? true : false; +}; + + TxProposals.prototype.get = function(ntxid) { var ret = this.txps[ntxid]; if (!ret) diff --git a/js/models/Wallet.js b/js/models/Wallet.js index 4907d56fe..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 @@ -144,7 +145,6 @@ Wallet.builderOpts = { signhash: bitcore.Transaction.SIGHASH_ALL, fee: undefined, feeSat: undefined, - builderClass: undefined, }; /** @@ -467,8 +467,6 @@ Wallet.prototype._processTxProposalPayPro = function(txp, cb) { }; /** - * _processIncomingTxProposal - * * @desc Process an NEW incoming transaction proposal. Runs safety and sanity checks on it. * * @param mergeInfo Proposals merge information, as returned by TxProposals.merge @@ -490,6 +488,12 @@ Wallet.prototype._processIncomingNewTxProposal = function(txp, cb) { }); }; + +/* only for stubbing */ +Wallet.prototype._txProposalFromUntrustedObj = function(data, opts) { + return TxProposal.fromUntrustedObj(data, opts); +}; + /** * @desc * Handles a NEW 'TXPROPOSAL' network message @@ -500,27 +504,31 @@ Wallet.prototype._processIncomingNewTxProposal = function(txp, cb) { * @emits txProposalsUpdated */ Wallet.prototype._onTxProposal = function(senderId, data) { + preconditions.checkArgument(data.txProposal); var self = this; - var incomingTx = TxProposal.fromUntrustedObj(data.txProposal, Wallet.builderOpts); - var incomingNtxid = incomingTx.getId(); try { - var localTx = this.txProposals.get(incomingNtxid); - } catch (e) {}; + var incomingTx = self._txProposalFromUntrustedObj(data.txProposal, Wallet.builderOpts); + var incomingNtxid = incomingTx.getId(); + } catch (e) { + log.warn(e); + return; + } - if (localTx) { - log.debug('Ignoring existing tx Proposal:' + incomingNtxid); + if (this.txProposals.exist(incomingNtxid)) { + log.warn('Ignoring existing tx Proposal:' + incomingNtxid); return; } self._processIncomingNewTxProposal(incomingTx, function(err) { if (err) { - log.info('Corrupt TX proposal received from:', senderId, err.toString()); + log.warn('Corrupt TX proposal received from:', senderId, err.toString()); return; } - var keyMap = self._getPubkeyToCopayerMap(incomingTx); - incomingTx.setCopayers(keyMap); + + var pubkeyToCopayerMap = self._getPubkeyToCopayerMap(incomingTx); + incomingTx.setCopayers(pubkeyToCopayerMap); self.txProposals.add(incomingTx); self.emitAndKeepAlive('txProposalEvent', { @@ -531,7 +539,7 @@ Wallet.prototype._onTxProposal = function(senderId, data) { }; -Wallet.prototype._onSignatures = function(senderId, data) { +Wallet.prototype._onSignature = function(senderId, data) { var self = this; try { var localTx = this.txProposals.get(data.ntxid); @@ -539,15 +547,12 @@ Wallet.prototype._onSignatures = function(senderId, data) { log.info('Ignoring signature for unknown tx Proposal:' + data.ntxid); return; }; - - var keyMap = self._getPubkeyToCopayerMap(locaTx); - - // TODO look senderIdin keyMap - localTx.addSignature(pubkeys, data.signature, keyMap); - - this.emitAndKeepAlive('txProposalEvent', { - type: 'signed', - cId: senderId, + localTx.addSignature(senderId, data.signatures); + self.issueTxIfComplete(data.ntxid, function(err, txid) { + self.emitAndKeepAlive('txProposalEvent', { + type: txid ? 'signedAndBroadcasted' : 'signed', + cId: senderId, + }); }); }; @@ -1259,10 +1264,13 @@ 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', - signatures: txp.getMySignatures(), + ntxid: ntxid, + signatures: signatures, walletId: this.id, }); }; @@ -1517,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(); + } +}; + /** * @@ -1531,13 +1549,13 @@ Wallet.prototype.sign = function(ntxid) { */ Wallet.prototype.signAndSend = function(ntxid, cb) { if (this.sign(ntxid)) { - var txp = this.txProposals.get(ntxid); this.sendSignature(ntxid); - if (txp.isFullySigned()) { - return this.broadcastTx(ntxid, cb); - } else { - return cb(null, ntxid, Wallet.TX_SIGNED); - } + 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')); } @@ -1552,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'); @@ -1575,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); } }); @@ -1590,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); @@ -1610,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); }); @@ -2183,7 +2198,7 @@ Wallet.prototype.spend = function(opts, cb) { 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'); 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/TxProposal.js b/test/TxProposal.js index 6e38d6d3d..af81b4d79 100644 --- a/test/TxProposal.js +++ b/test/TxProposal.js @@ -1,6 +1,5 @@ 'use strict'; - var Transaction = bitcore.Transaction; var WalletKey = bitcore.WalletKey; var Key = bitcore.Key; @@ -19,7 +18,18 @@ describe('TxProposal', function() { // These 2 signed the scripts below var PUBKEYS = ['03197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d', '03a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e3']; - // 1,2 signatures + + + // 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' @@ -27,12 +37,15 @@ describe('TxProposal', function() { return new Buffer(hex, 'hex'); }); + var someKeys = ["03b39d61dc9a504b13ae480049c140dcffa23a6cc9c09d12d6d1f332fee5e18ca5", "022929f515c5cf967474322468c3bd945bb6f281225b2c884b465680ef3052c07e"]; + function dummyBuilder(opts) { opts = opts || {}; - var script = SCRIPTSIG[opts.nsig - 1 || 1]; + var index = opts.nsig ? opts.nsig - 1 : 1; + var script = SCRIPTSIG[index]; var aIn = { s: script @@ -48,6 +61,7 @@ describe('TxProposal', function() { tx.hashForSignature = sinon.stub().returns( new Buffer('31103626e162f1cbfab6b95b08c9f6e78aae128523261cb37f8dfd4783cb09a7', 'hex')); + var builder = {}; builder.opts = opts.opts || {}; @@ -57,6 +71,7 @@ describe('TxProposal', function() { version: 1, opts: builder.opts, }); + builder.isFullySigned = sinon.stub().returns(false); builder.vanilla = { scriptSig: [SCRIPTSIG[1]], @@ -246,6 +261,7 @@ describe('TxProposal', function() { }); describe('Signature verification', function() { + var validScriptSig1Sig = new bitcore.Script(SCRIPTSIG[0]); var validScriptSig = new bitcore.Script(SCRIPTSIG[1]); var pubkeys = [ @@ -260,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) { @@ -300,14 +326,57 @@ describe('TxProposal', function() { pubkeys.should.deep.equal([PUBKEYS]); }); + + describe('#getSignatures', function() { - it('should', function() { + it('should get signatures', function() { var txp = dummyProposal(); - var sigs = txp.getSignatures(); + var sigs = txp.getSignatures(); sigs.length.should.equal(1); sigs[0].length.should.equal(2); - sigs[0][0].should.equal('304502200708a381dde585ef7fdfaeaeb5da9b451d3e22b01eac8a5e3d03b959e24a7478022100c90e76e423523a54a9e9c43858337ebcef1a539a7fc685c2698dd8648fcf1b9101'); - sigs[0][1].should.equal('3044022030a77c9613d6ee010717c1abc494668d877e3fa0ae4c520f65cc3b308754c98c02205219d387bcb291bd44805b9468439e4168b02a6a180cdbcc24d84d71d696c1ae01'); + 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() { @@ -452,7 +521,7 @@ describe('TxProposal', function() { it('with less signatures', function() { var txp = dummyProposal(); var txp1Sig = dummyProposal({ - onsig: true + nsig:1 }); var backup = txp.builder.vanilla.scriptSig[0]; var hasChanged = txp.merge(txp); @@ -542,7 +611,7 @@ describe('TxProposal', function() { var txp = dummyProposal(); var ts = Date.now(); - sinon.stub(txp,'getSignersPubKeys').returns(['pk1', 'pk0']); + sinon.stub(txp, 'getSignersPubKeys').returns(['pk1', 'pk0']); txp.signedBy = { 'creator': Date.now() }; @@ -558,7 +627,7 @@ describe('TxProposal', function() { it("should assign creator", function() { var txp = dummyProposal(); var ts = Date.now(); - sinon.stub(txp,'getSignersPubKeys').returns(['pk0']); + sinon.stub(txp, 'getSignersPubKeys').returns(['pk0']); txp.signedBy = {}; delete txp['creator']; delete txp['creatorTs']; @@ -578,24 +647,22 @@ describe('TxProposal', function() { txp.signedBy = {}; delete txp['creator']; delete txp['creatorTs']; - sinon.stub(txp,'getSignersPubKeys').returns(['pk0', 'pk1']); + sinon.stub(txp, 'getSignersPubKeys').returns(['pk0', 'pk1']); (function() { - txp.setCopayers( - { - 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(); - sinon.stub(txp,'getSignersPubKeys').returns(['pk0', 'pk1']); + sinon.stub(txp, 'getSignersPubKeys').returns(['pk0', 'pk1']); txp.creator = 'creator'; txp.signedBy = { 'creator': 1 diff --git a/test/Wallet.js b/test/Wallet.js index 8e3cf9d95..559793b92 100644 --- a/test/Wallet.js +++ b/test/Wallet.js @@ -586,46 +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); - //stub.restore(); - }); - var newId = '00bacacafe'; it('handle new connections', function(done) { var w = createW(); @@ -871,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, @@ -880,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(); }); }); @@ -894,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, @@ -939,25 +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 - 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(); + }); + + + }); @@ -1661,20 +1622,20 @@ describe('Wallet model', function() { }); }); - describe.skip('_onTxProposal', function() { + describe('_onTxProposal', function() { var w, data, txp; - beforeEach(function() { w = cachedCreateW(); - w._getPubkeyToCopayerMap = 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(), @@ -1685,197 +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 corrupt tx', function() { - w.txProposals.merge = sinon.stub().throws(new Error('test error')); + afterEach(function() {}); - sinon.stub(w, 'on'); + it('should handle corrupt message', function() { + w._txProposalFromUntrustedObj.throws('error'); w._onTxProposal('senderID', data); - w.on.called.should.equal(false); + w._processIncomingNewTxProposal.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, '_getPubkeyToCopayerMap').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, '_getPubkeyToCopayerMap').throws(new Error('test error')); - w._onTxProposal('senderID', data); - w.on.called.should.equal(false); - }); - it('should handle new 1', function(done) { - - 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('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); - done(); - }); - - w._onTxProposal('senderID', data); - }); - - it('should handle signed', function(done) { - var data = { - txProposal: { - dummy: 1, - }, - }; - 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), - }), - }, - }; - + it('should ignore localTx', function() { w.txProposals.get = sinon.stub().returns(txp); - w.txProposals.merge = sinon.stub().returns({ - ntxid: 1, - txp: txp, - new: false, - hasChanged: true, - }); + 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'); + w._processIncomingNewTxProposal.called.should.equal(true); + w._getPubkeyToCopayerMap.called.should.equal(true); + done(); + }) + w._onTxProposal('senderID', data); + }); + + + 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 = { + '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.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(); - }); }); @@ -2036,7 +1932,7 @@ describe('Wallet model', function() { 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'); + var sig = new Buffer(payload.signatures[0], 'hex'); sig.length.should.be.above(70); sig.length.should.be.below(74); });