diff --git a/.gitignore b/.gitignore index 7f0f7a730..ed1339448 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ lib-cov *.pid *.gz *.swp +*.sig tags pids logs @@ -48,6 +49,7 @@ browser-extensions/firefox/firefox-addon browser-extensions/firefox/data browser-extensions/firefox/copay.xpi version.js +!js/controllers/version.js android/package android/*.apk diff --git a/copay.js b/copay.js index 8e8b03089..5b2711dc9 100644 --- a/copay.js +++ b/copay.js @@ -1,5 +1,6 @@ // core module.exports.PublicKeyRing = require('./js/models/core/PublicKeyRing'); +module.exports.TxProposal = require('./js/models/core/TxProposal'); module.exports.TxProposals = require('./js/models/core/TxProposals'); module.exports.PrivateKey = require('./js/models/core/PrivateKey'); module.exports.Passphrase = require('./js/models/core/Passphrase'); diff --git a/css/main.css b/css/main.css index b1eeea852..af0a102b5 100644 --- a/css/main.css +++ b/css/main.css @@ -1006,7 +1006,7 @@ input.ng-invalid-match, input.ng-invalid-match:focus { .text-primary {color: #1ABC9C;} .text-secondary {color: #3498DB;} .text-white {color: #fff;} - +.text-warning {color: #CA5649;} .footer-setup a.text-gray:hover {color: #fff;} a.text-gray:hover {color: #2C3E50;} @@ -1014,6 +1014,7 @@ a.text-black:hover {color: #213140;} a.text-primary:hover {color: #50E3C2;} a.text-secondary:hover {color: #4A90E2;} a.text-white:hover {color: #ccc;} +a.text-warning:hover {color: #FD7262;} .box-setup-copayers { background: #2C3E50; diff --git a/img/step-1.svg b/img/step-1.svg new file mode 100644 index 000000000..50fc10493 --- /dev/null +++ b/img/step-1.svg @@ -0,0 +1,30 @@ + + + + Group@1x + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/img/step-2.svg b/img/step-2.svg new file mode 100644 index 000000000..9a3cec072 --- /dev/null +++ b/img/step-2.svg @@ -0,0 +1,32 @@ + + + + Group + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/img/step-3.svg b/img/step-3.svg new file mode 100644 index 000000000..e6b50d55b --- /dev/null +++ b/img/step-3.svg @@ -0,0 +1,34 @@ + + + + Group + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/index.html b/index.html index f09603637..1df6aa092 100644 --- a/index.html +++ b/index.html @@ -107,6 +107,7 @@ + diff --git a/js/controllers/copayers.js b/js/controllers/copayers.js index c6a9c2c54..c239beff1 100644 --- a/js/controllers/copayers.js +++ b/js/controllers/copayers.js @@ -1,7 +1,7 @@ 'use strict'; angular.module('copayApp.controllers').controller('CopayersController', - function($scope, $rootScope, $location, backupService) { + function($scope, $rootScope, $location, backupService, walletFactory, controllerUtils) { $scope.backup = function() { var w = $rootScope.wallet; @@ -18,4 +18,12 @@ angular.module('copayApp.controllers').controller('CopayersController', $location.path('/addresses'); }; + $scope.deleteWallet = function() { + var w = $rootScope.wallet; + w.disconnect(); + walletFactory.delete(w.id, function() { + controllerUtils.logout(); + }); + }; + }); diff --git a/js/controllers/home.js b/js/controllers/home.js index 6ca571481..2aac5071f 100644 --- a/js/controllers/home.js +++ b/js/controllers/home.js @@ -9,5 +9,5 @@ angular.module('copayApp.controllers').controller('HomeController', if ($rootScope.pendingPayment) { notification.info('Login Required', 'Please open wallet to complete payment'); } - $scope.hasWallets = walletFactory.getWallets().length > 0 ? true : false; + $scope.hasWallets = (walletFactory.getWallets() && walletFactory.getWallets().length > 0) ? true : false; }); diff --git a/js/controllers/open.js b/js/controllers/open.js index 2ca4ae03a..5efd96e16 100644 --- a/js/controllers/open.js +++ b/js/controllers/open.js @@ -11,7 +11,7 @@ angular.module('copayApp.controllers').controller('OpenController', }; $scope.loading = false; $scope.wallets = walletFactory.getWallets().sort(cmp); - $scope.selectedWalletId = $scope.wallets.length ? $scope.wallets[0].id : null; + $scope.selectedWalletId = walletFactory.storage.getLastOpened() || ($scope.wallets[0] && $scope.wallets[0].id); $scope.openPassword = ''; $scope.open = function(form) { diff --git a/js/controllers/send.js b/js/controllers/send.js index 3af9941d2..a87e5c256 100644 --- a/js/controllers/send.js +++ b/js/controllers/send.js @@ -50,7 +50,7 @@ angular.module('copayApp.controllers').controller('SendController', var w = $rootScope.wallet; w.createTx(address, amount, commentText, function(ntxid) { - if (w.totalCopayers > 1) { + if (w.isShared()) { $scope.loading = false; var message = 'The transaction proposal has been created'; notification.success('Success!', message); diff --git a/js/controllers/settings.js b/js/controllers/settings.js index a3ab1241e..36ffa144e 100644 --- a/js/controllers/settings.js +++ b/js/controllers/settings.js @@ -76,6 +76,8 @@ angular.module('copayApp.controllers').controller('SettingsController', unitToSatoshi: $scope.selectedUnit.value, })); - window.location.reload(); + // Go home reloading the application + var hashIndex = window.location.href.indexOf('#!/'); + window.location = window.location.href.substr(0, hashIndex); }; }); diff --git a/js/controllers/sidebar.js b/js/controllers/sidebar.js index 7606483b9..ea185383e 100644 --- a/js/controllers/sidebar.js +++ b/js/controllers/sidebar.js @@ -3,8 +3,6 @@ angular.module('copayApp.controllers').controller('SidebarController', function($scope, $rootScope, $sce, $location, $http, notification, controllerUtils) { - $scope.version = copay.version; - $scope.networkName = config.networkName; $scope.menu = [{ 'title': 'Addresses', 'icon': 'fi-address-book', @@ -62,23 +60,6 @@ angular.module('copayApp.controllers').controller('SidebarController', return new Array(num); } - $http.get('https://api.github.com/repos/bitpay/copay/tags').success(function(data) { - var toInt = function(s) { - return parseInt(s); - }; - var latestVersion = data[0].name.replace('v', '').split('.').map(toInt); - var currentVersion = copay.version.split('.').map(toInt); - var title = 'Copay ' + data[0].name + ' available.'; - var content; - if (currentVersion[0] < latestVersion[0]) { - content = 'It\'s important that you update your wallet at https://copay.io'; - notification.version(title, content, true); - } else if (currentVersion[0] == latestVersion[0] && currentVersion[1] < latestVersion[1]) { - var content = 'Please update your wallet at https://copay.io'; - notification.version(title, content, false); - } - }); - // Init socket handlers (with no wallet yet) controllerUtils.setSocketHandlers(); diff --git a/js/controllers/version.js b/js/controllers/version.js new file mode 100644 index 000000000..5e886c44a --- /dev/null +++ b/js/controllers/version.js @@ -0,0 +1,26 @@ +'use strict'; + +angular.module('copayApp.controllers').controller('VersionController', + function($scope, $rootScope, $http, notification) { + + $scope.version = copay.version; + $scope.networkName = config.networkName; + + $http.get('https://api.github.com/repos/bitpay/copay/tags').success(function(data) { + var toInt = function(s) { + return parseInt(s); + }; + var latestVersion = data[0].name.replace('v', '').split('.').map(toInt); + var currentVersion = copay.version.split('.').map(toInt); + var title = 'Copay ' + data[0].name + ' available.'; + var content; + if (currentVersion[0] < latestVersion[0]) { + content = 'It\'s important that you update your wallet at https://copay.io'; + notification.version(title, content, true); + } else if (currentVersion[0] == latestVersion[0] && currentVersion[1] < latestVersion[1]) { + var content = 'Please update your wallet at https://copay.io'; + notification.version(title, content, false); + } + }); + + }); diff --git a/js/models/blockchain/Insight.js b/js/models/blockchain/Insight.js index 863863e70..bafde9861 100644 --- a/js/models/blockchain/Insight.js +++ b/js/models/blockchain/Insight.js @@ -2,6 +2,7 @@ var imports = require('soop').imports(); var bitcore = require('bitcore'); +var coinUtil = bitcore.util; var preconditions = require('preconditions').singleton(); var http; @@ -37,33 +38,6 @@ function _asyncForEach(array, fn, callback) { } }; -function removeRepeatedElements(ar) { - var ya = false, - v = "", - aux = [].concat(ar), - r = Array(); - for (var i in aux) { // - v = aux[i]; - ya = false; - for (var a in aux) { - if (v == aux[a]) { - if (ya == false) { - ya = true; - } else { - aux[a] = ""; - } - } - } - } - for (var a in aux) { - if (aux[a] != "") { - r.push(aux[a]); - } - } - return r; -} - - Insight.prototype._getOptions = function(method, path, data) { return { host: this.host, @@ -78,6 +52,25 @@ Insight.prototype._getOptions = function(method, path, data) { }; }; + +// This is vulneable to txid maneability +// TODO: if ret = false, +// check output address from similar transactions. +// +Insight.prototype.checkSentTx = function(tx, cb) { + var hash = coinUtil.formatHashFull(tx.getHash()); + var options = this._getOptions('GET', '/api/tx/' + hash); + + this._request(options, function(err, res) { + if (err) return cb(err); + var ret = false; + if (res && res.txid === hash) { + ret = hash; + } + return cb(err, ret); + }); +}; + Insight.prototype.getTransactions = function(addresses, cb) { preconditions.shouldBeArray(addresses); preconditions.shouldBeFunction(cb); @@ -101,8 +94,11 @@ Insight.prototype.getTransactions = function(addresses, cb) { callback(); }); }, function() { - var clean_txids = removeRepeatedElements(txids); - _asyncForEach(clean_txids, function(txid, callback2) { + var uniqueTxids = {}; + for (var k in txids) { + uniqueTxids[txids[k]] = 1; + } + _asyncForEach(Object.keys(uniqueTxids), function(txid, callback2) { var options = self._getOptions('GET', '/api/tx/' + txid); self._request(options, function(err, res) { txs.push(res); @@ -164,8 +160,8 @@ Insight.prototype.checkActivity = function(addresses, cb) { var getOutputs = function(t) { return flatArray( t.vout.map(function(vout) { - return vout.scriptPubKey.addresses; - }) + return vout.scriptPubKey.addresses; + }) ); }; diff --git a/js/models/core/HDPath.js b/js/models/core/HDPath.js index 2e32ade7e..126b84a0a 100644 --- a/js/models/core/HDPath.js +++ b/js/models/core/HDPath.js @@ -33,12 +33,12 @@ HDPath.FullBranch = function(addressIndex, isChange, copayerIndex) { return BIP45_PUBLIC_PREFIX + '/' + sub; }; -HDPath.indicesForPath = function(path) { +HDPath.indexesForPath = function(path) { preconditions.shouldBeString(path); var s = path.split('/'); return { isChange: s[3] === '1', - index: parseInt(s[4]), + addressIndex: parseInt(s[4]), copayerIndex: parseInt(s[2]) }; }; diff --git a/js/models/core/PublicKeyRing.js b/js/models/core/PublicKeyRing.js index ad7fe6f15..c61cea9f2 100644 --- a/js/models/core/PublicKeyRing.js +++ b/js/models/core/PublicKeyRing.js @@ -23,14 +23,13 @@ function PublicKeyRing(opts) { this.copayersHK = opts.copayersHK || []; - this.indexes = opts.indexes ? HDParams.fromList(opts.indexes) - : HDParams.init(this.totalCopayers); + this.indexes = opts.indexes ? HDParams.fromList(opts.indexes) : HDParams.init(this.totalCopayers); - this.publicKeysCache = opts.publicKeysCache || {}; - this.nicknameFor = opts.nicknameFor || {}; - this.copayerIds = []; - this.copayersBackup = opts.copayersBackup || []; - this.addressToPath = {}; + this.publicKeysCache = opts.publicKeysCache || {}; + this.nicknameFor = opts.nicknameFor || {}; + this.copayerIds = []; + this.copayersBackup = opts.copayersBackup || []; + this.addressToPath = {}; } PublicKeyRing.fromObj = function(data) { @@ -100,14 +99,6 @@ PublicKeyRing.prototype._checkKeys = function() { throw new Error('dont have required keys yet'); }; -PublicKeyRing.prototype._newExtendedPublicKey = function() { - return new PrivateKey({ - networkName: this.network.name - }) - .deriveBIP45Branch() - .extendedPublicKeyString(); -}; - PublicKeyRing.prototype._updateBip = function(index) { var hk = this.copayersHK[index].derive(HDPath.IdBranch); this.copayerIds[index] = hk.eckey.public.toString('hex'); @@ -126,6 +117,8 @@ PublicKeyRing.prototype.nicknameForCopayer = function(copayerId) { }; PublicKeyRing.prototype.addCopayer = function(newEpk, nickname) { + preconditions.checkArgument(newEpk); + if (this.isComplete()) throw new Error('PKR already has all required key:' + this.totalCopayers); @@ -134,10 +127,6 @@ PublicKeyRing.prototype.addCopayer = function(newEpk, nickname) { throw new Error('PKR already has that key'); }); - if (!newEpk) { - newEpk = this._newExtendedPublicKey(); - } - var i = this.copayersHK.length; var bip = new HK(newEpk); this.copayersHK.push(bip); @@ -192,7 +181,9 @@ PublicKeyRing.prototype.getAddress = function(index, isChange, id) { // Overloaded to receive a PubkeyString or a consigner index PublicKeyRing.prototype.getHDParams = function(id) { var copayerIndex = this.getCosigner(id); - var index = this.indexes.filter(function(i) { return i.copayerIndex == copayerIndex }); + var index = this.indexes.filter(function(i) { + return i.copayerIndex == copayerIndex + }); if (index.length != 1) throw new Error('no index for copayerIndex'); return index[0]; @@ -231,9 +222,11 @@ PublicKeyRing.prototype.getCosigner = function(pubKey) { if (typeof pubKey == 'undefined') return HDPath.SHARED_INDEX; if (typeof pubKey == 'number') return pubKey; - var sorted = this.copayersHK.map(function(h, i){ + var sorted = this.copayersHK.map(function(h, i) { return h.eckey.public.toString('hex'); - }).sort(function(h1, h2){ return h1.localeCompare(h2); }); + }).sort(function(h1, h2) { + return h1.localeCompare(h2); + }); var index = sorted.indexOf(pubKey); if (index == -1) throw new Error('no public key in ring'); @@ -255,41 +248,87 @@ PublicKeyRing.prototype.getAddressesInfo = function(opts, pubkey) { PublicKeyRing.prototype.getAddressesInfoForIndex = function(index, opts, copayerIndex) { opts = opts || {}; - var isOwned = index.copayerIndex == HDPath.SHARED_INDEX - || index.copayerIndex == copayerIndex; + var isOwned = index.copayerIndex == HDPath.SHARED_INDEX || index.copayerIndex == copayerIndex; - var ret = []; - if (!opts.excludeChange) { - for (var i = 0; i < index.changeIndex; i++) { - var a = this.getAddress(i, true, index.copayerIndex); - ret.unshift({ - address: a, - addressStr: a.toString(), - isChange: true, - owned: isOwned - }); - } + var ret = []; + if (!opts.excludeChange) { + for (var i = 0; i < index.changeIndex; i++) { + var a = this.getAddress(i, true, index.copayerIndex); + ret.unshift({ + address: a, + addressStr: a.toString(), + isChange: true, + owned: isOwned + }); } + } - if (!opts.excludeMain) { - for (var i = 0; i < index.receiveIndex; i++) { - var a = this.getAddress(i, false, index.copayerIndex); - ret.unshift({ - address: a, - addressStr: a.toString(), - isChange: false, - owned: isOwned - }); - } + if (!opts.excludeMain) { + for (var i = 0; i < index.receiveIndex; i++) { + var a = this.getAddress(i, false, index.copayerIndex); + ret.unshift({ + address: a, + addressStr: a.toString(), + isChange: false, + owned: isOwned + }); } + } - return ret; + return ret; }; +PublicKeyRing.prototype.getForPath = function(path) { + var p = HDPath.indexesForPath(path); + var pubKeys = this.getPubKeys(p.addressIndex, p.isChange, p.copayerIndex); + return pubKeys; +}; + +PublicKeyRing.prototype.getForPaths = function(paths) { + preconditions.checkArgument(paths); + return paths.map(this.getForPath.bind(this)); +}; + + +PublicKeyRing.prototype.forPaths = function(paths) { + return { + pubKeys: paths.map(this.getForPath.bind(this)), + copayerIds: this.copayerIds, + } +}; + + +// returns pubkey -> copayerId. +PublicKeyRing.prototype.copayersForPubkeys = function(pubkeys, paths) { + preconditions.checkArgument(pubkeys); + preconditions.checkArgument(paths); + + var inKeyMap = {}, ret = {}; + for(var i in pubkeys ){ + inKeyMap[pubkeys[i]] = 1; + }; + + var keys = this.getForPaths(paths); + for(var i in keys ){ + for(var copayerIndex in keys[i] ){ + var kHex = keys[i][copayerIndex].toString('hex'); + if (inKeyMap[kHex]) { + ret[kHex] =this.copayerIds[copayerIndex]; + delete inKeyMap[kHex]; + } + } + } + for(var i in inKeyMap) + throw new Error('Pubkey not identified') + + return ret; +}; + + // TODO this could be cached PublicKeyRing.prototype._addScriptMap = function(map, path) { - var p = HDPath.indicesForPath(path); - var script = this.getRedeemScript(p.index, p.isChange, p.copayerIndex); + var p = HDPath.indexesForPath(path); + var script = this.getRedeemScript(p.addressIndex, p.isChange, p.copayerIndex); map[Address.fromScript(script, this.network.name).toString()] = script.getBuffer().toString('hex'); }; diff --git a/js/models/core/TxProposal.js b/js/models/core/TxProposal.js new file mode 100644 index 000000000..3ddeac22e --- /dev/null +++ b/js/models/core/TxProposal.js @@ -0,0 +1,321 @@ +'use strict'; + +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 VERSION = 1; +var CORE_FIELDS = ['builderObj', 'inputChainPaths', 'version', 'comment']; + + +function TxProposal(opts) { + preconditions.checkArgument(opts); + preconditions.checkArgument(opts.inputChainPaths, 'no inputChainPaths'); + preconditions.checkArgument(opts.builder, 'no builder'); + preconditions.checkArgument(opts.inputChainPaths, 'no inputChainPaths'); + + this.inputChainPaths = opts.inputChainPaths; + this.version = opts.version; + this.builder = opts.builder; + this.createdTs = opts.createdTs; + this.createdTs = opts.createdTs; + this._inputSignatures = []; + + // CopayerIds + this.creator = opts.creator; + 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._sync(); +} + + +TxProposal.prototype._check = function() { + + if (this.builder.signhash && this.builder.signhash !== Transaction.SIGHASH_ALL) { + throw new Error('Invalid tx proposal'); + } + + var tx = this.builder.build(); + if (!tx.ins.length) + throw new Error('Invalid tx proposal: no ins'); + + for (var i in tx.ins) { + var scriptSig = tx.ins[i].s; + if (!scriptSig || !scriptSig.length) { + throw new Error('Invalid tx proposal: no signatures'); + } + } + + for (var i = 0; i < tx.ins.length; i++) { + var hashType = tx.getHashType(i); + if (hashType && hashType !== Transaction.SIGHASH_ALL) + throw new Error('Invalid tx proposal: bad signatures'); + } +}; + + +TxProposal.prototype._updateSignedBy = function() { + this._inputSignatures = []; + + var tx = this.builder.build(); + for (var i in tx.ins) { + var scriptSig = new Script(tx.ins[i].s); + var signatureCount = scriptSig.countSignatures(); + var info = TxProposal._infoFromRedeemScript(scriptSig); + var txSigHash = tx.hashForSignature(info.script, parseInt(i), Transaction.SIGHASH_ALL); + var signatureIndexes = TxProposal._verifySignatures(info.keys, scriptSig, txSigHash); + if (signatureIndexes.length !== signatureCount) + throw new Error('Invalid signature'); + this._inputSignatures[i] = signatureIndexes.map(function(i) { + var r = info.keys[i].toString('hex'); + return r; + }); + }; +}; + +TxProposal.prototype._sync = function() { + this._check(); + this._updateSignedBy(); + return this; +} + + +TxProposal.prototype.getId = function() { + preconditions.checkState(this.builder); + return this.builder.build().getNormalizedHash().toString('hex'); +}; + +TxProposal.prototype.toObj = function() { + var o = JSON.parse(JSON.stringify(this)); + delete o['builder']; + o.builderObj = this.builder.toObj(); + return o; +}; + + +TxProposal._trim = function(o) { + var ret = {}; + CORE_FIELDS.forEach(function(k) { + ret[k] = o[k]; + }); + return ret; +}; + +TxProposal.fromObj = function(o, forceOpts) { + preconditions.checkArgument(o.builderObj); + delete o['builder']; + + try { + // force opts is requested. + for (var k in forceOpts) { + o.builderObj.opts[k] = forceOpts[k]; + } + o.builder = TransactionBuilder.fromObj(o.builderObj); + } catch (e) { + + // backwards (V0) compatatibility fix. + if (!o.version) { + o.builder = new BuilderMockV0(o.builderObj); + o.readonly = 1; + }; + } + return new TxProposal(o); +}; + +TxProposal.fromUntrustedObj = function(o, forceOpts) { + return TxProposal.fromObj(TxProposal._trim(o), forceOpts); +}; + +TxProposal.prototype.toObjTrim = function() { + return TxProposal._trim(this.toObj()); +}; + +TxProposal._formatKeys = function(keys) { + var ret = []; + for (var i in keys) { + if (!Buffer.isBuffer(keys[i])) + throw new Error('keys must be buffers'); + + var k = new Key(); + k.public = keys[i]; + ret.push(k); + }; + return ret; +}; + +TxProposal._verifySignatures = function(inKeys, scriptSig, txSigHash) { + preconditions.checkArgument(Buffer.isBuffer(txSigHash)); + preconditions.checkArgument(inKeys); + preconditions.checkState(Buffer.isBuffer(inKeys[0])); + + if (scriptSig.chunks[0] !== 0) + throw new Error('Invalid scriptSig'); + + var keys = TxProposal._formatKeys(inKeys); + var ret = []; + for (var i = 1; i <= scriptSig.countSignatures(); i++) { + var chunk = scriptSig.chunks[i]; + var sigRaw = new Buffer(chunk.slice(0, chunk.length - 1)); + for (var j in keys) { + var k = keys[j]; + if (k.verifySignatureSync(txSigHash, sigRaw)) { + ret.push(parseInt(j)); + break; + } + } + } + return ret; +}; + +TxProposal._infoFromRedeemScript = function(s) { + var redeemScript = new Script(s.chunks[s.chunks.length - 1]); + if (!redeemScript) + throw new Error('Bad scriptSig (no redeemscript)'); + + var pubkeys = redeemScript.capture(); + if (!pubkeys || !pubkeys.length) + throw new Error('Bad scriptSig (no pubkeys)'); + + return { + keys: pubkeys, + script: redeemScript, + }; +}; + +TxProposal.prototype.mergeBuilder = function(incoming) { + var b0 = this.builder; + var b1 = incoming.builder; + + var before = JSON.stringify(b0.toObj()); + b0.merge(b1); + var after = JSON.stringify(b0.toObj()); + return after !== before; +}; + + +TxProposal.prototype.setSeen = function(copayerId) { + if (!this.seenBy[copayerId]) + this.seenBy[copayerId] = Date.now(); +}; + +TxProposal.prototype.setRejected = function(copayerId) { + + if (this.signedBy[copayerId]) + throw new Error('Can not reject a signed TX'); + + if (!this.rejectedBy[copayerId]) + this.rejectedBy[copayerId] = Date.now(); +}; + +TxProposal.prototype.setSent = function(sentTxid) { + this.sentTxid = sentTxid; + this.sentTs = Date.now(); +}; + + + +TxProposal.prototype._allSignatures = function() { + var ret = {}; + for (var i in this._inputSignatures) + for (var j in this._inputSignatures[i]) + ret[this._inputSignatures[i][j]] = true; + + return ret; +}; + + +TxProposal.prototype.setCopayers = function(senderId, keyMap, readOnlyPeers) { + var newCopayer = {}, + oldCopayers = {}, + newSignedBy = {}, + readOnlyPeers = {}, + isNew = 1; + + for (var k in this.signedBy) { + oldCopayers[k] = 1; + isNew = 0; + }; + + if (isNew == 0) { + if (!this.creator || !this.createdTs) + throw new Error('Existing TX has no creator'); + + if (!this.signedBy[this.creator]) + throw new Error('Existing TX is not signed by creator'); + + + if (Object.keys(this.signedBy).length === 0) + throw new Error('Existing TX has no signatures'); + } + + + var iSig = this._inputSignatures[0]; + for (var i in iSig) { + var copayerId = keyMap[iSig[i]]; + if (!copayerId) + throw new Error('Found unknown signature') + + if (oldCopayers[copayerId]) { + //Already have it. Do nothing + } else { + newCopayer[copayerId] = Date.now(); + delete oldCopayers[i]; + } + } + + // 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 + 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]; + } + + // signedBy has preference over rejectedBy + for (var i in this.signedBy) { + delete this.rejectedBy[i]; + } + + return Object.keys(newCopayer); +}; + +// merge will not merge any metadata. +TxProposal.prototype.merge = function(incoming) { + var hasChanged = this.mergeBuilder(incoming); + this._sync(); + return hasChanged; +}; + +//This should be on bitcore / Transaction +TxProposal.prototype.countSignatures = function() { + var tx = this.builder.build(); + var ret = 0; + for (var i in tx.ins) { + ret += tx.countInputSignatures(i); + } + return ret; +}; + +module.exports = TxProposal; diff --git a/js/models/core/TxProposals.js b/js/models/core/TxProposals.js index 77462cca8..f0933afd6 100644 --- a/js/models/core/TxProposals.js +++ b/js/models/core/TxProposals.js @@ -1,174 +1,16 @@ 'use strict'; - -var imports = require('soop').imports(); +var BuilderMockV0 = require('./BuilderMockV0');; var bitcore = require('bitcore'); var util = bitcore.util; var Transaction = bitcore.Transaction; var BuilderMockV0 = require('./BuilderMockV0');; -var TransactionBuilder = bitcore.TransactionBuilder; +var TxProposal = require('./TxProposal');; var Script = bitcore.Script; +var Key = bitcore.Key; var buffertools = bitcore.buffertools; var preconditions = require('preconditions').instance(); -function TxProposal(opts) { - this.creator = opts.creator; - this.createdTs = opts.createdTs; - this.seenBy = opts.seenBy || {}; - this.signedBy = opts.signedBy || {}; - this.rejectedBy = opts.rejectedBy || {}; - this.builder = opts.builder; - this.sentTs = opts.sentTs || null; - this.sentTxid = opts.sentTxid || null; - this.inputChainPaths = opts.inputChainPaths || []; - this.comment = opts.comment || null; -} - -TxProposal.prototype.getID = function() { - return this.builder.build().getNormalizedHash().toString('hex'); -}; - -TxProposal.prototype.toObj = function() { - var o = JSON.parse(JSON.stringify(this)); - delete o['builder']; - o.builderObj = this.builder.toObj(); - return o; -}; - - -TxProposal.prototype.setSent = function(sentTxid) { - this.sentTxid = sentTxid; - this.sentTs = Date.now(); -}; - -TxProposal.fromObj = function(o, forceOpts) { - var t = new TxProposal(o); - - try { - // force opts is requested. - for (var k in forceOpts) { - o.builderObj.opts[k] = forceOpts[k]; - } - t.builder = TransactionBuilder.fromObj(o.builderObj); - - } catch (e) { - if (!o.version) { - t.builder = new BuilderMockV0(o.builderObj); - t.readonly = 1; - }; - } - - return t; -}; - - -TxProposal.prototype.isValid = function() { - if (this.builder.signhash && this.builder.signhash !== Transaction.SIGHASH_ALL) { - return false; - } - var tx = this.builder.build(); - for (var i = 0; i < tx.ins.length; i++) { - var hashType = tx.getHashType(i); - if (hashType && hashType !== Transaction.SIGHASH_ALL) { - return false; - } - } - return true; -}; - -TxProposal.getSentTs = function() { - return this.sentTs; -}; - -TxProposal.prototype.merge = function(other, author) { - var ret = {}; - ret.events = this.mergeMetadata(other, author); - ret.hasChanged = this.mergeBuilder(other); - return ret; -}; - -TxProposal.prototype.mergeBuilder = function(other) { - var b0 = this.builder; - var b1 = other.builder; - - // TODO: improve this comparison - var before = JSON.stringify(b0.toObj()); - b0.merge(b1); - var after = JSON.stringify(b0.toObj()); - return after !== before; -}; - -TxProposal.prototype.mergeMetadata = function(v1, author) { - var events = []; - var v0 = this; - - var ntxid = this.getID(); - - Object.keys(v1.seenBy).forEach(function(k) { - if (!v0.seenBy[k]) { - // TODO: uncomment below and change protocol to make this work - //if (k != author) throw new Error('Non authoritative seenBy change by ' + author); - v0.seenBy[k] = v1.seenBy[k]; - events.push({ - type: 'seen', - cId: k, - txId: ntxid - }); - } - }); - - Object.keys(v1.signedBy).forEach(function(k) { - if (!v0.signedBy[k]) { - // TODO: uncomment below and change protocol to make this work - //if (k != author) throw new Error('Non authoritative signedBy change by ' + author); - v0.signedBy[k] = v1.signedBy[k]; - events.push({ - type: 'signed', - cId: k, - txId: ntxid - }); - } - }); - - Object.keys(v1.rejectedBy).forEach(function(k) { - if (!v0.rejectedBy[k]) { - // TODO: uncomment below and change protocol to make this work - //if (k != author) throw new Error('Non authoritative rejectedBy change by ' + author); - v0.rejectedBy[k] = v1.rejectedBy[k]; - events.push({ - type: 'rejected', - cId: k, - txId: ntxid - }); - } - }); - - if (!v0.sentTxid && v1.sentTxid) { - v0.sentTs = v1.sentTs; - v0.sentTxid = v1.sentTxid; - events.push({ - type: 'broadcast', - txId: ntxid - }); - } - - return events; - -}; - -//This should be on bitcore / Transaction -TxProposal.prototype.countSignatures = function() { - var tx = this.builder.build(); - - var ret = 0; - for (var i in tx.ins) { - ret += tx.countInputSignatures(i); - } - return ret; -}; - -module.exports = require('soop')(TxProposal); - function TxProposals(opts) { opts = opts || {}; @@ -178,6 +20,7 @@ function TxProposals(opts) { this.txps = {}; } +// fromObj => from a trusted source TxProposals.fromObj = function(o, forceOpts) { var ret = new TxProposals({ networkName: o.networkName, @@ -187,7 +30,7 @@ TxProposals.fromObj = function(o, forceOpts) { o.txps.forEach(function(o2) { var t = TxProposal.fromObj(o2, forceOpts); if (t.builder) { - var id = t.getID(); + var id = t.getId(); ret.txps[id] = t; } }); @@ -198,14 +41,9 @@ TxProposals.prototype.getNtxids = function() { return Object.keys(this.txps); }; -TxProposals.prototype.toObj = function(onlyThisNtxid) { - if (onlyThisNtxid) throw new Error(); +TxProposals.prototype.toObj = function() { var ret = []; for (var id in this.txps) { - - if (onlyThisNtxid && id != onlyThisNtxid) - continue; - var t = this.txps[id]; if (!t.sent) ret.push(t.toObj()); @@ -217,50 +55,53 @@ TxProposals.prototype.toObj = function(onlyThisNtxid) { }; }; -TxProposals.prototype.merge = function(inTxp, author) { - var myTxps = this.txps; - var ntxid = inTxp.getID(); - var ret = {}; - ret.events = []; - ret.events.hasChanged = false; +TxProposals.prototype.merge = function(inObj, builderOpts) { + var incomingTx = TxProposal.fromUntrustedObj(inObj, builderOpts); + incomingTx._sync(); + + var myTxps = this.txps; + var ntxid = incomingTx.getId(); + var ret = { + ntxid: ntxid + }; if (myTxps[ntxid]) { - var v0 = myTxps[ntxid]; - var v1 = inTxp; - ret = v0.merge(v1, author); + + // Merge an existing txProposal + ret.hasChanged = myTxps[ntxid].merge(incomingTx); + + } else { - this.txps[ntxid] = inTxp; - ret.hasChanged = true; - ret.events.push({ - type: 'new', - cid: inTxp.creator, - tx: ntxid - }); + // Create a new one + ret.new = ret.hasChanged = 1; + this.txps[ntxid] = incomingTx; } + + ret.txp = this.txps[ntxid]; return ret; }; -TxProposals.prototype.add = function(data) { - preconditions.checkArgument(data.inputChainPaths); - preconditions.checkArgument(data.signedBy); - preconditions.checkArgument(data.creator); - preconditions.checkArgument(data.createdTs); - preconditions.checkArgument(data.builder); - var txp = new TxProposal(data); - var ntxid = txp.getID(); +// 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.setSent = function(ntxid, txid) { - //sent TxProposals are local an not broadcasted. - this.txps[ntxid].setSent(txid); + +TxProposals.prototype.get = function(ntxid) { + var ret = this.txps[ntxid]; + if (!ret) + throw new Error('Unknown TXP: '+ntxid); + + return ret; }; - TxProposals.prototype.getTxProposal = function(ntxid, copayers) { - var txp = this.txps[ntxid]; + var txp = this.get(ntxid); + var i = JSON.parse(JSON.stringify(txp)); i.builder = txp.builder; i.ntxid = ntxid; @@ -296,6 +137,17 @@ TxProposals.prototype.getTxProposal = function(ntxid, copayers) { return i; }; + +TxProposals.prototype.reject = function(ntxid, copayerId) { + var txp = this.get(ntxid); + txp.setRejected(copayerId); +}; + +TxProposals.prototype.seen = function(ntxid, copayerId) { + var txp = this.get(ntxid); + txp.setSeen(copayerId); +}; + //returns the unspent txid-vout used in PENDING Txs TxProposals.prototype.getUsedUnspent = function(maxRejectCount) { var ret = {}; @@ -312,5 +164,4 @@ TxProposals.prototype.getUsedUnspent = function(maxRejectCount) { return ret; }; -TxProposals.TxProposal = TxProposal; -module.exports = require('soop')(TxProposals); +module.exports = TxProposals; diff --git a/js/models/core/Wallet.js b/js/models/core/Wallet.js index db37355cf..55649c296 100644 --- a/js/models/core/Wallet.js +++ b/js/models/core/Wallet.js @@ -17,6 +17,7 @@ var Address = bitcore.Address; var HDParams = require('./HDParams'); var PublicKeyRing = require('./PublicKeyRing'); +var TxProposal = require('./TxProposal'); var TxProposals = require('./TxProposals'); var PrivateKey = require('./PrivateKey'); var copayConfig = require('../../../config'); @@ -36,7 +37,7 @@ function Wallet(opts) { }); if (copayConfig.forceNetwork && this.getNetworkName() !== copayConfig.networkName) throw new Error('Network forced to ' + copayConfig.networkName + - ' and tried to create a Wallet with network ' + this.getNetworkName()); + ' and tried to create a Wallet with network ' + this.getNetworkName()); this.log('creating ' + opts.requiredCopayers + ' of ' + opts.totalCopayers + ' wallet'); @@ -58,11 +59,11 @@ function Wallet(opts) { } -Wallet.builderOpts = { - lockTime: null, - signhash: bitcore.Transaction.SIGNHASH_ALL, - fee: null, - feeSat: null, +Wallet.builderOpts = { + lockTime: null, + signhash: bitcore.Transaction.SIGNHASH_ALL, + fee: null, + feeSat: null, }; Wallet.parent = EventEmitter; @@ -129,39 +130,158 @@ Wallet.prototype._handlePublicKeyRing = function(senderId, data, isInbound) { }; - -Wallet.prototype._handleTxProposal = function(senderId, data) { - this.log('RECV TXPROPOSAL: ', data); - var inTxp = TxProposals.TxProposal.fromObj(data.txProposal, Wallet.builderOpts); - - - - var valid = inTxp.isValid(); - if (!valid) { - var corruptEvent = { +Wallet.prototype._processProposalEvents = function(senderId, m) { + var ev; + if (m) { + if (m.new) { + ev = { + type: 'new', + cid: senderId + } + } else if (m.newCopayer) { + ev = { + type: 'signed', + cid: m.newCopayer + }; + } + } else { + ev = { type: 'corrupt', - cId: inTxp.creator + cId: senderId, }; - this.emit('txProposalEvent', corruptEvent); - return; - } - var mergeInfo = this.txProposals.merge(inTxp, senderId); - var added = this.addSeenToTxProposals(); - - if (added) { - this.log('### BROADCASTING txProposals with my seenBy updated.'); - this.sendTxProposal(inTxp.getID()); } - this.emit('txProposalsUpdated'); - this.store(); - - for (var i = 0; i < mergeInfo.events.length; i++) { - this.emit('txProposalEvent', mergeInfo.events[i]); - } + if (ev) + this.emit('txProposalEvent', ev); }; + + +/* OTDO + events.push({ +type: 'signed', +cId: k, +txId: ntxid +}); +*/ +Wallet.prototype._getKeyMap = function(txp) { + preconditions.checkArgument(txp); + + var keyMap = this.publicKeyRing.copayersForPubkeys(txp._inputSignatures[0], txp.inputChainPaths); + + var inSig = JSON.stringify(txp._inputSignatures[0].sort()); + + if (JSON.stringify(Object.keys(keyMap).sort()) !== inSig) { + throw new Error('inputSignatures dont match know copayers pubkeys'); + } + + var keyMapStr = JSON.stringify(keyMap); + // All inputs must be signed with the same copayers + for (var i in txp._inputSignatures) { + if (!i) continue; + var inSigX = JSON.stringify(txp._inputSignatures[i].sort()); + if (inSigX !== inSig) + throw new Error('found inputs with different signatures:'); + } + return keyMap; +}; + + +Wallet.prototype._checkSentTx = function(ntxid, cb) { + var txp = this.txProposals.get(ntxid); + var tx = txp.builder.build(); + + this.blockchain.checkSentTx(tx, function(err, txid) { + var ret = false; + if (txid) { + txp.setSent(txid); + ret = txid; + } + return cb(ret); + }); +}; + + +Wallet.prototype._handleTxProposal = function(senderId, data) { + var self = this; + this.log('RECV TXPROPOSAL: ', data); + var m; + + try { + m = this.txProposals.merge(data.txProposal, Wallet.builderOpts); + var keyMap = this._getKeyMap(m.txp); + ret.newCopayer = m.txp.setCopayers(senderId, keyMap); + + } catch (e) { + this.log('Corrupt TX proposal received from:', senderId, e); + } + + if (m) { + + if (m.hasChanged) { + this.sendSeen(m.ntxid); + var tx = m.txp.builder.build(); + if (tx.isComplete()) { + this._checkSentTx(m.ntxid, function(ret) { + if (ret) { + self.emit('txProposalsUpdated'); + self.store(); + } + }); + } else { + this.sendTxProposal(m.ntxid); + } + } + this.emit('txProposalsUpdated'); + this.store(); + } + this._processProposalEvents(senderId, m); +}; + + +Wallet.prototype._handleReject = function(senderId, data, isInbound) { + preconditions.checkState(data.ntxid); + this.log('RECV REJECT:', data); + + var txp = this.txProposals.get(data.ntxid); + + if (!txp) + throw new Error('Received Reject for an unknown TX from:' + senderId); + + if (txp.signedBy[senderId]) + throw new Error('Received Reject for an already signed TX from:' + senderId); + + txp.setRejected(senderId); + this.store(); + + this.emit('txProposalsUpdated'); + this.emit('txProposalEvent', { + type: 'rejected', + cId: senderId, + txId: data.ntxid, + }); +}; + +Wallet.prototype._handleSeen = function(senderId, data, isInbound) { + preconditions.checkState(data.ntxid); + this.log('RECV SEEN:', data); + + var txp = this.txProposals.get(data.ntxid); + txp.setSeen(senderId); + this.store(); + this.emit('txProposalsUpdated'); + this.emit('txProposalEvent', { + type: 'seen', + cId: senderId, + txId: data.ntxid, + }); + +}; + + + Wallet.prototype._handleAddressBook = function(senderId, data, isInbound) { + preconditions.checkState(data.addressBook); this.log('RECV ADDRESSBOOK:', data); var rcv = data.addressBook; var hasChange; @@ -193,24 +313,30 @@ Wallet.prototype._handleData = function(senderId, data, isInbound) { // This handler is repeaded on WalletFactory (#join). TODO case 'walletId': this.sendWalletReady(senderId); - break; + break; case 'walletReady': this.sendPublicKeyRing(senderId); - this.sendAddressBook(senderId); - this.sendAllTxProposals(senderId); // send old txps - break; + this.sendAddressBook(senderId); + this.sendAllTxProposals(senderId); // send old txps + break; case 'publicKeyRing': this._handlePublicKeyRing(senderId, data, isInbound); - break; + break; + case 'reject': + this._handleReject(senderId, data, isInbound); + break; + case 'seen': + this._handleSeen(senderId, data, isInbound); + break; case 'txProposal': this._handleTxProposal(senderId, data, isInbound); - break; + break; case 'indexes': this._handleIndexes(senderId, data, isInbound); - break; + break; case 'addressbook': this._handleAddressBook(senderId, data, isInbound); - break; + break; } }; @@ -384,6 +510,7 @@ Wallet.prototype.toObj = function() { return walletObj; }; +// fromObj => from a trusted source Wallet.fromObj = function(o, storage, network, blockchain) { var opts = JSON.parse(JSON.stringify(o.opts)); opts.addressBook = o.addressBook; @@ -418,11 +545,31 @@ Wallet.prototype.sendAllTxProposals = function(recipients) { Wallet.prototype.sendTxProposal = function(ntxid, recipients) { preconditions.checkArgument(ntxid); - preconditions.checkState(this.txProposals.txps[ntxid]); + this.log('### SENDING txProposal ' + ntxid + ' TO:', recipients || 'All', this.txProposals); this.send(recipients, { type: 'txProposal', - txProposal: this.txProposals.txps[ntxid].toObj(), + txProposal: this.txProposals.get(ntxid).toObjTrim(), + walletId: this.id, + }); +}; + +Wallet.prototype.sendSeen = function(ntxid) { + preconditions.checkArgument(ntxid); + this.log('### SENDING seen: ' + ntxid + ' TO: All'); + this.send(null, { + type: 'seen', + ntxid: ntxid, + walletId: this.id, + }); +}; + +Wallet.prototype.sendReject = function(ntxid) { + preconditions.checkArgument(ntxid); + this.log('### SENDING reject: ' + ntxid + ' TO: All'); + this.send(null, { + type: 'reject', + ntxid: ntxid, walletId: this.id, }); }; @@ -517,30 +664,22 @@ Wallet.prototype.getTxProposals = function() { Wallet.prototype.reject = function(ntxid) { - var myId = this.getMyCopayerId(); - var txp = this.txProposals.txps[ntxid]; - if (!txp || txp.rejectedBy[myId] || txp.signedBy[myId]) { - throw new Error('Invalid transaction to reject: ' + ntxid); - } - - txp.rejectedBy[myId] = Date.now(); - this.sendTxProposal(ntxid); + var txp = this.txProposals.reject(ntxid, this.getMyCopayerId()); + this.sendReject(ntxid); this.store(); this.emit('txProposalsUpdated'); }; - - Wallet.prototype.sign = function(ntxid, cb) { preconditions.checkState(typeof this.getMyCopayerId() !== 'undefined'); var self = this; setTimeout(function() { var myId = self.getMyCopayerId(); - var txp = self.txProposals.txps[ntxid]; - if (!txp || txp.rejectedBy[myId] || txp.signedBy[myId]) { - if (cb) cb(false); - } - + var txp = self.txProposals.get(ntxid); + // if (!txp || txp.rejectedBy[myId] || txp.signedBy[myId]) { + // if (cb) cb(false); + // } + // var keys = self.privateKey.getForPaths(txp.inputChainPaths); var b = txp.builder; @@ -559,14 +698,13 @@ Wallet.prototype.sign = function(ntxid, cb) { }, 10); }; + Wallet.prototype.sendTx = function(ntxid, cb) { - var txp = this.txProposals.txps[ntxid]; - if (!txp) return; - + var txp = this.txProposals.get(ntxid); var tx = txp.builder.build(); - if (!tx.isComplete()) return; + if (!tx.isComplete()) + throw new Error('Tx is not complete. Can not broadcast'); this.log('Broadcasting Transaction'); - var scriptSig = tx.ins[0].getScript(); var size = scriptSig.serialize().length; @@ -577,28 +715,23 @@ Wallet.prototype.sendTx = function(ntxid, cb) { this.blockchain.sendRawTransaction(txHex, function(txid) { self.log('BITCOIND txid:', txid); if (txid) { - self.txProposals.setSent(ntxid, txid); + self.txProposals.get(ntxid).setSent(txid); self.sendTxProposal(ntxid); self.store(); + return cb(txid); + } else { + self.log('Sent failed. Checking is the TX was sent already'); + self._checkSentTx(ntxid, function(txid) { + console.log('[Wallet.js.730:txid:]', txid); //TODO + if (txid) + self.store(); + + return cb(txid); + }); } - return cb(txid); }); }; -Wallet.prototype.addSeenToTxProposals = function() { - var ret = false; - var myId = this.getMyCopayerId(); - - for (var k in this.txProposals.txps) { - var txp = this.txProposals.txps[k]; - if (!txp.seenBy[myId]) { - - txp.seenBy[myId] = Date.now(); - ret = true; - } - } - return ret; -}; // TODO: remove this method and use getAddressesInfo everywhere Wallet.prototype.getAddresses = function(opts) { @@ -719,8 +852,9 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos var priv = this.privateKey; opts = opts || {}; - preconditions.checkArgument(new Address(toAddress).network().name === this.getNetworkName()); - preconditions.checkState(pkr.isComplete()); + preconditions.checkArgument(new Address(toAddress).network().name === this.getNetworkName(), 'networkname mismatch'); + preconditions.checkState(pkr.isComplete(), 'pubkey ring incomplete'); + preconditions.checkState(priv, 'no private key'); if (comment) preconditions.checkArgument(comment.length <= 100); if (!opts.remainderOut) { @@ -729,16 +863,16 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos }; } - for (var k in Wallet.builderOpts){ + for (var k in Wallet.builderOpts) { opts[k] = Wallet.builderOpts[k]; } var b = new Builder(opts) - .setUnspent(utxos) - .setOutputs([{ - address: toAddress, - amountSatStr: amountSatStr, - }]); + .setUnspent(utxos) + .setOutputs([{ + address: toAddress, + amountSatStr: amountSatStr, + }]); var selectedUtxos = b.getSelectedUnspent(); var inputChainPaths = selectedUtxos.map(function(utxo) { @@ -747,22 +881,23 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos b = b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths)); - if (priv) { - var keys = priv.getForPaths(inputChainPaths); - var signed = b.sign(keys); - } + var keys = priv.getForPaths(inputChainPaths); + var signed = b.sign(keys); var myId = this.getMyCopayerId(); var now = Date.now(); - var me = {}; var tx = b.build(); - if (priv && tx.countInputSignatures(0)) me[myId] = now; + if (!tx.countInputSignatures(0)) + throw new Error('Could not sign generated tx'); + + var me = {}; + me[myId] = now; var meSeen = {}; if (priv) meSeen[myId] = now; - var data = { + var ntxid = this.txProposals.add(new TxProposal({ inputChainPaths: inputChainPaths, signedBy: me, seenBy: meSeen, @@ -770,9 +905,7 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos createdTs: now, builder: b, comment: comment - }; - - var ntxid = this.txProposals.add(data); + })); return ntxid; }; @@ -831,29 +964,29 @@ Wallet.prototype.indexDiscovery = function(start, change, cosigner, gap, cb) { var self = this; async.doWhilst( function _do(next) { - // Optimize window to minimize the derivations. - var scanWindow = (lastActive == -1) ? gap : gap - (scanIndex - lastActive) + 1; - var addresses = self.deriveAddresses(scanIndex, scanWindow, change, cosigner); - self.blockchain.checkActivity(addresses, function(err, actives) { - if (err) throw err; + // Optimize window to minimize the derivations. + var scanWindow = (lastActive == -1) ? gap : gap - (scanIndex - lastActive) + 1; + var addresses = self.deriveAddresses(scanIndex, scanWindow, change, cosigner); + self.blockchain.checkActivity(addresses, function(err, actives) { + if (err) throw err; - // Check for new activities in the newlly scanned addresses - var recentActive = actives.reduce(function(r, e, i) { - return e ? scanIndex + i : r; - }, lastActive); - hasActivity = lastActive != recentActive; - lastActive = recentActive; - scanIndex += scanWindow; - next(); - }); - }, - function _while() { - return hasActivity; - }, - function _finnaly(err) { - if (err) return cb(err); - cb(null, lastActive); - } + // Check for new activities in the newlly scanned addresses + var recentActive = actives.reduce(function(r, e, i) { + return e ? scanIndex + i : r; + }, lastActive); + hasActivity = lastActive != recentActive; + lastActive = recentActive; + scanIndex += scanWindow; + next(); + }); + }, + function _while() { + return hasActivity; + }, + function _finnaly(err) { + if (err) return cb(err); + cb(null, lastActive); + } ); } @@ -913,6 +1046,10 @@ Wallet.prototype.toggleAddressBookEntry = function(key) { this.store(); }; +Wallet.prototype.isShared = function() { + return this.totalCopayers > 1; +} + Wallet.prototype.isReady = function() { var ret = this.publicKeyRing.isComplete() && this.publicKeyRing.isFullyBackup(); return ret; diff --git a/js/models/core/WalletFactory.js b/js/models/core/WalletFactory.js index f689c1c6a..cc56cd976 100644 --- a/js/models/core/WalletFactory.js +++ b/js/models/core/WalletFactory.js @@ -102,10 +102,7 @@ WalletFactory.prototype.read = function(walletId) { WalletFactory.prototype.create = function(opts) { opts = opts || {}; - this.log('### CREATING NEW WALLET.' + - (opts.id ? ' USING ID: ' + opts.id : ' NEW ID') + - (opts.privateKey ? ' USING PrivateKey: ' + opts.privateKey.getId() : ' NEW PrivateKey') - ); + this.log('### CREATING NEW WALLET.' + (opts.id ? ' USING ID: ' + opts.id : ' NEW ID') + (opts.privateKey ? ' USING PrivateKey: ' + opts.privateKey.getId() : ' NEW PrivateKey')); opts.privateKey = opts.privateKey || new PrivateKey({ networkName: this.networkName @@ -121,7 +118,8 @@ WalletFactory.prototype.create = function(opts) { }); opts.publicKeyRing.addCopayer( opts.privateKey.deriveBIP45Branch().extendedPublicKeyString(), - opts.nickname); + opts.nickname + ); this.log('\t### PublicKeyRing Initialized'); opts.txProposals = opts.txProposals || new TxProposals({ @@ -143,6 +141,7 @@ WalletFactory.prototype.create = function(opts) { opts.version = opts.version || this.version; var w = new Wallet(opts); w.store(); + this.storage.setLastOpened(w.id); return w; }; @@ -156,9 +155,9 @@ WalletFactory.prototype._checkVersion = function(inVersion) { //We only check for major version differences if (thisV0 < inV0) { throw new Error('Major difference in software versions' + - '. Received:' + inVersion + - '. Current version:' + this.version + - '. Aborting.'); + '. Received:' + inVersion + + '. Current version:' + this.version + + '. Aborting.'); } }; @@ -179,6 +178,8 @@ WalletFactory.prototype.open = function(walletId, opts) { if (w) { w.store(); } + + this.storage.setLastOpened(walletId); return w; }; @@ -194,6 +195,7 @@ WalletFactory.prototype.delete = function(walletId, cb) { var s = this.storage; this.log('## DELETING WALLET ID:' + walletId); //TODO s.deleteWallet(walletId); + s.setLastOpened(undefined); return cb(); }; diff --git a/js/models/storage/LocalEncrypted.js b/js/models/storage/LocalEncrypted.js index dd5a0f6d7..df9c9c703 100644 --- a/js/models/storage/LocalEncrypted.js +++ b/js/models/storage/LocalEncrypted.js @@ -172,6 +172,13 @@ Storage.prototype.deleteWallet = function(walletId) { } }; +Storage.prototype.setLastOpened = function(walletId) { + this.setGlobal('lastOpened', walletId); +} + +Storage.prototype.getLastOpened = function() { + return this.getGlobal('lastOpened'); +} //obj contains keys to be set Storage.prototype.setFromObj = function(walletId, obj) { diff --git a/js/services/controllerUtils.js b/js/services/controllerUtils.js index 49c9840e1..50c51c4af 100644 --- a/js/services/controllerUtils.js +++ b/js/services/controllerUtils.js @@ -277,17 +277,28 @@ angular.module('copayApp.services') i.outs = outs; i.fee = i.builder.feeSat * satToUnit; i.missingSignatures = tx.countInputMissingSignatures(0); + i.actionList = getActionList(i.peerActions); txs.push(i); } }); - $rootScope.txs = txs; //.some(function(i) {return i.isPending; } ); + $rootScope.txs = txs; if ($rootScope.pendingTxCount < pendingForUs) { $rootScope.txAlertCount = pendingForUs; } $rootScope.pendingTxCount = pendingForUs; }; + function getActionList(actions) { + var peers = Object.keys(actions).map(function(i) { + return {cId: i, actions: actions[i] } + }); + + return peers.sort(function(a, b) { + return !!b.actions.create - !!a.actions.create; + }); + } + $rootScope.$watch('insightError', function(status) { if (status) { if (status === -1) { diff --git a/test/mocks/FakeBlockchain.js b/test/mocks/FakeBlockchain.js index d47b44af6..e4c3997bc 100644 --- a/test/mocks/FakeBlockchain.js +++ b/test/mocks/FakeBlockchain.js @@ -1,6 +1,5 @@ 'use strict'; -var imports = require('soop').imports(); var bitcore = require('bitcore'); function FakeBlockchain(opts) { @@ -47,4 +46,4 @@ FakeBlockchain.prototype.sendRawTransaction = function(rawtx, cb) { return cb(txid); }; -module.exports = require('soop')(FakeBlockchain); +module.exports = FakeBlockchain; diff --git a/test/mocks/FakeBuilder.js b/test/mocks/FakeBuilder.js new file mode 100644 index 000000000..ace53be70 --- /dev/null +++ b/test/mocks/FakeBuilder.js @@ -0,0 +1,51 @@ +'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.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], + } +} + + +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/test/mocks/FakeStorage.js b/test/mocks/FakeStorage.js index 9ea94c1b0..c7d86f3c1 100644 --- a/test/mocks/FakeStorage.js +++ b/test/mocks/FakeStorage.js @@ -19,6 +19,13 @@ FakeStorage.prototype.getGlobal = function(id) { return this.storage[id]; }; +FakeStorage.prototype.setLastOpened = function(val) { + this.storage['lastOpened'] = val; +}; + +FakeStorage.prototype.getLastOpened = function() { + return this.storage['lastOpened']; +}; FakeStorage.prototype.removeGlobal = function(id) { delete this.storage[id]; diff --git a/test/mocks/FakeWallet.js b/test/mocks/FakeWallet.js index 945e584e9..bdca60a5c 100644 --- a/test/mocks/FakeWallet.js +++ b/test/mocks/FakeWallet.js @@ -46,6 +46,10 @@ FakeWallet.prototype.getAddressesInfo = function() { return ret; }; +FakeWallet.prototype.isShared = function() { + return this.totalCopayers > 1; +} + FakeWallet.prototype.isReady = function() { return true; } diff --git a/test/test.HDPath.js b/test/test.HDPath.js index 500ba348d..1bf5bf0b0 100644 --- a/test/test.HDPath.js +++ b/test/test.HDPath.js @@ -70,9 +70,9 @@ describe('HDPath model', function() { ].forEach(function(datum) { var path = datum[0]; var result = datum[1]; - it('should get the correct indices for path ' + path, function() { - var i = HDPath.indicesForPath(path); - i.index.should.equal(result.index); + it('should get the correct indexes for path ' + path, function() { + var i = HDPath.indexesForPath(path); + i.addressIndex.should.equal(result.index); i.isChange.should.equal(result.isChange); }); }); diff --git a/test/test.PublicKeyRing.js b/test/test.PublicKeyRing.js index 41d874676..0dbbd447c 100644 --- a/test/test.PublicKeyRing.js +++ b/test/test.PublicKeyRing.js @@ -13,11 +13,20 @@ try { } catch (e) { var copay = require('../copay'); //node } +var PrivateKey = copay.PrivateKey; var PublicKeyRing = copay.PublicKeyRing; var aMasterPubKey = 'tprv8ZgxMBicQKsPdSVTiWXEqCCzqRaRr9EAQdn5UVMpT9UHX67Dh1FmzEMbavPumpAicsUm2XvC6NTdcWB89yN5DUWx5HQ7z3KByUg7Ht74VRZ'; +var getNewEpk = function() { + return new PrivateKey({ + networkName: 'livenet', + }) + .deriveBIP45Branch() + .extendedPublicKeyString(); +} + var createW = function(networkName) { var config = { networkName: networkName || 'livenet', @@ -29,8 +38,8 @@ var createW = function(networkName) { var copayers = []; for (var i = 0; i < 5; i++) { w.isComplete().should.equal(false); - w.remainingCopayers().should.equal(5-i); - var newEpk = w.addCopayer(); + w.remainingCopayers().should.equal(5 - i); + var newEpk = w.addCopayer(getNewEpk()); copayers.push(newEpk); } w.isComplete().should.equal(true); @@ -43,6 +52,14 @@ var createW = function(networkName) { }; }; +var cachedW; +var getCachedW = function() { + if (!cachedW) { + cachedW = createW(); + } + return cachedW; +}; + describe('PublicKeyRing model', function() { it('should create an instance (livenet)', function() { @@ -78,7 +95,7 @@ describe('PublicKeyRing model', function() { }); it('should add and check when adding shared pub keys', function() { - var k = createW(); + var k = getCachedW(); var w = k.w; var copayers = k.copayers; @@ -92,7 +109,7 @@ describe('PublicKeyRing model', function() { }); it('should be able to to store and read', function() { - var k = createW(); + var k = getCachedW(); var w = k.w; var copayers = k.copayers; var changeN = 2; @@ -124,10 +141,10 @@ describe('PublicKeyRing model', function() { it('should generate some p2sh addresses', function() { - var k = createW(); + var k = getCachedW(); var w = k.w; - [true, false].forEach(function(isChange){ + [true, false].forEach(function(isChange) { for (var i = 0; i < 2; i++) { var a = w.generateAddress(isChange, k.pub); a.isValid().should.equal(true); @@ -148,7 +165,7 @@ describe('PublicKeyRing model', function() { var a = w.getAddresses(); a.length.should.equal(1); - [true, false].forEach(function(isChange){ + [true, false].forEach(function(isChange) { for (var i = 0; i < 2; i++) { w.generateAddress(isChange, k.pub); } @@ -185,18 +202,12 @@ describe('PublicKeyRing model', function() { }); it('should set backup ready', function() { - var w = createW().w; + var w = getCachedW().w; w.isBackupReady().should.equal(false); w.setBackupReady(); w.isBackupReady().should.equal(true); }); - it('should set backup ready', function() { - var w = createW().w; - w.isBackupReady().should.equal(false); - w.setBackupReady(); - w.isBackupReady().should.equal(true); - }); it('should check for other backups', function() { var w = createW().w; @@ -213,7 +224,7 @@ describe('PublicKeyRing model', function() { }); it('should merge backup', function() { - var w = createW().w; + var w = getCachedW().w; w.copayersBackup = ["a", "b"]; var hasChanged = w.mergeBackups(["b", "c"]); @@ -313,11 +324,10 @@ describe('PublicKeyRing model', function() { var w0 = new PublicKeyRing({ networkName: 'livenet', }); - w0.addCopayer(); - w0.addCopayer(); - w0.addCopayer(); - w0.addCopayer(); - w0.addCopayer(); + + for (var i = 0; i < 5; i++) + w0.addCopayer(getNewEpk()); + (function() { w0.merge(w); }).should.throw(); @@ -327,7 +337,7 @@ describe('PublicKeyRing model', function() { var wx = new PublicKeyRing({ networkName: 'livenet', }); - wx.addCopayer(); + wx.addCopayer(getNewEpk()); (function() { w.merge(wx); }).should.throw(); @@ -343,7 +353,7 @@ describe('PublicKeyRing model', function() { var copayers = []; for (var i = 0; i < 2; i++) { w.isComplete().should.equal(false); - w.addCopayer(); + w.addCopayer(getNewEpk()); } var w2 = new PublicKeyRing({ @@ -354,7 +364,7 @@ describe('PublicKeyRing model', function() { var copayers = []; for (var i = 0; i < 3; i++) { w2.isComplete().should.equal(false); - w2.addCopayer(); + w2.addCopayer(getNewEpk()); } w2.merge(w).should.equal(true); w2.isComplete().should.equal(true); @@ -379,7 +389,7 @@ describe('PublicKeyRing model', function() { networkName: 'livenet', id: w.id, }); - w2.addCopayer(); + w2.addCopayer(getNewEpk()); w.merge(w2).should.equal(true); } w.isComplete().should.equal(true); @@ -393,7 +403,7 @@ describe('PublicKeyRing model', function() { var w = new PublicKeyRing(config); should.exist(w); for (var i = 0; i < 3; i++) { - w.addCopayer(); + w.addCopayer(getNewEpk()); }; w._setNicknameForIndex(0, 'pepe0'); w._setNicknameForIndex(1, 'pepe1'); @@ -409,7 +419,7 @@ describe('PublicKeyRing model', function() { networkName: 'livenet', id: w.id, }); - w2.addCopayer(); + w2.addCopayer(getNewEpk()); w2._setNicknameForIndex(0, 'juan' + i); w.merge(w2).should.equal(true); } @@ -430,7 +440,7 @@ describe('PublicKeyRing model', function() { var w = new PublicKeyRing(config); should.exist(w); for (var i = 0; i < 3; i++) { - w.addCopayer(null, 'tito' + i); + w.addCopayer(getNewEpk(), 'tito' + i); }; w.nicknameForIndex(0).should.equal('tito0'); w.nicknameForIndex(1).should.equal('tito1'); @@ -468,7 +478,7 @@ describe('PublicKeyRing model', function() { }); it('#getRedeemScriptMap check tests', function() { - var k = createW(); + var k = getCachedW(); var w = k.w; var amount = 2; @@ -489,4 +499,27 @@ describe('PublicKeyRing model', function() { }); }); + it('#getForPath should return 5 pubkeys', function() { + var w = getCachedW().w; + var pubkeys = w.getForPath('m/45\'/2147483647/1/0'); + pubkeys.length.should.equal(5); + }); + + it('#getForPaths should return 2 arrays of 5 pubkey ', function() { + var w = getCachedW().w; + var pubkeys = w.getForPaths(['m/45\'/2147483647/1/0', 'm/45\'/2147483647/1/1']); + pubkeys.length.should.equal(2); + pubkeys[0].length.should.equal(5); + pubkeys[1].length.should.equal(5); + }); + + it('#forPaths should return copayers and pubkeys ', function() { + var w = getCachedW().w; + var ret = w.forPaths(['m/45\'/2147483647/1/0', 'm/45\'/2147483647/1/1']); + ret.copayerIds.length.should.equal(5); + ret.pubKeys.length.should.equal(2); + ret.pubKeys[0].length.should.equal(5); + ret.pubKeys[1].length.should.equal(5); + }); + }); diff --git a/test/test.TxProposal.js b/test/test.TxProposal.js new file mode 100644 index 000000000..3ac2f310d --- /dev/null +++ b/test/test.TxProposal.js @@ -0,0 +1,431 @@ +'use strict'; + +var chai = chai || require('chai'); +var should = chai.should(); +var bitcore = bitcore || require('bitcore'); +var Transaction = bitcore.Transaction; +var buffertools = bitcore.buffertools; +var WalletKey = bitcore.WalletKey; +var Key = bitcore.Key; +var bignum = bitcore.Bignum; +var Script = bitcore.Script; +var TransactionBuilder = bitcore.TransactionBuilder; +var util = bitcore.util; +var networks = bitcore.networks; +var sinon = require('sinon'); +try { + var copay = require('copay'); //browser +} catch (e) { + var copay = require('../copay'); //node +} + +var FakeBuilder = require('./mocks/FakeBuilder'); +var TxProposal = copay.TxProposal; + +var dummyProposal = new TxProposal({ + creator: 1, + createdTs: 1, + builder: new FakeBuilder(), + inputChainPaths: ['m/1'], +}); + +var someKeys = ["03b39d61dc9a504b13ae480049c140dcffa23a6cc9c09d12d6d1f332fee5e18ca5", "022929f515c5cf967474322468c3bd945bb6f281225b2c884b465680ef3052c07e"]; + +describe('TxProposal', function() { + describe('new', function() { + it('should fail to create an instance with wrong arguments', function() { + + (function() { + var txp = new TxProposal(); + }).should.throw('Illegal Argument'); + + (function() { + var txp = new TxProposal({ + creator: 1 + }); + }).should.throw('no inputChainPaths'); + + }); + + + it('should create an instance', function() { + var txp = new TxProposal({ + creator: 1, + createdTs: 1, + builder: new FakeBuilder(), + inputChainPaths: 'm/1', + }); + should.exist(txp); + + txp.creator.should.equal(1); + should.exist(txp.builder); + txp.inputChainPaths.should.equal('m/1'); + }); + }); + describe('#getId', function() { + it('should return id', function() { + var b = new FakeBuilder(); + var spy = sinon.spy(b.tx, 'getNormalizedHash'); + var txp = new TxProposal({ + creator: 1, + createdTs: 1, + builder: b, + inputChainPaths: 'm/1', + }); + txp.getId().should.equal('123456');; + sinon.assert.callCount(spy, 1); + }); + }); + describe('#toObj', function() { + it('should return an object and remove builder', function() { + var b = new FakeBuilder(); + var txp = new TxProposal({ + creator: 1, + createdTs: 1, + builder: b, + inputChainPaths: 'm/1', + }); + var o = txp.toObj(); + should.exist(o); + o.creator.should.equal(1); + should.not.exist(o.builder); + should.exist(o.builderObj); + }); + it('toObjTrim', function() { + var b = new FakeBuilder(); + var txp = new TxProposal({ + creator: 1, + createdTs: 1, + builder: b, + inputChainPaths: 'm/1', + comment: 'hola', + }); + var o = txp.toObjTrim(); + should.exist(o); + should.not.exist(o.creator); + should.not.exist(o.builder); + should.exist(o.comment); + should.exist(o.builderObj); + }); + + }); + describe('#fromObj', function() { + it.skip('should create from Object', function() { + var b = new FakeBuilder(); + var txp = TxProposal.fromObj({ + creator: 1, + createdTs: 1, + builderObj: b.toObj(), + inputChainPaths: ['m/1'], + }); + should.exist(txp); + }); + + + it('should fail to create from wrong object', function() { + var b = new FakeBuilder(); + (function() { + var txp = TxProposal.fromObj({ + creator: 1, + createdTs: 1, + builderObj: b.toObj(), + inputChainPaths: ['m/1'], + }); + }).should.throw('Invalid'); + }); + + + }); + + describe('#setSent', function() { + it('should set txid and timestamp', function() { + var now = Date.now(); + var txp = dummyProposal; + txp.setSent('3a42'); + txp.sentTs.should.gte(now); + txp.sentTxid.should.equal('3a42'); + }); + }); + + + describe('Signature verification', function() { + var validScriptSig = new bitcore.Script(FakeBuilder.VALID_SCRIPTSIG_BUF); + + var pubkeys = [ + '03197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d', + '0380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127', + '0392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed03', + '03a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e3', + '03e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e4' + ].map(function(hex) { + return new Buffer(hex, 'hex'); + }); + var keyBuf = someKeys.map(function(hex) { + return new Buffer(hex, 'hex'); + }); + it('#_formatKeys', function() { + (function() { + TxProposal._formatKeys(someKeys); + }).should.throw('buffers'); + var res = TxProposal._formatKeys(keyBuf); + }); + it('#_verifyScriptSig arg checks', function() { + (function() { + TxProposal._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); + }); + 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()); + ret.should.deep.equal([0, 3]); + }); + it('#_infoFromRedeemScript', function() { + var info = TxProposal._infoFromRedeemScript(validScriptSig); + var keys = info.keys; + keys.length.should.equal(5); + for (var i in keys) { + keys[i].toString('hex').should.equal(pubkeys[i].toString('hex')); + } + Buffer.isBuffer(info.script.getBuffer()).should.equal(true); + }); + it('#_updateSignedBy', function() { + var txp = dummyProposal; + txp._inputSignatures.should.deep.equal([ + ['03197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d', '03a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e3'] + ]); + }); + describe('#_check', function() { + var txp = dummyProposal; + var backup = txp.builder.tx.ins; + + it('OK', function() { + txp._check(); + }); + it('FAIL ins', function() { + txp.builder.tx.ins = []; + (function() { + txp._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); + (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); + (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); + (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; + (function() { + txp._check(); + }).should.throw('no signatures'); + txp.builder.tx.ins[0].s = backup; + }); + }); + describe('#merge', function() { + var txp = dummyProposal; + var backup = txp.builder.tx.ins; + it('with self', function() { + var hasChanged = txp.merge(txp); + hasChanged.should.equal(false); + }); + + it('with less signatures', function() { + var backup = txp.builder.vanilla.scriptSig[0]; + txp.builder.merge = function() { + // 2 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); + + txp.builder.vanilla.scriptSig = [backup]; + txp.builder.tx.ins[0].s = new Buffer(backup, 'hex'); + }); + + + it('with more signatures', function() { + txp.builder.merge = function() { + // 3 signatures. + this.vanilla.scriptSig = ['00483045022100f75bd3eb92d8c9be9a94d848bbd1985fc0eaf4c47fb470a0b222881802a1f03802204eb239ae3082779b1ec4f2e69baa0362494071e707e1696c14ad23c8f2e184e20148304502201981482db0f369ce943293b6fec06a0347918663c766a79d4cbd0457801768d1022100aedf8d7c51d55a9ddbdcc0067ed6b648b77ce9660447bbcf4e2c209698efa0a30148304502203f0ddad47757f8705cb40e7c706590d2e2028a7027ffdb26dd208fd6155e0d28022100ccd206f9b969ab7f88ee4c5c6cee48c800a62dda024c5a8de7eb8612b833a0c0014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae']; + this.tx.ins[0].s = new Buffer(this.vanilla.scriptSig[0], 'hex'); + }; + var hasChanged = txp.merge(txp); + hasChanged.should.equal(true); + }); + }); + describe('#setCopayers', function() { + it("should fails if Tx has no creator", function() { + var txp = dummyProposal; + txp.signedBy = { + 'hugo': 1 + }; + delete txp['creator']; + (function() { + txp.setCopayers('juan', { + pk1: 'pepe' + }) + }).should.throw('no creator'); + }); + it("should fails if Tx is not signed by creator", function() { + var txp = dummyProposal; + txp.creator = 'creator'; + txp.signedBy = { + 'hugo': 1 + }; + txp._inputSignatures = [ + ['pkX'] + ]; + (function() { + txp.setCopayers('juan', { + pk1: 'pepe' + }) + }).should.throw('creator'); + }); + + + it("should fails if Tx has unmapped signatures", function() { + var txp = dummyProposal; + txp.creator = 'creator'; + txp.signedBy = { + creator: 1 + }; + txp._inputSignatures = [ + ['pk0', 'pkX'] + ]; + (function() { + txp.setCopayers('juan', { + pk1: 'pepe' + }) + }).should.throw('unknown sig'); + }); + + // This was disabled. Unnecessary to check this. + it.skip("should be signed by sender", function() { + var txp = dummyProposal; + var ts = Date.now(); + txp._inputSignatures = [ + ['pk1', 'pk0'] + ]; + txp.signedBy = { + 'creator': Date.now() + }; + (function() { + txp.setCopayers('juan', { + pk0: 'creator', + pk1: 'pepe', + pk2: 'john' + }) + }).should.throw('senders sig'); + }); + + + it("should set signedBy (trivial case)", function() { + var txp = dummyProposal; + var ts = Date.now(); + txp._inputSignatures = [ + ['pk1', 'pk0'] + ]; + txp.signedBy = { + 'creator': Date.now() + }; + txp.setCopayers('pepe', { + pk0: 'creator', + pk1: 'pepe', + pk2: 'john' + }) + Object.keys(txp.signedBy).length.should.equal(2); + txp.signedBy['pepe'].should.gte(ts); + txp.signedBy['creator'].should.gte(ts); + }); + it("should assign creator", function() { + var txp = dummyProposal; + var ts = Date.now(); + txp._inputSignatures = [ + ['pk0'] + ]; + txp.signedBy = {}; + delete txp['creator']; + delete txp['creatorTs']; + txp.setCopayers('creator', { + pk0: 'creator', + pk1: 'pepe', + pk2: 'john' + }) + Object.keys(txp.signedBy).length.should.equal(1); + txp.creator.should.equal('creator'); + txp.createdTs.should.gte(ts); + txp.seenBy['creator'].should.equal(txp.createdTs); + }) + it("New tx should have only 1 signature", function() { + var txp = dummyProposal; + var ts = Date.now(); + txp.signedBy = {}; + delete txp['creator']; + delete txp['creatorTs']; + txp._inputSignatures = [ + ['pk0', 'pk1'] + ]; + (function() { + txp.setCopayers( + 'creator', { + 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._inputSignatures = [ + ['pk0', 'pk1'] + ]; + txp.creator = 'creator'; + txp.signedBy = { + 'creator': 1 + }; + txp.setCopayers('pepe', { + pk0: 'creator', + pk1: 'pepe', + pk2: 'john' + }) + Object.keys(txp.signedBy).length.should.equal(2); + txp.creator.should.equal('creator'); + txp.signedBy['creator'].should.equal(1); + txp.signedBy['pepe'].should.gte(ts); + }) + }); + + }); +}); diff --git a/test/test.TxProposals.js b/test/test.TxProposals.js index 4ce94e9f6..a7a25e727 100644 --- a/test/test.TxProposals.js +++ b/test/test.TxProposals.js @@ -12,717 +12,105 @@ var Script = bitcore.Script; var TransactionBuilder = bitcore.TransactionBuilder; var util = bitcore.util; var networks = bitcore.networks; +var sinon = require('sinon'); try { var copay = require('copay'); //browser } catch (e) { var copay = require('../copay'); //node } -var fakeStorage = copay.FakeStorage; -var PrivateKey = copay.PrivateKey || require('../js/models/PrivateKey'); -var TxProposals = copay.TxProposals || require('../js/models/TxProposal'); -var is_browser = (typeof process == 'undefined' || typeof process.versions === 'undefined') -var PublicKeyRing = is_browser ? copay.PublicKeyRing : - require('soop').load('../js/models/core/PublicKeyRing', { - Storage: fakeStorage + +var FakeBuilder = require('./mocks/FakeBuilder'); +var TxProposal = copay.TxProposal; +var TxProposals = copay.TxProposals; + +var dummyProposal = new TxProposal({ + creator: 1, + createdTs: 1, + builder: new FakeBuilder(), + inputChainPaths: ['m/1'], }); -var config = { - networkName: 'testnet', -}; +var someKeys = ["03b39d61dc9a504b13ae480049c140dcffa23a6cc9c09d12d6d1f332fee5e18ca5", "022929f515c5cf967474322468c3bd945bb6f281225b2c884b465680ef3052c07e"]; -var unspentTest = [{ - "address": "dummy", - "scriptPubKey": "dummy", - "txid": "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1", - "vout": 1, - "amount": 10, - "confirmations": 7 -}]; - -var createPKR = function(bip32s) { - var w = new PublicKeyRing(config); - should.exist(w); - - for (var i = 0; i < 5; i++) { - if (bip32s && i < bip32s.length) { - var b = bip32s[i]; - w.addCopayer(b.deriveBIP45Branch().extendedPublicKeyString()); - } else { - w.addCopayer(); - } - } - - var pubkey = bip32s[0].publicHex; - - w.generateAddress(false, pubkey); - w.generateAddress(false, pubkey); - w.generateAddress(false, pubkey); - w.generateAddress(true, pubkey); - w.generateAddress(true, pubkey); - w.generateAddress(true, pubkey); - - return w; -}; - -var vopts = { - verifyP2SH: true, - dontVerifyStrictEnc: true -}; - - -describe('TxProposals model', function() { - - var isChange = false; - var addressIndex = 0; - - it('verify TXs', function(done) { - - var priv = new PrivateKey(config); - var priv2 = new PrivateKey(config); - var priv3 = new PrivateKey(config); - var pub = priv.publicHex; - - var ts = Date.now(); - var pkr = createPKR([priv, priv2, priv3]); - var opts = { - remainderOut: { - address: pkr.generateAddress(true, pub).toString() - } - }; - - var w = new TxProposals({ - networkName: config.networkName, - }); - - unspentTest[0].address = pkr.getAddress(addressIndex, isChange, pub).toString(); - unspentTest[0].scriptPubKey = pkr.getScriptPubKeyHex(addressIndex, isChange, pub); - w.add(createTx( - '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', - '123456789', - unspentTest, - opts, - priv, - pkr - )); - var ntxid = Object.keys(w.txps)[0]; - var b = w.txps[ntxid].builder; - var tx = b.build(); - tx.isComplete().should.equal(false); - - var ringIndex = pkr.getHDParams(pub); - b.sign(priv2.getAll(ringIndex.getReceiveIndex(), ringIndex.getChangeIndex(), ringIndex.copayerIndex)); - b.sign(priv3.getAll(ringIndex.getReceiveIndex(), ringIndex.getChangeIndex(), ringIndex.copayerIndex)); - tx = b.build(); - tx.isComplete().should.equal(true); - - var s = new Script(new bitcore.Buffer(unspentTest[0].scriptPubKey, 'hex')); - - tx.verifyInput(0, s, { - verifyP2SH: true, - dontVerifyStrictEnc: true - }, function(err, results) { - should.not.exist(err); - results.should.equal(true); - done(); +describe('TxProposals', function() { + describe('constructor', function() { + it('should create an instance', function() { + var txps = new TxProposals(); + should.exist(txps); + txps.network.name.should.equal('testnet'); }); }); - - - it('should create an instance', function() { - var w = new TxProposals({ - networkName: config.networkName - }); - should.exist(w); - w.network.name.should.equal(config.networkName); - }); - - var createTx = function(toAddress, amountSatStr, utxos, opts, priv, pkr) { - opts = opts || {}; - - var pub = priv.publicHex; - - if (!pkr.isComplete()) { - throw new Error('publicKeyRing is not complete'); - } - - if (!opts.remainderOut) { - opts.remainderOut = { - address: pkr.generateAddress(true, pub).toString() - }; - }; - - var b = new TransactionBuilder(opts) - .setUnspent(utxos) - .setOutputs([{ - address: toAddress, - amountSatStr: amountSatStr, - }]); - var selectedUtxos = b.getSelectedUnspent(); - var inputChainPaths = selectedUtxos.map(function(utxo) { - return pkr.pathForAddress(utxo.address); - }); - - var selectedUtxos = b.getSelectedUnspent(); - - var inputChainPaths = selectedUtxos.map(function(utxo) { - return pkr.pathForAddress(utxo.address); - }); - - b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths)); - var signRet; - if (priv) { - var pkeys = priv.getForPaths(inputChainPaths); - b.sign(pkeys); - } - var me = {}; - if (priv) me[priv.getId()] = Date.now(); - - var tx = b.build(); - - return { - inputChainPaths: inputChainPaths, - creator: priv.getId(), - createdTs: new Date(), - signedBy: priv && tx.countInputSignatures(0) ? me : {}, - seenBy: priv ? me : {}, - builder: b, - }; - }; - - - it('#getUsedUnspend', function() { - var priv = new PrivateKey(config); - var pub = priv.publicHex; - - var w = new TxProposals({ - networkName: config.networkName, - }); - var start = new Date().getTime(); - var pkr = createPKR([priv]); - var ts = Date.now(); - unspentTest[0].address = pkr.getAddress(addressIndex, isChange, pub).toString(); - unspentTest[0].scriptPubKey = pkr.getScriptPubKeyHex(addressIndex, isChange, pub); - w.add(createTx( - '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', - '123456789', - unspentTest, {}, - priv, - pkr - )); - var uu = w.getUsedUnspent(); - var uuk = Object.keys(uu); - uuk.length.should.equal(1); - uuk[0].split(',')[0].should.equal(unspentTest[0].txid); - }); - - it('#merge with self', function() { - var priv = new PrivateKey(config); - var pub = priv.publicHex; - - var w = new TxProposals({ - networkName: config.networkName, - }); - var start = new Date().getTime(); - var pkr = createPKR([priv]); - var ts = Date.now(); - - unspentTest[0].address = pkr.getAddress(addressIndex, isChange, pub).toString(); - unspentTest[0].scriptPubKey = pkr.getScriptPubKeyHex(addressIndex, isChange, pub); - w.add(createTx( - '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', - '123456789', - unspentTest, {}, - priv, - pkr - )); - var ntxid = Object.keys(w.txps)[0]; - var tx = w.txps[ntxid].builder.build(); - tx.isComplete().should.equal(false); - tx.countInputMissingSignatures(0).should.equal(2); - - var x = priv.getId(); - (w.txps[ntxid].signedBy[priv.getId()] - ts > 0).should.equal(true); - (w.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true); - - var info = w.merge(w.txps[ntxid], pkr.getCopayerId(0)); - info.events.length.should.equal(0); - - Object.keys(w.txps).length.should.equal(1); - - tx.isComplete().should.equal(false); - tx.countInputMissingSignatures(0).should.equal(2); - - (w.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true); - (w.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true); - }); - - - - it('#merge, merge signatures case 1', function() { - var priv2 = new PrivateKey(config); - var priv = new PrivateKey(config); - var pub = priv.publicHex; - - var ts = Date.now(); - var pkr = createPKR([priv]); - var opts = { - remainderOut: { - address: pkr.generateAddress(true, pub).toString() - } - }; - - - var w = new TxProposals({ - networkName: config.networkName, - }); - unspentTest[0].address = pkr.getAddress(addressIndex, isChange, pub).toString(); - unspentTest[0].scriptPubKey = pkr.getScriptPubKeyHex(addressIndex, isChange, pub); - w.add(createTx( - '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', - '123456789', - unspentTest, - opts, - priv2, - pkr - )); - - var ntxid = Object.keys(w.txps)[0]; - var tx = w.txps[ntxid].builder.build(); - tx.isComplete().should.equal(false); - tx.countInputSignatures(0).should.equal(0); - tx.countInputMissingSignatures(0).should.equal(1); - - Object.keys(w.txps[ntxid].signedBy).length.should.equal(0); - Object.keys(w.txps[ntxid].seenBy).length.should.equal(1); - - - var w2 = new TxProposals({ - networkName: config.networkName, - publicKeyRing: w.publicKeyRing, - }); - unspentTest[0].address = pkr.getAddress(addressIndex, isChange, pub).toString(); - unspentTest[0].scriptPubKey = pkr.getScriptPubKeyHex(addressIndex, isChange, pub); - w2.add(createTx( - '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', - '123456789', - unspentTest, - opts, - priv, - pkr - )); - - var ntxid = Object.keys(w.txps)[0]; - var tx = w2.txps[ntxid].builder.build(); - tx.isComplete().should.equal(false); - tx.countInputMissingSignatures(0).should.equal(2); - - - (w2.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true, 'asdsd'); - (w2.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true); - - var info = w.merge(w2.txps[ntxid], pkr.getCopayerId(0)); - info.events.length.should.equal(2); - info.events[0].type.should.equal('seen'); - info.events[1].type.should.equal('signed'); - - Object.keys(w.txps).length.should.equal(1); - - var tx = w.txps[ntxid].builder.build(); - tx.isComplete().should.equal(false); - tx.countInputMissingSignatures(0).should.equal(2); - (w.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true); - (w.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true); - - }); - - var _dumpChunks = function(scriptSig, label) { - console.log('## DUMP: ' + label + ' ##'); - for (var i = 0; i < scriptSig.chunks.length; i++) { - console.log('\tCHUNK ', i, scriptSig.chunks[i]); - } - }; - - - it('#merge, merge signatures case 2', function() { - - var o1 = { - extendedPrivateKeyString: 'tprv8ZgxMBicQKsPdSF1avR6mXyDj5Uv1XY2UyUHSDpAXQ5TvPN7prGeDppjy4562rBB9gMMAhRfFdJrNDpQ4t69kkqHNEEen3PX1zBJqSehJDH', - networkName: 'testnet', - privateKeyCache: {} - }; - var o2 = { - extendedPrivateKeyString: 'tprv8ZgxMBicQKsPdVeB5RzuxS9JQcACueZYgUaM5eWzaEBkHjW5Pg6Mqez1APSqoUP1jUdbT8WVG7ZJYTXvUL7XtPzFYBXjmdKuwSor1dcNQ8j', - networkName: 'testnet', - privateKeyCache: {} - }; - var o3 = { - extendedPrivateKeyString: 'tprv8ZgxMBicQKsPeHWNrPVZtQVgcCtXBr5TACNbDQ56rwqNJce9MEc64US6DJKxpWsrebEomxxWZFDtkvkZGkzA43uLvdF4XHiWqoNaL6Dq2Gd', - networkName: 'testnet', - privateKeyCache: {} - }; - - - var priv = PrivateKey.fromObj(o1); - var priv2 = PrivateKey.fromObj(o2); - var priv3 = PrivateKey.fromObj(o3); - var pub = priv.publicHex; - - var ts = Date.now(); - var pkr = createPKR([priv, priv2]); - var opts = { - remainderOut: { - address: '2MxK2m7cPtEwjZBB8Ksq7ppjkgJyFPJGemr' - } - }; - var addressToSign = pkr.generateAddress(false, pub); - unspentTest[0].address = addressToSign.toString(); - unspentTest[0].scriptPubKey = pkr.getScriptPubKeyHex(addressIndex, isChange, pub); - var tx, txb; - - var w = new TxProposals({ - networkName: config.networkName, - }); - - w.add(createTx( - '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', - '123456789', - unspentTest, - opts, - priv3, - pkr - )); - - var ntxid = Object.keys(w.txps)[0]; - txb = w.txps[ntxid].builder; - tx = txb.build(); - - tx.isComplete().should.equal(false); - tx.countInputMissingSignatures(0).should.equal(1); - - Object.keys(w.txps[ntxid].signedBy).length.should.equal(0); - Object.keys(w.txps[ntxid].seenBy).length.should.equal(1); - - var w2 = new TxProposals({ - networkName: config.networkName, - }); - - - - w2.add(createTx( - '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', - '123456789', - unspentTest, - opts, - priv, - pkr - )); - var ntxid = Object.keys(w2.txps)[0]; - txb = w2.txps[ntxid].builder; - tx = txb.build(); - - tx.isComplete().should.equal(false); - tx.countInputMissingSignatures(0).should.equal(2); - - (w2.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true); - (w2.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true); - - var info = w.merge(w2.txps[ntxid], pkr.getCopayerId(0)); - info.events.length.should.equal(2); - info.events[0].type.should.equal('seen'); - info.events[1].type.should.equal('signed'); - - tx = w.txps[ntxid].builder.build(); - tx.isComplete().should.equal(false); - tx.countInputMissingSignatures(0).should.equal(2); - (w.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true); - (w.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true); - - - var w3 = new TxProposals({ - networkName: config.networkName, - publicKeyRing: pkr, - }); - w3.add(createTx( - '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', - '123456789', - unspentTest, - opts, - priv2, - pkr - )); - tx = w3.txps[ntxid].builder.build(); - tx.isComplete().should.equal(false); - tx.countInputMissingSignatures(0).should.equal(2); - - (w3.txps[ntxid].signedBy[priv2.id] - ts > 0).should.equal(true); - (w3.txps[ntxid].seenBy[priv2.id] - ts > 0).should.equal(true); - - var info = w.merge(w3.txps[ntxid], pkr.getCopayerId(1)); - - Object.keys(w.txps).length.should.equal(1); - - (w.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true); - (w.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true); - (w.txps[ntxid].signedBy[priv2.id] - ts > 0).should.equal(true); - (w.txps[ntxid].seenBy[priv2.id] - ts > 0).should.equal(true); - - tx = w.txps[ntxid].builder.build(); - tx.isComplete().should.equal(false); - tx.countInputMissingSignatures(0).should.equal(1); - }); - - - it('#merge, merge signatures case 3', function() { - - var priv = new PrivateKey(config); - var priv2 = new PrivateKey(config); - var priv3 = new PrivateKey(config); - var pub = priv.publicHex; - - - var ts = Date.now(); - var pkr = createPKR([priv, priv2, priv3]); - var opts = { - remainderOut: { - address: pkr.generateAddress(true, pub).toString() - } - }; - - var w = new TxProposals({ - networkName: config.networkName, - }); - unspentTest[0].address = pkr.getAddress(addressIndex, isChange, pub).toString(); - unspentTest[0].scriptPubKey = pkr.getScriptPubKeyHex(addressIndex, isChange, pub); - w.add(createTx( - '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', - '123456789', - unspentTest, - opts, - priv, - pkr - )); - var ntxid = Object.keys(w.txps)[0]; - var tx = w.txps[ntxid].builder.build(); - tx.isComplete().should.equal(false); - tx.countInputMissingSignatures(0).should.equal(2); - (w.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true); - (w.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true); - - - var w2 = new TxProposals({ - networkName: config.networkName, - }); - unspentTest[0].address = pkr.getAddress(addressIndex, isChange, pub).toString(); - unspentTest[0].scriptPubKey = pkr.getScriptPubKeyHex(addressIndex, isChange, pub); - w2.add(createTx( - '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', - '123456789', - unspentTest, - opts, - priv2, - pkr - )); - var tx = w2.txps[ntxid].builder.build(); - tx.isComplete().should.equal(false); - tx.countInputMissingSignatures(0).should.equal(2); - (w2.txps[ntxid].signedBy[priv2.id] - ts > 0).should.equal(true); - (w2.txps[ntxid].seenBy[priv2.id] - ts > 0).should.equal(true); - - var w3 = new TxProposals({ - networkName: config.networkName, - }); - unspentTest[0].address = pkr.getAddress(addressIndex, isChange, pub).toString(); - unspentTest[0].scriptPubKey = pkr.getScriptPubKeyHex(addressIndex, isChange, pub); - w3.add(createTx( - '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', - '123456789', - unspentTest, - opts, - priv3, - pkr - )); - var tx = w3.txps[ntxid].builder.build(); - tx.isComplete().should.equal(false); - tx.countInputMissingSignatures(0).should.equal(2); - (w3.txps[ntxid].signedBy[priv3.id] - ts > 0).should.equal(true); - (w3.txps[ntxid].seenBy[priv3.id] - ts > 0).should.equal(true); - - var info = w.merge(w2.txps[ntxid], pkr.getCopayerId(1)); - - Object.keys(w.txps).length.should.equal(1); - var tx = w.txps[ntxid].builder.build(); - tx.isComplete().should.equal(false); - tx.countInputMissingSignatures(0).should.equal(1); - (w.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true); - (w.txps[ntxid].seenBy[priv2.id] - ts > 0).should.equal(true); - (w.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true); - (w.txps[ntxid].signedBy[priv2.id] - ts > 0).should.equal(true); - - - var info = w.merge(w3.txps[ntxid], pkr.getCopayerId(2)); - - var tx = w.txps[ntxid].builder.build(); - tx.isComplete().should.equal(true); - tx.countInputMissingSignatures(0).should.equal(0); - Object.keys(w.txps).length.should.equal(1); - (w.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true); - (w.txps[ntxid].seenBy[priv2.id] - ts > 0).should.equal(true); - (w.txps[ntxid].seenBy[priv3.id] - ts > 0).should.equal(true); - (w.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true); - (w.txps[ntxid].signedBy[priv2.id] - ts > 0).should.equal(true); - (w.txps[ntxid].signedBy[priv3.id] - ts > 0).should.equal(true); - }); - - - it('#fromObj stored (hardcoded) data', function() { - var txp = TxProposals.TxProposal.fromObj(txpv1); - txp.getID().should.equal('5cae6e225335acd2725856c71ef1ca61c42f118967102c5d0ed6710343e4a19f'); - var tx = txp.builder.build(); - tx.countInputSignatures(0).should.equal(2); - tx.countInputMissingSignatures(0).should.equal(0); - }); - - it('#toObj #fromObj roundtrip', function() { - - var priv = new PrivateKey(config); - var pub = priv.publicHex; - - var pkr = createPKR([priv]); - var w = new TxProposals({ - walletId: 'qwerty', - networkName: config.networkName, - }); - var ts = Date.now(); - - unspentTest[0].address = pkr.getAddress(addressIndex, isChange, pub).toString(); - unspentTest[0].scriptPubKey = pkr.getScriptPubKeyHex(addressIndex, isChange, pub); - w.add(createTx( - '15q6HKjWHAksHcH91JW23BJEuzZgFwydBt', - '123456789', - unspentTest, {}, - priv, - pkr - )); - var ntxid = Object.keys(w.txps)[0]; - var tx = w.txps[ntxid].builder.build(); - tx.isComplete().should.equal(false); - tx.countInputMissingSignatures(0).should.equal(2); - (w.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true); - (w.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true); - - var o = w.toObj(); - should.exist(o); - o.txps.length.should.equal(1); - - should.exist(o.txps[0]); - should.exist(o.txps[0].signedBy); - should.exist(o.txps[0].seenBy); - should.exist(o.txps[0].builderObj); - should.exist(o.txps[0].signedBy[priv.id]); - - var o2 = JSON.parse(JSON.stringify(o)); - var w2 = TxProposals.fromObj(o2); - w2.walletId.should.equal(w.walletId); - - var tx2 = w2.txps[ntxid].builder.build(); - tx2.isComplete().should.equal(false); - tx2.countInputMissingSignatures(0).should.equal(2); - (w2.txps[ntxid].signedBy[priv.id] - ts > 0).should.equal(true); - (w2.txps[ntxid].seenBy[priv.id] - ts > 0).should.equal(true); - should.exist(w2.txps[ntxid].builder); - should.exist(w2.txps[ntxid].builder.valueInSat); - - w2.merge(w.txps[ntxid], pkr.getCopayerId(0)); - Object.keys(w2.txps).length.should.equal(1); - }); - - describe('TxProposal model', function() { - var createMockTxp = function(raw) { - var tx = new Transaction(); - tx.parse(new Buffer(raw, 'hex')); - var txb = new TransactionBuilder(); - var txp = new TxProposals.TxProposal({ - builder: txb + describe('#fromObj', function() { + it('should create an instance from an Object', function() { + var txps = TxProposals.fromObj({ + networkName:'livenet', + walletId: '123a12', + txps: [], }); - txb.build = function() { - return tx; - }; - return txp; - }; - - it('should validate for no signatures yet in tx', function() { - // taken from https://gist.github.com/gavinandresen/3966071 - var raw = '010000000189632848f99722915727c5c75da8db2dbf194342a0429828f66ff88fab2af7d60000000000ffffffff0140420f000000000017a914f815b036d9bbbce5e9f2a00abd1bf3dc91e955108700000000'; - var txp = createMockTxp(raw); - txp.isValid().should.equal(true); + should.exist(txps); + txps.network.name.should.equal('livenet'); }); - it('should validate for no signatures yet in copay generated tx', function() { - // taken from copay incomplete tx proposal - var raw = '0100000001e205297fd05e4504d72761dc7a16e5cc9f4ab89877f28aee97c1cc66b3f07d690100000000ffffffff01706f9800000000001976a91473707e88f79c9c616b44bc766a25efcb9f49346688ac00000000'; - var txp = createMockTxp(raw); - txp.isValid().should.equal(true); - }); - it('should validate for a SIGHASH_NONE tx in builder', function() { - var raw = '010000000145c3bf51ced6cefaea8c6578a645316270dbf8600f46969d31136e1e06829598000000007000483045022100877c715e0f3bd6377086c96d4757b2c983682a1934d9e3f894941f4f1e18d4710220272ed81758d7a391ee4c15a29246f3fe75efbddeaf1118e4c0d3bb14f57cdba601255121022f58491a833933a9bea80d8e820e66bee91bd8c71bfa972fe70482360b48129951aeffffffff01706f9800000000001976a91408328947f0caf8728729d740cbecdfe3c2327db588ac00000000'; - var txp = createMockTxp(raw); - txp.isValid().should.equal(true); - }); - it('should not validate for a non SIGHASH_NONE tx in builder with 1 input', function() { - var raw = '0100000001eaf08f93f895127fbf000128ac74f6e8c7f003854e5ee1f02a5fd820cb689beb00000000fdfe00004730440220778f3174393e9ee6b0bfa876b4150db6f12a4da9715044ead5e345c2781ceee002203aab31f1e1d3dcf77ca780d9af798139719891917c9a09123dba54483ef462bc02493046022100dd93b64b30580029605dbba09d7fa34194d9ff38fda0c4fa187c52bf7f79ae98022100dd7b056762087b9aa8ccfde328d7067fa1753b78c0ee25577122569ff9de1d57024c695221039f847c24f09d7299c10bba4e41b24dc78e47bbb05fd7c1d209c994899d6881062103d363476e634fc5cdc11e9330c05a141c1e0c7f8b616817bdb83e7579bbf870942103fb2072953ceab87c6da450ac661685a881ddb661002d2ec1d60bfd33e3ec807d53aeffffffff01d06bf5050000000017a914db682f579cf6ca483880460fcf4ab63e223dc07e8700000000'; - var txp = createMockTxp(raw); - txp.isValid().should.equal(false); - }); - it('should not validate for a non SIGHASH_NONE tx in builder with 1 input', function() { - var raw = '0100000002d903852d223b3100fcc01e0b02d73a76a0787cdff7d000e9cba0e931917f407501000000fdfe0000493046022100b232e994fdca7fd61fcf8ffe4a7f746ff8f8baf2667ac80841de0250f521c402022100862c0783ca7eafcbd2786b9444ed6e83ae941dcc2248bea4db12b7815d15de050247304402200189fe0cde9d1dd192553f4dddb6764df3eb643f9f71be8aa015f41f2d4fd11f02205513b8ca985c3b5b936f814c7eba92e2e2985c83927ca06c41081d264c0be7a7024c695221026fa1a3ed0c820c1053c8ba101f3c96f85c55624a902a82cf6b2896ed5f9b3d1521035a3383c13dd346a5784adfe3ec3026ab31d519fdfae2740497b10bdfb994e6442103c7477a6668d5bc250fe727e358d951b9e05f1d7c02059bf59ecbb335f1eeec7953aeffffffffd903852d223b3100fcc01e0b02d73a76a0787cdff7d000e9cba0e931917f407500000000fdfd0000483045022100bdb9d14569af66d84af63416d77296ace24a96f1720d30e74bc6e316a4b3727502206ed54d532467393488889d72edbb667d075de491a89e8e496fee8791b943fa37024730440220379c30c884a21a949d8ec32d6934ffa9faf86add4d839de0f5fbd2b90f8ef1e802204048df2ec0035ce5e4bf01e9d70fd93a45a41ce2630100d692cd908cdaa61fc0024c69522102203938ef947327edce2cf2997c55b433be3d3ffcf3284c10d6fcdf4b01c6221f21033b60c3363a226ce9b850af655c6e1470d9a0936d7f56ea4a07ab84005f91cd1b210385755bc813fe7f92577b93bf689bf0d9b2118e6bbb7fee5d3d16976f4f7271af53aeffffffff01c02d9a3b0000000017a914db682f579cf6ca483880460fcf4ab63e223dc07e8700000000'; - var txp = createMockTxp(raw); - txp.isValid().should.equal(false); + it('should fail create an instance from an Object with errors', function() { + (function() {var txps = TxProposals.fromObj({ + networkName:'livenet', + walletId: '123a12', + txps: [ { a: 1 }], + }) }).should.throw('Illegal'); + }); + }); + describe('#getNtxids', function() { + it('should return keys', function() { + var txps = new TxProposals(); + txps.txps = {a:1, b:2}; + txps.getNtxids().should.deep.equal(['a','b']); + }); + }); + describe('#toObj', function() { + it('should an object', function() { + var txps = TxProposals.fromObj({ + networkName:'livenet', + walletId: '123a12', + txps: [], + }); + var o = txps.toObj(); + o.walletId.should.equal('123a12'); + o.networkName.should.equal('livenet'); + }); + it('should export txps', function() { + var txps = TxProposals.fromObj({ + networkName:'livenet', + walletId: '123a12', + txps: [], + }); + txps.txps = { + 'hola' : dummyProposal, + 'chau' : dummyProposal, + }; + var o = txps.toObj(); + o.txps.length.should.equal(2); + }); + it('should filter sent txp', function() { + var txps = TxProposals.fromObj({ + networkName:'livenet', + walletId: '123a12', + txps: [], + }); + var d = JSON.parse(JSON.stringify(dummyProposal)); + d.sent=1; + txps.txps = { + 'hola' : dummyProposal, + 'chau' : d, + }; + var o = txps.toObj(); + o.txps.length.should.equal(1); + }); + }); + describe.skip('#merge', function() { + it('should merge', function() { + var txps = new TxProposals(); + var d = dummyProposal; + txps.merge(d.toObj(),{}); }); }); - }); - -var txpv1 = { - "creator": "0361fb4252367715405a0d27f99cc74a671133292e8d725e009536d7257c8c01b0", - "createdTs": 1406310417996, - "seenBy": { - "0361fb4252367715405a0d27f99cc74a671133292e8d725e009536d7257c8c01b0": 1406310417996, - "02ba1599c64da4d80e25985be46c50e944b65f02e2b48c930528ce763d6710158f": 1406310418162 - }, - "signedBy": { - "0361fb4252367715405a0d27f99cc74a671133292e8d725e009536d7257c8c01b0": 1406310417996, - "02ba1599c64da4d80e25985be46c50e944b65f02e2b48c930528ce763d6710158f": 1406310645549 - }, - "rejectedBy": {}, - "sentTs": 1406310645873, - "sentTxid": "87296c50e8601437d63d556afb27c3b8e3819214be0a9d756d401a8286c0ec43", - "inputChainPaths": ["m/45'/0/1/1"], - "comment": "test 6", - "builderObj": { - "version": 1, - "outs": [{ - "address": "mph66bnLvcn9KUSMrpikUBUZZkN2C1Z5tg", - "amountSatStr": 100 - }], - "utxos": [{ - "address": "2NEodmgBa4SH3VwE2asgW34vMYe8VThBZNo", - "txid": "8f8deda12dad6248e655054632a27f6891ebb37e8d2b3dd1bff87e71fd451ac7", - "vout": 1, - "ts": 1406312717, - "scriptPubKey": "a914ec7bce12d0e82a7d2b5431f6d89ca70af317f5a187", - "amount": 0.009798, - "confirmations": 0, - "confirmationsFromCache": false - }], - "opts": { - "spendUnconfirmed": true, - "remainderOut": { - "address": "2N74XAozMH3JB3XgeBkRvRw1J8TtfLTtvny" - } - }, - "scriptSig": ["00483045022100f167ad33b8bef4c65af8d19c1a849d1770cc8d1e35bffebe6b5459dcbe655c7802207b37370b308ba668fe19f8e8bc462c9fbdc6c67f79900670758d228d83ea96da014730440220038ad3f4cc7b0738b593454ec189913ae4b442bc83da153d68d9a0077bd1b09102202b5728a08f302e97de61ea37280b48ccdd575f0d235c22f5e0ecac6a4ab0f46401475221024739614847d5233a46913482c17c6860194ad78abb3bf47de46223047d8a0b5821024c6dc65a52c5eaaa080b96888091544f8ab8712caa7e0b69ea4b45f6f059557452ae"], - "hashToScriptMap": { - "2NEodmgBa4SH3VwE2asgW34vMYe8VThBZNo": "5221024739614847d5233a46913482c17c6860194ad78abb3bf47de46223047d8a0b5821024c6dc65a52c5eaaa080b96888091544f8ab8712caa7e0b69ea4b45f6f059557452ae" - } - } -}; diff --git a/test/test.Wallet.js b/test/test.Wallet.js index 04b1626de..fad38aff6 100644 --- a/test/test.Wallet.js +++ b/test/test.Wallet.js @@ -10,7 +10,7 @@ try { } var copayConfig = require('../config'); var Wallet = require('../js/models/core/Wallet'); -var Structure = copay.Structure; +var PrivateKey = copay.PrivateKey; var Storage = require('./mocks/FakeStorage'); var Network = require('./mocks/FakeNetwork'); var Blockchain = require('./mocks/FakeBlockchain'); @@ -19,22 +19,30 @@ var TransactionBuilder = bitcore.TransactionBuilder; var Transaction = bitcore.Transaction; var Address = bitcore.Address; +var config = { + requiredCopayers: 3, + totalCopayers: 5, + spendUnconfirmed: true, + reconnectDelay: 100, + networkName: 'testnet', +}; + +var getNewEpk = function() { + return new PrivateKey({ + networkName: config.networkName, + }) + .deriveBIP45Branch() + .extendedPublicKeyString(); +} + var addCopayers = function(w) { for (var i = 0; i < 4; i++) { - w.publicKeyRing.addCopayer(); + w.publicKeyRing.addCopayer(getNewEpk()); } }; describe('Wallet model', function() { - var config = { - requiredCopayers: 3, - totalCopayers: 5, - spendUnconfirmed: true, - reconnectDelay: 100, - networkName: 'testnet', - }; - it('should fail to create an instance', function() { (function() { new Wallet(config) @@ -47,12 +55,11 @@ describe('Wallet model', function() { }); - var createW = function(netKey, N, conf) { + var createW = function(N, conf) { var c = JSON.parse(JSON.stringify(conf || config)); if (!N) N = c.totalCopayers; - if (netKey) c.netKey = netKey; var mainPrivateKey = new copay.PrivateKey({ networkName: config.networkName }); @@ -148,8 +155,7 @@ describe('Wallet model', function() { var createW2 = function(privateKeys, N, conf) { if (!N) N = 3; - var netKey = 'T0FbU2JLby0='; - var w = createW(netKey, N, conf); + var w = createW(N, conf); should.exist(w); var pkr = w.publicKeyRing; @@ -157,9 +163,9 @@ describe('Wallet model', function() { for (var i = 0; i < N - 1; i++) { if (privateKeys) { var k = privateKeys[i]; - pkr.addCopayer(k ? k.deriveBIP45Branch().extendedPublicKeyString() : null); + pkr.addCopayer(k ? k.deriveBIP45Branch().extendedPublicKeyString() : getNewEpk()); } else { - pkr.addCopayer(); + pkr.addCopayer(getNewEpk()); } } @@ -212,12 +218,12 @@ describe('Wallet model', function() { var t = w.txProposals; var txp = t.txps[ntxid]; + Object.keys(txp._inputSignatures).length.should.equal(1); var tx = txp.builder.build(); should.exist(tx); chai.expect(txp.comment).to.be.null; tx.isComplete().should.equal(false); Object.keys(txp.seenBy).length.should.equal(1); - Object.keys(txp.signedBy).length.should.equal(1); }); it('#create with comment', function() { @@ -363,6 +369,14 @@ describe('Wallet model', function() { }, w.reconnectDelay * callCount * (callCount + 1) / 2); }); + it('#isSingleUser', function() { + var w = createW(); + w.isShared().should.equal(true); + + w.totalCopayers = 1; + w.isShared().should.equal(false); + }); + it('#isReady', function() { var w = createW(); w.publicKeyRing.isComplete().should.equal(false); @@ -426,19 +440,7 @@ describe('Wallet model', function() { var w = createW(); var txp = { 'txProposal': { - creator: '02c643ef43c14481fa8e81e61438c2cbc39a59024663f8cab575d28a248fe53d96', - createdTs: '2014-07-24T23:54:26.682Z', - seenBy: { - '02c643ef43c14481fa8e81e61438c2cbc39a59024663f8cab575d28a248fe53d96': 1406246066682 - }, - signedBy: { - '02c643ef43c14481fa8e81e61438c2cbc39a59024663f8cab575d28a248fe53d96': 1406246066682 - }, - rejectedBy: {}, - sentTs: null, - sentTxid: null, - inputChainPaths: ['m/45\'/2/0/0'], - comment: null, + inputChainPaths: ['m/1'], builderObj: { version: 1, outs: [{ @@ -466,9 +468,13 @@ describe('Wallet model', function() { } }; + var stub = sinon.stub(w.publicKeyRing,'copayersForPubkeys').returns( + {'027445ab3a935dce7aee1dadb0d103ed6147a0f83deb80474a04538b2c5bc4d509':'pepe'} + ); w._handleTxProposal('senderID', txp, true); Object.keys(w.txProposals.txps).length.should.equal(1); w.getTxProposals().length.should.equal(1); + //stub.restore(); }); var newId = '00bacacafe'; @@ -494,7 +500,8 @@ describe('Wallet model', function() { var w = createW(); var r = w.getRegisteredCopayerIds(); r.length.should.equal(1); - w.publicKeyRing.addCopayer(); + w.publicKeyRing.addCopayer(getNewEpk()); + r = w.getRegisteredCopayerIds(); r.length.should.equal(2); r[0].should.not.equal(r[1]); @@ -504,7 +511,7 @@ describe('Wallet model', function() { var w = createW(); var r = w.getRegisteredPeerIds(); r.length.should.equal(1); - w.publicKeyRing.addCopayer(); + w.publicKeyRing.addCopayer(getNewEpk()); r = w.getRegisteredPeerIds(); r.length.should.equal(2); r[0].should.not.equal(r[1]); @@ -634,10 +641,11 @@ describe('Wallet model', function() { }); }); it('should create & sign transaction from received funds', function(done) { - this.timeout(10000); - var w = cachedCreateW2(); - var pk = w.privateKey; - w.privateKey = null; + var k2 = new PrivateKey({ + networkName: config.networkName + }); + + var w = createW2([k2]); var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); w.createTx(toAddress, amountSatStr, null, function(ntxid) { @@ -646,24 +654,36 @@ describe('Wallet model', function() { w.getTxProposals()[0].rejectedByUs.should.equal(false); done(); }); - w.privateKey = pk; + w.privateKey = k2; w.sign(ntxid, function(success) { success.should.equal(true); }); }); }); - it('should create & reject transaction', function(done) { + it('should fail to reject a signed transaction', function() { var w = cachedCreateW2(); - w.privateKey = null; var utxo = createUTXO(w); w.blockchain.fixUnspent(utxo); w.createTx(toAddress, amountSatStr, null, function(ntxid) { - w.on('txProposalsUpdated', function() { - w.getTxProposals()[0].signedByUs.should.equal(false); - w.getTxProposals()[0].rejectedByUs.should.equal(true); - done(); - }); + (function() { + w.reject(ntxid); + }).should.throw('reject a signed'); + }); + }); + + it('should create & reject transaction', function(done) { + var w = cachedCreateW2(); + var oldK = w.privateKey; + var utxo = createUTXO(w); + w.blockchain.fixUnspent(utxo); + w.createTx(toAddress, amountSatStr, null, function(ntxid) { + var s = sinon.stub(w, 'getMyCopayerId').returns('213'); + Object.keys(w.txProposals.get(ntxid).rejectedBy).length.should.equal(0); w.reject(ntxid); + Object.keys(w.txProposals.get(ntxid).rejectedBy).length.should.equal(1); + w.txProposals.get(ntxid).rejectedBy['213'].should.gt(1); + s.restore(); + done(); }); }); it('should create & sign & send a transaction', function(done) { @@ -1013,32 +1033,91 @@ describe('Wallet model', function() { copayConfig.forceNetwork = backup; }); }); + describe('_getKeymap', function() { + var w = cachedCreateW(); - describe('validate txProposals', function() { - var a1 = 'n1pKARYYUnZwxBuGj3y7WqVDu6VLN7n971'; - var a2 = 'mtxYYJXZJmQc2iJRHQ4RZkfxU5K7TE2qMJ'; - var utxos = [{ - address: a1, - txid: '2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1', - vout: 1, - scriptPubKey: Address.getScriptPubKeyFor(a1).serialize().toString('hex'), - amount: 0.5, - confirmations: 200 - }, { - address: a2, - txid: '88c4520ffd97ea565578afe0b40919120be704b36561c71ba4e450e83cb3c9fd', - vout: 1, - scriptPubKey: Address.getScriptPubKeyFor(a2).serialize().toString('hex'), - amount: 0.5001, - confirmations: 200 - }]; - var destAddress = 'myuAQcCc1REUgXGsCTiYhZvPPc3XxZ36G1'; - var outs = [{ - address: destAddress, - amount: 1.0 - }]; + it('should set keymap', function() { + var stub = sinon.stub(w.publicKeyRing, 'copayersForPubkeys', function() { + return { + '123': 'juan' + }; + }); + var txp = { + _inputSignatures: [ + ['123'] + ], + inputChainPaths: ['/m/1'], + }; + var map = w._getKeyMap(txp); + Object.keys(map).length.should.equal(1); + map['123'].should.equal('juan'); + stub.restore(); + }); + + it('should throw if unmatched sigs', function() { + var stub = sinon.stub(w.publicKeyRing, 'copayersForPubkeys', function() { + return { + '123': 'juan' + }; + }); + var txp = { + _inputSignatures: [ + ['234'] + ], + inputChainPaths: ['/m/1'], + }; + (function() { + w._getKeyMap(txp); + }).should.throw('dont match know copayers'); + stub.restore(); + }); + + it('should set keymap with multiple signatures', function() { + var stub = sinon.stub(w.publicKeyRing, 'copayersForPubkeys', function() { + return { + '123': 'juan', + '234': 'pepe', + }; + }); + var txp = { + _inputSignatures: [ + ['234', '123'] + ], + inputChainPaths: ['/m/1'], + }; + var map = w._getKeyMap(txp); + Object.keys(map).length.should.equal(2); + map['123'].should.equal('juan'); + map['234'].should.equal('pepe'); + stub.restore(); + }); + + it('should throw is one inputs has missing sigs', function() { + var stub = sinon.stub(w.publicKeyRing, 'copayersForPubkeys', function() { + return { + '123': 'juan', + '234': 'pepe', + }; + }); + var txp = { + _inputSignatures: [ + ['234', '123'], + ['234'] + ], + inputChainPaths: ['/m/1'], + }; + (function() { + w._getKeyMap(txp); + }).should.throw('different sig'); + stub.restore(); + }); + }); + + + + describe('_handleTxProposal', function() { + var testValidate = function(response, result, done) { - var testValidate = function(signhash, result, done) { var w = cachedCreateW(); var spy = sinon.spy(); w.on('txProposalEvent', spy); @@ -1046,47 +1125,149 @@ describe('Wallet model', function() { e.type.should.equal(result); done(); }); - var opts = {}; - opts.signhash = signhash; - var txb = new TransactionBuilder(opts) - .setUnspent(utxos) - .setOutputs(outs) - .sign(['cVBtNonMyTydnS3NnZyipbduXo9KZfF1aUZ3uQHcvJB6UARZbiWG', - 'cRVF68hhZp1PUQCdjr2k6aVYb2cn6uabbySDPBizAJ3PXF7vDXTL' - ]); + // txp.prototype.getId = function() {return 'aa'}; var txp = { - 'txProposal': { - 'builderObj': txb.toObj() - } + dummy: 1 }; - w._handleTxProposal('senderID', txp, true); + var txp = { + 'txProposal': txp + }; + var merge = sinon.stub(w.txProposals, 'merge', function() { + if (response == 0) throw new Error(); + return { + newCopayer: ['juan'], + ntxid: 1, + new: response == 1 + }; + }); + + w._handleTxProposal('senderID', txp); spy.callCount.should.equal(1); + merge.restore(); }; - it('should validate for undefined', function(done) { + it('should handle corrupt', function(done) { + var result = 'corrupt'; + testValidate(0, result, done); + }); + it('should handle new', function(done) { var result = 'new'; - var signhash; - testValidate(signhash, result, done); + testValidate(1, result, done); }); - it('should validate for SIGHASH_ALL', function(done) { - var result = 'new'; - var signhash = Transaction.SIGHASH_ALL; - testValidate(signhash, result, done); + it('should handle signed', function(done) { + var result = 'signed'; + testValidate(2, result, done); }); - it('should not validate for different SIGHASH_NONE', function(done) { - var result = 'corrupt'; - var signhash = Transaction.SIGHASH_NONE; - testValidate(signhash, result, done); + + }); + + + describe('_handleReject', function() { + it('should fails if unknown tx', function() { + var w = cachedCreateW(); + (function() { + w._handleReject(1, { + ntxid: 1 + }, 1); + }).should.throw('Unknown TXP'); }); - it('should not validate for different SIGHASH_SINGLE', function(done) { - var result = 'corrupt'; - var signhash = Transaction.SIGHASH_SINGLE; - testValidate(signhash, result, done); + it('should fail to reject a signed tx', function() { + var w = cachedCreateW(); + w.txProposals.txps['qwerty'] = { + signedBy: { + john: 1 + } + }; + (function() { + w._handleReject('john', { + ntxid: 'qwerty' + }, 1); + }).should.throw('already signed'); }); - it('should not validate for different SIGHASH_ANYONECANPAY', function(done) { - var result = 'corrupt'; - var signhash = Transaction.SIGHASH_ANYONECANPAY; - testValidate(signhash, result, done); + it('should reject a tx', function() { + var w = cachedCreateW(); + + function txp() { + this.ok = 0; + this.signedBy = {}; + }; + txp.prototype.setRejected = function() { + this.ok = 1; + }; + txp.prototype.toObj = function() {}; + + var spy1 = sinon.spy(w, 'store'); + var spy2 = sinon.spy(w, 'emit'); + w.txProposals.txps['qwerty'] = new txp(); + w.txProposals.txps['qwerty'].ok.should.equal(0); + w._handleReject('john', { + ntxid: 'qwerty' + }, 1); + w.txProposals.txps['qwerty'].ok.should.equal(1); + spy1.calledOnce.should.equal(true); + spy2.callCount.should.equal(2); + spy2.firstCall.args.should.deep.equal(['txProposalsUpdated']); + spy2.secondCall.args.should.deep.equal(['txProposalEvent', { + type: 'rejected', + cId: 'john', + txId: 'qwerty', + }]); }); }); + + + describe('_handleSeen', function() { + it('should fails if unknown tx', function() { + var w = cachedCreateW(); + (function() { + w._handleReject(1, { + ntxid: 1 + }, 1); + }).should.throw('Unknown TXP'); + }); + it('should set seen a tx', function() { + var w = cachedCreateW(); + + function txp() { + this.ok = 0; + this.signedBy = {}; + }; + txp.prototype.setSeen = function() { + this.ok = 1; + }; + txp.prototype.toObj = function() {}; + + var spy1 = sinon.spy(w, 'store'); + var spy2 = sinon.spy(w, 'emit'); + w.txProposals.txps['qwerty'] = new txp(); + w.txProposals.txps['qwerty'].ok.should.equal(0); + w._handleSeen('john', { + ntxid: 'qwerty' + }, 1); + w.txProposals.txps['qwerty'].ok.should.equal(1); + spy1.calledOnce.should.equal(true); + spy2.callCount.should.equal(2); + spy2.firstCall.args.should.deep.equal(['txProposalsUpdated']); + spy2.secondCall.args.should.deep.equal(['txProposalEvent', { + type: 'seen', + cId: 'john', + txId: 'qwerty', + }]); + }); + }); + + it('getNetwork', function() { + var w = cachedCreateW(); + var n = w.getNetwork(); + n.maxPeers.should.equal(5); + should.exist(n.networkNonce); + }); + + it('#disconnect', function() { + var w = cachedCreateW(); + var spy1 = sinon.spy(w.network, 'disconnect'); + w.disconnect(); + spy1.callCount.should.equal(1); + }); + }); diff --git a/test/test.WalletFactory.js b/test/test.WalletFactory.js index 00cad01ce..c61787932 100644 --- a/test/test.WalletFactory.js +++ b/test/test.WalletFactory.js @@ -317,6 +317,20 @@ describe('WalletFactory model', function() { }); }); + it('should clean lastOpened on delete wallet', function(done) { + var wf = new WalletFactory(config, '0.0.1'); + var w = wf.create({ + name: 'test wallet' + }); + + wf.storage.setLastOpened(w.id); + wf.delete(w.id, function() { + var last = wf.storage.getLastOpened(); + should.equal(last, undefined); + done(); + }); + }); + it('should return false if wallet does not exist', function() { var opts = { 'requiredCopayers': 2, @@ -343,6 +357,23 @@ describe('WalletFactory model', function() { wf.read.calledWith(walletId).should.be.true; }); + it('should save lastOpened on create/open a wallet', function() { + var opts = { + 'requiredCopayers': 2, + 'totalCopayers': 3 + }; + var wf = new WalletFactory(config, '0.0.1'); + var w = wf.create(opts); + var last = wf.storage.getLastOpened(); + should.equal(last, w.id); + + wf.storage.setLastOpened('other_id'); + + var wo = wf.open(w.id, opts); + last = wf.storage.getLastOpened(); + should.equal(last, w.id); + }); + it('should return error if network are differents', function() { var opts = { 'requiredCopayers': 2, diff --git a/test/test.blockchain.Insight.js b/test/test.blockchain.Insight.js index 18a35471f..e80d628f8 100644 --- a/test/test.blockchain.Insight.js +++ b/test/test.blockchain.Insight.js @@ -82,9 +82,9 @@ describe('Insight model', function() { sinon - .stub(http, 'request') - .returns(req) - .yields(request); + .stub(http, 'request') + .returns(req) + .yields(request); i.getUnspent(['2MuD5LnZSViZZYwZbpVsagwrH8WWvCztdmV', '2NBSLoMvsHsf2Uv3LA17zV4beH6Gze6RovA'], function(e, ret) { should.not.exist(e); @@ -113,9 +113,9 @@ describe('Insight model', function() { req.end = function() {}; sinon - .stub(http, 'request') - .returns(req) - .yields(request); + .stub(http, 'request') + .returns(req) + .yields(request); i.sendRawTransaction(rawtx, function(a) { should.exist(a); @@ -200,5 +200,33 @@ describe('Insight model', function() { }); }); + describe("#checkSentTx", function() { + it('should return true if Tx is found', function(done) { + var w = new Insight(); + w._request = sinon.stub().yields(null, { + txid: "414142", + }); + var tx = function() {}; + tx.prototype.getHash = function(){return new Buffer('BAA')}; + w.checkSentTx(new tx(), function(err, ret) { + should.not.exist(err); + ret.should.equal('414142'); + done(); + }); + }); + it('should return false if Tx is not found', function(done) { + var w = new Insight(); + w._request = sinon.stub().yields(null, { + txid: "414142", + }); + var tx = function() {}; + tx.prototype.getHash = function(){return new Buffer('ABC')}; + w.checkSentTx(new tx(), function(err, ret) { + should.not.exist(err); + ret.should.equal(false); + done(); + }); + }); + }); }); diff --git a/test/test.performance.js b/test/test.performance.js index d2b2bfb75..9bca46ebc 100644 --- a/test/test.performance.js +++ b/test/test.performance.js @@ -5,6 +5,15 @@ var should = chai.should(); var PrivateKey = require('../js/models/core/PrivateKey'); var PublicKeyRing = require('../js/models/core/PublicKeyRing'); +var getNewEpk = function() { + return new PrivateKey({ + networkName: 'livenet', + }) + .deriveBIP45Branch() + .extendedPublicKeyString(); +} + + describe('Performance tests', function() { describe('PrivateKey', function() { it('should optimize BIP32 private key gen time with cache', function() { @@ -43,7 +52,7 @@ describe('Performance tests', function() { requiredCopayers: M }); for (var i = 0; i < N; i++) { - pkr1.addCopayer(); // add new random ext public key + pkr1.addCopayer(getNewEpk()); // add new random ext public key } var generateN = 5; var generated = []; diff --git a/test/test.storage.LocalEncrypted.js b/test/test.storage.LocalEncrypted.js index 779f3542b..2212ab41f 100644 --- a/test/test.storage.LocalEncrypted.js +++ b/test/test.storage.LocalEncrypted.js @@ -148,6 +148,18 @@ describe('Storage/LocalEncrypted model', function() { s.getName(1).should.equal('hola'); }); }); + + describe('#getLastOpened #setLastOpened', function() { + it('should get/set names', function() { + var s = new LocalEncrypted({ + localStorage: localMock, + password: 'password' + }); + s.setLastOpened('hey'); + s.getLastOpened().should.equal('hey'); + }); + }); + describe('#getWallets', function() { it('should retreive wallets from storage', function() { var s = new LocalEncrypted({ diff --git a/test/unit/controllers/controllersSpec.js b/test/unit/controllers/controllersSpec.js index ba9d9e70e..02aa8c804 100644 --- a/test/unit/controllers/controllersSpec.js +++ b/test/unit/controllers/controllersSpec.js @@ -214,7 +214,7 @@ describe("Unit: Controllers", function() { }); - describe("Unit: Sidebar Controller", function() { + describe("Unit: Version Controller", function() { var scope, $httpBackendOut; var GH = 'https://api.github.com/repos/bitpay/copay/tags'; beforeEach(inject(function($controller, $injector) { @@ -235,7 +235,7 @@ describe("Unit: Controllers", function() { beforeEach(inject(function($controller, $rootScope) { rootScope = $rootScope; scope = $rootScope.$new(); - headerCtrl = $controller('SidebarController', { + headerCtrl = $controller('VersionController', { $scope: scope, }); })); @@ -273,8 +273,24 @@ describe("Unit: Controllers", function() { scope.$apply(); }); - it('should return an array of n undefined elements', function() { + it('should return networkName', function() { $httpBackend.flush(); // need flush + var networkName = scope.networkName; + expect(networkName).equal('livenet'); + }); + }); + + describe("Unit: Sidebar Controller", function() { + var rootScope; + beforeEach(inject(function($controller, $rootScope) { + rootScope = $rootScope; + scope = $rootScope.$new(); + headerCtrl = $controller('SidebarController', { + $scope: scope, + }); + })); + + it('should return an array of n undefined elements', function() { var n = 5; var array = scope.getNumber(n); expect(array.length).equal(n); diff --git a/views/copayers.html b/views/copayers.html index e71e2d6c0..a20294192 100644 --- a/views/copayers.html +++ b/views/copayers.html @@ -3,6 +3,7 @@
Copay +
@@ -73,8 +74,12 @@
+ Delete wallet + | Download seed backup + ng-show="!$root.wallet.publicKeyRing.isComplete()">Download seed backup
diff --git a/views/open.html b/views/open.html index e6055ce71..f58445e24 100644 --- a/views/open.html +++ b/views/open.html @@ -6,6 +6,7 @@
Copay +
diff --git a/views/send.html b/views/send.html index e80ccda0e..816b02e3f 100644 --- a/views/send.html +++ b/views/send.html @@ -77,7 +77,7 @@
-
+