From 5f666aef668d8de1ae402491ce40e0a69563de80 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Tue, 2 Sep 2014 18:29:02 -0300 Subject: [PATCH 01/13] Remove BuilderMockV0 --- js/models/core/BuilderMockV0.js | 26 -------------------------- js/models/core/TxProposal.js | 8 +------- js/models/core/TxProposals.js | 4 +--- 3 files changed, 2 insertions(+), 36 deletions(-) delete mode 100644 js/models/core/BuilderMockV0.js diff --git a/js/models/core/BuilderMockV0.js b/js/models/core/BuilderMockV0.js deleted file mode 100644 index be3926d10..000000000 --- a/js/models/core/BuilderMockV0.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - - -var bitcore = require('bitcore'); -var Transaction = bitcore.Transaction; - -function BuilderMockV0 (data) { - this.vanilla = data; - this.tx = new Transaction(); - this.tx.parse(new Buffer(data.tx, 'hex')); -}; - -BuilderMockV0.prototype.build = function() { - return this.tx; -}; - - -BuilderMockV0.prototype.getSelectedUnspent = function() { - return []; -}; - -BuilderMockV0.prototype.toObj = function() { - return this.vanilla; -}; - -module.exports = BuilderMockV0; diff --git a/js/models/core/TxProposal.js b/js/models/core/TxProposal.js index 2c7b6d02b..88046ca62 100644 --- a/js/models/core/TxProposal.js +++ b/js/models/core/TxProposal.js @@ -3,7 +3,6 @@ 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; @@ -136,12 +135,7 @@ TxProposal.fromObj = function(o, forceOpts) { } o.builder = TransactionBuilder.fromObj(o.builderObj); } catch (e) { - - // backwards (V0) compatatibility fix. - if (!o.version) { - o.builder = new BuilderMockV0(o.builderObj); - o.readonly = 1; - }; + throw new Error("Old version of wallet detected."); } return new TxProposal(o); }; diff --git a/js/models/core/TxProposals.js b/js/models/core/TxProposals.js index 11d9ad3b8..6f4cbc625 100644 --- a/js/models/core/TxProposals.js +++ b/js/models/core/TxProposals.js @@ -1,11 +1,9 @@ 'use strict'; -var BuilderMockV0 = require('./BuilderMockV0');; var bitcore = require('bitcore'); var util = bitcore.util; var Transaction = bitcore.Transaction; -var BuilderMockV0 = require('./BuilderMockV0');; -var TxProposal = require('./TxProposal');; +var TxProposal = require('./TxProposal'); var Script = bitcore.Script; var Key = bitcore.Key; var buffertools = bitcore.buffertools; From 61aea8db3d26d3ad02433ce344ed92037c738360 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Tue, 2 Sep 2014 18:43:40 -0300 Subject: [PATCH 02/13] Fix tests --- test/test.TxProposal.js | 2 +- test/test.WalletFactory.js | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/test/test.TxProposal.js b/test/test.TxProposal.js index 9ad7fe2a8..3e22e130c 100644 --- a/test/test.TxProposal.js +++ b/test/test.TxProposal.js @@ -133,7 +133,7 @@ describe('TxProposal', function() { builderObj: b.toObj(), inputChainPaths: ['m/1'], }); - }).should.throw('Invalid'); + }).should.throw('Invalid or Incompatible Backup Detected'); }); diff --git a/test/test.WalletFactory.js b/test/test.WalletFactory.js index b79bdcd5c..058f91c9b 100644 --- a/test/test.WalletFactory.js +++ b/test/test.WalletFactory.js @@ -432,16 +432,13 @@ describe('WalletFactory model', function() { wf.network.start.getCall(0).args[0].privkey.length.should.equal(64); //privkey is hex of private key buffer }); }); - describe('dont break backwards compatibility of wallets', function() { - it('should be able to import unencrypted legacy wallet TxProposal: v0', function() { - var wf = new WalletFactory(config, '0.0.5'); - var w = wf.fromObj(JSON.parse(legacyO)); + describe('break backwards compatibility with older versions', function() { + it('should\'nt be able to import unencrypted legacy wallet TxProposal: v0', function() { - should.exist(w); - w.id.should.equal('55d4bd062d32f90a'); - should.exist(w.publicKeyRing.getCopayerId); - should.exist(w.txProposals.toObj()); - should.exist(w.privateKey.toObj()); + (function() { + var wf = new WalletFactory(config, '0.0.5'); + var w = wf.fromObj(JSON.parse(legacyO)); + }).should.throw('Invalid or Incompatible Backup Detected'); }); it('should be able to import simple 1-of-1 encrypted legacy testnet wallet', function(done) { From b39a6833392ce44ca9d8b6e495e621c94208d049 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Tue, 2 Sep 2014 18:44:54 -0300 Subject: [PATCH 03/13] Fix error message --- js/models/core/TxProposal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/models/core/TxProposal.js b/js/models/core/TxProposal.js index 88046ca62..b9206a4c9 100644 --- a/js/models/core/TxProposal.js +++ b/js/models/core/TxProposal.js @@ -135,7 +135,7 @@ TxProposal.fromObj = function(o, forceOpts) { } o.builder = TransactionBuilder.fromObj(o.builderObj); } catch (e) { - throw new Error("Old version of wallet detected."); + throw new Error("Invalid or Incompatible Backup Detected."); } return new TxProposal(o); }; From 5a45c66573d129e5c2d0ead2192c68e20aaebd6e Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Wed, 3 Sep 2014 13:47:31 -0300 Subject: [PATCH 04/13] JSDoc and beautified the code fro PublicKeyRing --- js/models/core/PublicKeyRing.js | 549 +++++++++++++++++++++++++------- 1 file changed, 430 insertions(+), 119 deletions(-) diff --git a/js/models/core/PublicKeyRing.js b/js/models/core/PublicKeyRing.js index a16411a7b..0c22694aa 100644 --- a/js/models/core/PublicKeyRing.js +++ b/js/models/core/PublicKeyRing.js @@ -1,14 +1,30 @@ 'use strict'; var preconditions = require('preconditions').instance(); +var _ = require('underscore'); +var log = require('../../log'); var bitcore = require('bitcore'); var HK = bitcore.HierarchicalKey; +var Address = bitcore.Address; +var Script = bitcore.Script; var PrivateKey = require('./PrivateKey'); var HDPath = require('./HDPath'); var HDParams = require('./HDParams'); -var Address = bitcore.Address; -var Script = bitcore.Script; +/** + * @desc + * + * @constructor + * @param {Object} opts + * @param {string} opts.walletId + * @param {string} opts.network 'livenet' to signal the bitcoin main network, all others are testnet + * @param {number=} opts.requiredCopayers - defaults to 3 + * @param {number=} opts.totalCopayers - defaults to 5 + * @param {Object[]=} opts.indexes - an array to be deserialized using {@link HDParams#fromList} + * (defaults to all indexes in zero) + * @param {Object=} opts.nicknameFor - nicknames for other copayers + * @param {boolean[]=} opts.copayersBackup - whether other copayers have backed up their wallets + */ function PublicKeyRing(opts) { opts = opts || {}; @@ -29,24 +45,52 @@ function PublicKeyRing(opts) { this.copayerIds = []; this.copayersBackup = opts.copayersBackup || []; this.addressToPath = {}; -} - +}; +/** + * @desc Returns an object with only the keys needed to rebuild a PublicKeyRing + * + * @TODO: Figure out if this is the correct pattern + * This is a static method and is probably used for serialization. + * + * @static + * @param {Object} data + * @param {string} data.walletId - a string to identify a wallet + * @param {string} data.networkName - the name of the bitcoin network + * @param {number} data.requiredCopayers - the number of required copayers + * @param {number} data.totalCopayers - the number of copayers in the ring + * @param {Object[]} data.indexes - an array of objects that can be turned into + * an array of HDParams + * @param {Object} data.nicknameFor - a registry of nicknames for other copayers + * @param {boolean[]} data.copayersBackup - whether copayers have backed up their wallets + * @param {string[]} data.copayersExtPubKeys - the extended public keys of copayers + * @returns {Object} a trimmed down version of PublicKeyRing that can be used + * as a parameter + */ PublicKeyRing.trim = function(data) { var opts = {}; - ['walletId', 'networkName', 'requiredCopayers', 'totalCopayers','indexes','nicknameFor','copayersBackup', 'copayersExtPubKeys' ].forEach(function(k){ - opts[k] = data[k]; + ['walletId', 'networkName', 'requiredCopayers', 'totalCopayers', + 'indexes','nicknameFor','copayersBackup', 'copayersExtPubKeys' + ].forEach(function(k){ + opts[k] = data[k]; }); - return opts; }; +/** + * @desc Deserializes a PublicKeyRing from a plain object + * + * If the data parameter is an instance of PublicKeyRing already, + * it will fail, throwing an assertion error. + * + * @static + * @param {object} data - a serialized version of PublicKeyRing {@see PublicKeyRing#trim} + * @return {PublicKeyRing} - the deserialized object + */ PublicKeyRing.fromObj = function(data) { preconditions.checkArgument(!(data instanceof PublicKeyRing), 'bad data format: Did you use .toObj()?'); - - var opts = PublicKeyRing.trim(data); - + // Support old indexes schema if (!Array.isArray(opts.indexes)) { opts.indexes = HDParams.update(opts.indexes, opts.totalCopayers); @@ -61,6 +105,12 @@ PublicKeyRing.fromObj = function(data) { return ret; }; +/** + * @desc Serialize this object to a plain object with all the data needed to + * rebuild it + * + * @return {Object} a serialized version of a PublicKeyRing + */ PublicKeyRing.prototype.toObj = function() { return { walletId: this.walletId, @@ -73,91 +123,175 @@ PublicKeyRing.prototype.toObj = function() { copayersExtPubKeys: this.copayersHK.map(function(b) { return b.extendedPublicKeyString(); }), - nicknameFor: this.nicknameFor, + nicknameFor: this.nicknameFor }; }; -PublicKeyRing.prototype.getCopayerId = function(i) { - preconditions.checkArgument(typeof i !== 'undefined'); - return this.copayerIds[i]; +/** + * @desc + * Retrieve a copayer's public key as a hexadecimal encoded string + * + * @param {number} copayerId - the copayer id + * @returns {string} the extended public key of the i-th copayer + */ +PublicKeyRing.prototype.getCopayerId = function(copayerId) { + preconditions.checkArgument(!_.isUndefined(copayerId)) + preconditions.checkArgument(_.isNumber(copayerId)); + + return this.copayerIds[copayerId]; }; +/** + * @desc + * Get the amount of registered copayers in this PubKeyRing + * + * @returns {number} amount of copayers present + */ PublicKeyRing.prototype.registeredCopayers = function() { return this.copayersHK.length; }; +/** + * @desc + * Returns true if all the needed copayers have joined the public key ring + * + * @returns {boolean} + */ PublicKeyRing.prototype.isComplete = function() { return this.remainingCopayers() == 0; }; +/** + * @desc + * Returns the number of copayers yet to join to make the public key ring complete + * + * @returns {number} + */ PublicKeyRing.prototype.remainingCopayers = function() { return this.totalCopayers - this.registeredCopayers(); }; +/** + * @desc + * Returns an array of copayer's public keys + * + * @returns {string[]} a list of hexadecimal strings with the public keys for + * the copayers in this ring + */ PublicKeyRing.prototype.getAllCopayerIds = function() { return this.copayerIds; }; -PublicKeyRing.prototype.myCopayerId = function(i) { +/** + * @desc + * Gets the current user's copayerId + * + * @returns {string} the extended public key hexadecimal-encoded + */ +PublicKeyRing.prototype.myCopayerId = function() { return this.getCopayerId(0); }; +/** + * @desc Throws an error if the public key ring isn't complete + */ PublicKeyRing.prototype._checkKeys = function() { - - if (!this.isComplete()) - throw new Error('dont have required keys yet'); + if (!this.isComplete()) throw new Error('dont have required keys yet'); }; +/** + * @desc + * Updates the internal register of the public hex string for a copayer, based + * on the value of the hierarchical key stored in copayersHK + * + * @private + * @param {number} index - the index of the copayer to update + */ PublicKeyRing.prototype._updateBip = function(index) { var hk = this.copayersHK[index].derive(HDPath.IdBranch); this.copayerIds[index] = hk.eckey.public.toString('hex'); }; +/** + * @desc + * Sets a nickname for one of the copayers + * + * @private + * @param {number} index - the index of the copayer to update + * @param {string} nickname - the new nickname for that copayer + */ PublicKeyRing.prototype._setNicknameForIndex = function(index, nickname) { this.nicknameFor[this.copayerIds[index]] = nickname; }; +/** + * @desc + * Fetch the name of a copayer + * + * @param {number} index - the index of the copayer + * @return {string} the nickname of the index-th copayer + */ PublicKeyRing.prototype.nicknameForIndex = function(index) { return this.nicknameFor[this.copayerIds[index]]; }; +/** + * @desc + * Fetch the name of a copayer using its public key + * + * @param {string} copayerId - the public key ring of a copayer, hex encoded + * @return {string} the nickname of the copayer with such pubkey + */ PublicKeyRing.prototype.nicknameForCopayer = function(copayerId) { return this.nicknameFor[copayerId] || 'NN'; }; -PublicKeyRing.prototype.addCopayer = function(newEpk, nickname) { - preconditions.checkArgument(newEpk); +/** + * @desc + * Add a copayer into the public key ring. + * + * @param {string} newHexaExtendedPublicKey - an hex encoded string with the copayer's pubkey + * @param {string} nickname - a nickname for this copayer + * @return {string} the newHexaExtendedPublicKey parameter + */ +PublicKeyRing.prototype.addCopayer = function(newHexaExtendedPublicKey, nickname) { + preconditions.checkArgument(newHexaExtendedPublicKey && _.isString(newHexaExtendedPublicKey)); + preconditions.checkArgument(!this.isComplete()); + preconditions.checkArgument(!nickname || _.isString(nickname)); + preconditions.checkArgument(!_.any(this.copayersHK, + function(copayer) { return copayer.extendedPublicKeyString === newHexaExtendedPublicKey; } + )); - if (this.isComplete()) - throw new Error('PKR already has all required key:' + this.totalCopayers); + var newCopayerIndex = this.copayersHK.length; + var hierarchicalKey = new HK(newHexaExtendedPublicKey); - this.copayersHK.forEach(function(b) { - if (b.extendedPublicKeyString() === newEpk) - throw new Error('PKR already has that key'); - }); + this.copayersHK.push(hierarchicalKey); + this._updateBip(newCopayerIndex); - var i = this.copayersHK.length; - var bip = new HK(newEpk); - this.copayersHK.push(bip); - this._updateBip(i); if (nickname) { - this._setNicknameForIndex(i, nickname); + this._setNicknameForIndex(newCopayerIndex, nickname); } - return newEpk; + return newHexaExtendedPublicKey; }; +/** + * @desc + * Get all the public keys for the copayers in this ring, for a given branch of Copay + * + * @param {number} index - the index for the shared address + * @param {boolean} isChange - whether to derive a change address o receive address + * @param {number} copayerIndex - the index of the copayer that requested the derivation + * @return {Buffer[]} an array of derived public keys in hexa format + */ PublicKeyRing.prototype.getPubKeys = function(index, isChange, copayerIndex) { this._checkKeys(); var path = HDPath.Branch(index, isChange, copayerIndex); var pubKeys = this.publicKeysCache[path]; if (!pubKeys) { - pubKeys = []; - var l = this.copayersHK.length; - for (var i = 0; i < l; i++) { - var hk = this.copayersHK[i].derive(path); - pubKeys[i] = hk.eckey.public; - } + pubKeys = _.map(this.copayersHK, function(hdKey) { + return hdKey.derive(path).eckey.public; + }); this.publicKeysCache[path] = pubKeys.map(function(pk) { return pk.toString('hex'); }); @@ -166,19 +300,39 @@ PublicKeyRing.prototype.getPubKeys = function(index, isChange, copayerIndex) { return new Buffer(s, 'hex'); }); } - - return pubKeys; }; -// TODO this could be cached +/** + * @desc + * Generate a new Script for a copay address generated by index, isChange, and copayerIndex + * + * @TODO this could be cached + * + * @param {number} index - the index for the shared address + * @param {boolean} isChange - whether to derive a change address o receive address + * @param {number} copayerIndex - the index of the copayer that requested the derivation + * @returns {bitcore.Script} + */ PublicKeyRing.prototype.getRedeemScript = function(index, isChange, copayerIndex) { var pubKeys = this.getPubKeys(index, isChange, copayerIndex); var script = Script.createMultisig(this.requiredCopayers, pubKeys); return script; }; -// TODO this could be cached +/** + * @desc + * Get the address for a multisig based on the given params. + * + * Caches the address to the branch in the member addressToPath + * + * @TODO this could be cached + * + * @param {number} index - the index for the shared address + * @param {boolean} isChange - whether to derive a change address o receive address + * @param {number} copayerIndex - the index of the copayer that requested the derivation + * @returns {bitcore.Address} + */ PublicKeyRing.prototype.getAddress = function(index, isChange, id) { var copayerIndex = this.getCosigner(id); var script = this.getRedeemScript(index, isChange, copayerIndex); @@ -187,7 +341,17 @@ PublicKeyRing.prototype.getAddress = function(index, isChange, id) { return address; }; -// Overloaded to receive a PubkeyString or a consigner index +/** + * @desc + * Get the parameters used to derive a pubkey or a cosigner index + * + * Overloaded to receive a PubkeyString or a consigner index + * + * @TODO: Couldn't really figure out what does this do + * + * @param {number|string} id public key in hex format, or the copayer's index + * @return ???? + */ PublicKeyRing.prototype.getHDParams = function(id) { var copayerIndex = this.getCosigner(id); var index = this.indexes.filter(function(i) { @@ -198,20 +362,44 @@ PublicKeyRing.prototype.getHDParams = function(id) { return index[0]; }; +/** + * @desc + * Get the path used to derive a pubkey or a cosigner index for an address + * + * @param {string} address a multisig p2sh address + * @return {HDPath} + */ PublicKeyRing.prototype.pathForAddress = function(address) { var path = this.addressToPath[address]; if (!path) throw new Error('Couldn\'t find path for address ' + address); return path; }; -// TODO this could be cached +/** + * @desc + * Get the hexadecimal representation of a P2SH script + * + * @param {number} index - index to use when generating the address + * @param {boolean} isChange - generate a change address or a receive addres + * @param {number|string} pubkey - index of the copayer, or his public key + * @returns {string} hexadecimal encoded P2SH hash + */ PublicKeyRing.prototype.getScriptPubKeyHex = function(index, isChange, pubkey) { var copayerIndex = this.getCosigner(pubkey); var addr = this.getAddress(index, isChange, copayerIndex); return Script.createP2SH(addr.payload()).getBuffer().toString('hex'); }; -//generate a new address, update index. +/** + * @desc + * Generates a new address and updates the last index used + * + * @param {truthy} isChange - generate a change address if true, otherwise + * generates a receive + * @param {number|string} pubkey - the pubkey for the copayer that generates the + * address (or index in the keyring) + * @returns {bitpay.Address} + */ PublicKeyRing.prototype.generateAddress = function(isChange, pubkey) { isChange = !!isChange; var HDParams = this.getHDParams(pubkey); @@ -221,15 +409,31 @@ PublicKeyRing.prototype.generateAddress = function(isChange, pubkey) { return ret; }; +/** + * @desc + * Retrieve the addresses from a getAddressInfo return object + * + * {@see PublicKeyRing#getAddressInfo} + * @returns {string[]} the result of retrieving the addresses from calling + */ PublicKeyRing.prototype.getAddresses = function(opts) { return this.getAddressesInfo(opts).map(function(info) { return info.address; }); }; +/** + * @desc + * Maps a copayer's public key to his index in the keyring + * + * @param {number|string|undefined} pubKey - if undefined, returns the SHARED_INDEX + * - if a number, just return it + * - if a string, assume is the hex encoded public key + * @returns {number} the index of the copayer with the given pubkey + */ PublicKeyRing.prototype.getCosigner = function(pubKey) { - if (typeof pubKey == 'undefined') return HDPath.SHARED_INDEX; - if (typeof pubKey == 'number') return pubKey; + if (_.isUndefined(pubKey)) return HDPath.SHARED_INDEX; + if (_.isNumber(pubKey)) return pubKey; var sorted = this.copayersHK.map(function(h, i) { return h.eckey.public.toString('hex'); @@ -241,9 +445,17 @@ PublicKeyRing.prototype.getCosigner = function(pubKey) { if (index == -1) throw new Error('public key is not on the ring'); return index; -} - +}; +/** + * @desc + * Gets information about addresses for a copayer + * + * @see PublicKeyRing#getAddressesInfoForIndex + * @param {Object} opts + * @param {string|number} pubkey - the pubkey or index of a copayer in the ring + * @returns {AddressInfo[]} + */ PublicKeyRing.prototype.getAddressesInfo = function(opts, pubkey) { var ret = []; var self = this; @@ -252,53 +464,88 @@ PublicKeyRing.prototype.getAddressesInfo = function(opts, pubkey) { ret = ret.concat(self.getAddressesInfoForIndex(index, opts, copayerIndex)); }); return ret; -} +}; +/** + * @typedef AddressInfo + * @property {bitcore.Address} address - the address generated + * @property {string} addressStr - the base58 encoded address + * @property {boolean} isChange - true if it's a change address + * @property {boolean} owned - true if it's an address generated by a copayer + */ +/** + * @desc + * Retrieves info about addresses generated by a copayer + * + * @param {HDParams} index - HDParams of the copayer + * @param {Object} opts + * @param {boolean} opts.excludeChange - don't append information about change addresses + * @param {boolean} opts.excludeMain - don't append information about receive addresses + * @param {string|number|undefined} copayerIndex - copayer index, pubkey, or undefined to fetch info + * about shared addresses + * @return {AddressInfo[]} a list of AddressInfo + */ 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 appendAddressInfo = function(address, isChange) { + ret.unshift({ + address: address, + addressStr: address.toString(), + isChange: isChange, + 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 - }); - } + for (var i = 0; !opts.excludeChange && i < index.changeIndex; i++) { + appendAddressInfo(this.getAddress(i, true, index.copayerIndex), true); + } + for (var i = 0; !opts.excludeMain && i < index.receiveIndex; i++) { + appendAddressInfo(this.getAddress(i, false, index.copayerIndex), false); } return ret; }; +/** + * @desc + * Retrieve the public keys for all cosigners for a given path + * + * @param {string} path - the BIP32 path + * @return {Buffer[]} the public keys, in buffer format + */ PublicKeyRing.prototype.getForPath = function(path) { var p = HDPath.indexesForPath(path); - var pubKeys = this.getPubKeys(p.addressIndex, p.isChange, p.copayerIndex); - return pubKeys; + return this.getPubKeys(p.addressIndex, p.isChange, p.copayerIndex); }; +/** + * @desc + * Retrieve the public keys for all cosigners for multiple paths + * @see PublicKeyRing#getForPath + * + * @param {string[]} paths - the BIP32 paths + * @return {Buffer[][]} the public keys, in buffer format + */ PublicKeyRing.prototype.getForPaths = function(paths) { - preconditions.checkArgument(paths); + preconditions.checkArgument(!_.isUndefined(paths)); + preconditions.checkArgument(_.isArray(paths)); + preconditions.checkArgument(_.all(paths, _.isString)); + return paths.map(this.getForPath.bind(this)); }; - +/** + * @desc + * Retrieve the public keys for derived addresses and the public keys for copayers + * + * @TODO: Should this exist? A user should just call getForPath(paths) + * + * @param {string[]} paths - the paths to be derived + * @return {Object} with keys pubKeys and copayerIds + */ PublicKeyRing.prototype.forPaths = function(paths) { return { pubKeys: paths.map(this.getForPath.bind(this)), @@ -306,8 +553,13 @@ PublicKeyRing.prototype.forPaths = function(paths) { } }; - -// returns pubkey -> copayerId. +/** + * @desc + * Returns a map from a pubkey of an address to the id that generated it + * + * @param {string[]} pubkeys - the pubkeys to query + * @param {string[]} paths - the paths to query + */ PublicKeyRing.prototype.copayersForPubkeys = function(pubkeys, paths) { preconditions.checkArgument(pubkeys); preconditions.checkArgument(paths); @@ -328,71 +580,87 @@ PublicKeyRing.prototype.copayersForPubkeys = function(pubkeys, paths) { } } - - for(var i in inKeyMap) - throw new Error('Pubkey not identified') + if (_.size(inKeyMap)) { + for (var i in inKeyMap) { + log.error('Pubkey ' + i + ' not identified'); + } + throw new Error('Pubkeys not identified'); + } return ret; }; - -// TODO this could be cached -PublicKeyRing.prototype._addScriptMap = function(map, path) { - 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'); -}; - +/** + * @desc + * Returns a map from address -> public key needed + * + * @param {HDPath[]} paths - paths to be solved + * @returns {Object} a map from addresses to Buffer with the hex pubkeys + */ PublicKeyRing.prototype.getRedeemScriptMap = function(paths) { var ret = {}; for (var i = 0; i < paths.length; i++) { var path = paths[i]; - this._addScriptMap(ret, path); + var p = HDPath.indexesForPath(path); + var script = this.getRedeemScript(p.addressIndex, p.isChange, p.copayerIndex); + ret[Address.fromScript(script, this.network.name).toString()] = script.getBuffer().toString('hex'); } return ret; }; +/** + * @desc + * Check if another PubKeyRing is similar to this one (checks network name, + * requiredCopayers, and totalCopayers). If ignoreId is falsy, also check that + * both walletIds match. + * + * @private + * @param {PubKeyRing} inPKR - the other PubKeyRing + * @param {boolean} ignoreId - whether to ignore checking for equal walletId + * @throws {Error} if the wallets mismatch + * @return true + */ + PublicKeyRing.prototype._checkInPKR = function(inPKR, ignoreId) { - if (!ignoreId && this.walletId !== inPKR.walletId) { + if (!ignoreId && this.walletId !== inPKR.walletId) throw new Error('inPKR walletId mismatch'); - } - if (this.network.name !== inPKR.network.name) { + if (this.network.name !== inPKR.network.name) throw new Error('Network mismatch. Should be ' + this.network.name + ' and found ' + inPKR.network.name); - } - if ( - this.requiredCopayers && inPKR.requiredCopayers && + if (this.requiredCopayers && inPKR.requiredCopayers && (this.requiredCopayers !== inPKR.requiredCopayers)) - throw new Error('inPKR requiredCopayers mismatch ' + this.requiredCopayers + '!=' + inPKR.requiredCopayers); + throw new Error('inPKR requiredCopayers mismatch ' + this.requiredCopayers + + '!=' + inPKR.requiredCopayers); - if ( - this.totalCopayers && inPKR.totalCopayers && - (this.totalCopayers !== inPKR.totalCopayers)) - throw new Error('inPKR totalCopayers mismatch' + this.totalCopayers + '!=' + inPKR.requiredCopayers); + if (this.totalCopayers && inPKR.totalCopayers && + this.totalCopayers !== inPKR.totalCopayers) + throw new Error('inPKR totalCopayers mismatch' + this.totalCopayers + + '!=' + inPKR.requiredCopayers); + + return true; }; - +/** + * @desc + * Merges the public keys of the wallet passed in as a parameter with ours. + * + * @param {PublicKeyRing} inPKR + * @return {boolean} true if there where changes in our internal state + */ PublicKeyRing.prototype._mergePubkeys = function(inPKR) { var self = this; - var hasChanged = false; - var l = self.copayersHK.length; if (self.isComplete()) return; inPKR.copayersHK.forEach(function(b) { - var haveIt = false; var epk = b.extendedPublicKeyString(); - for (var j = 0; j < l; j++) { - if (self.copayersHK[j].extendedPublicKeyString() === epk) { - haveIt = true; - break; - } - } + var haveIt = _.any(self.copayersHK, function(hk) { return hk.extendedPublicKeyString() === epk; }); + if (!haveIt) { if (self.isComplete()) { throw new Error('trying to add more pubkeys, when PKR isComplete at merge'); @@ -408,27 +676,57 @@ PublicKeyRing.prototype._mergePubkeys = function(inPKR) { return hasChanged; }; -PublicKeyRing.prototype.setBackupReady = function(copayerId) { +/** + * @desc + * Mark backup as done for us + * + * @TODO: REVIEW FUNCTIONALITY - it used to have a parameter that was not used at all! + * + * @return {boolean} true if everybody has backed up their wallet + */ +PublicKeyRing.prototype.setBackupReady = function() { if (this.isBackupReady()) return false; var cid = this.myCopayerId(); this.copayersBackup.push(cid); return true; -} +}; +/** + * @desc returns true if a copayer has backed up his wallet + * @param {string=} copayerId - the pubkey of a copayer, defaults to our own's + * @return {boolean} if this copayer has backed up + */ PublicKeyRing.prototype.isBackupReady = function(copayerId) { var cid = copayerId || this.myCopayerId(); return this.copayersBackup.indexOf(cid) != -1; -} +}; -PublicKeyRing.prototype.isFullyBackup = function(copayerId) { +/** + * @desc returns true if all copayers have backed up their wallets + * @return {boolean} + */ +PublicKeyRing.prototype.isFullyBackup = function() { return this.remainingBackups() == 0; -} +}; +/** + * @desc returns the amount of backups remaining + * @return {boolean} + */ PublicKeyRing.prototype.remainingBackups = function() { return this.totalCopayers - this.copayersBackup.length; }; +/** + * @desc + * Merges this public key ring with another one, optionally ignoring the + * wallet id + * + * @param {PublicKeyRing} inPkr + * @param {boolean} ignoreId + * @return {boolean} true if the internal state has changed + */ PublicKeyRing.prototype.merge = function(inPKR, ignoreId) { this._checkInPKR(inPKR, ignoreId); @@ -440,6 +738,15 @@ PublicKeyRing.prototype.merge = function(inPKR, ignoreId) { return !!hasChanged; }; + +/** + * @desc + * Merges the indexes for addresses generated with another copy of a list of + * HDParams + * + * @param {HDParams[]} indexes - indexes as received from another sources + * @return {boolean} true if the internal state has changed + */ PublicKeyRing.prototype.mergeIndexes = function(indexes) { var self = this; var hasChanged = false; @@ -452,6 +759,11 @@ PublicKeyRing.prototype.mergeIndexes = function(indexes) { return !!hasChanged } +/** + * @desc merges information about backups done by another copy of PublicKeyRing + * @param {string[]} backups - another copy of backups + * @return {boolean} true if the internal state has changed + */ PublicKeyRing.prototype._mergeBackups = function(backups) { var self = this; var hasChanged = false; @@ -463,7 +775,6 @@ PublicKeyRing.prototype._mergeBackups = function(backups) { }); return !!hasChanged -} - +}; module.exports = PublicKeyRing; From 64399d7c71fecfef3f1f98bb8d9700c18d96e10c Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Thu, 4 Sep 2014 01:33:12 -0300 Subject: [PATCH 05/13] JSDoc and better code for WalletFactory --- js/models/core/WalletFactory.js | 144 +++++++++++++++++++++++++++++--- 1 file changed, 133 insertions(+), 11 deletions(-) diff --git a/js/models/core/WalletFactory.js b/js/models/core/WalletFactory.js index 2155bb2c1..89e1b09fd 100644 --- a/js/models/core/WalletFactory.js +++ b/js/models/core/WalletFactory.js @@ -4,18 +4,34 @@ var TxProposals = require('./TxProposals'); var PublicKeyRing = require('./PublicKeyRing'); var PrivateKey = require('./PrivateKey'); var Wallet = require('./Wallet'); -var preconditions = require('preconditions').instance(); - +var _ = require('underscore'); var log = require('../../log'); - var Async = module.exports.Async = require('../network/Async'); var Insight = module.exports.Insight = require('../blockchain/Insight'); var StorageLocalEncrypted = module.exports.StorageLocalEncrypted = require('../storage/LocalEncrypted'); -/* - * WalletFactory +/** + * @desc + * WalletFactory - stores the state for a wallet in creation + * + * @param {Object} config - configuration for this wallet + * + * @TODO: Don't pass a class for these three components + * -- send a factory or instance, the 'new' call considered harmful for refactoring + * -- arguable, since all of them is called with an object as argument. + * -- Still, could it be hard to refactor? (for example, what if we want to fail hard if a network call gets interrupted?) + * @param {Storage} config.Storage - the class to instantiate to store the wallet (StorageLocalEncrypted by default) + * @param {Object} config.storage - the configuration to be sent to the Storage constructor + * @param {Network} config.Network - the class to instantiate to make network requests to copayers (the Async module by default) + * @param {Object} config.network - the configuration to be sent to the Network constructor + * @param {Blockchain} config.Blockchain - the class to instantiate to get information about the blockchain (Insight by default) + * @param {Object} config.blockchain - the configuration to be sent to the Blockchain constructor + * @param {string} config.networkName - the name of the bitcoin network to use ('testnet' or 'livenet') + * @TODO: Investigate what parameters go inside this object + * @param {Object} config.wallet - default configuration for the wallet + * @TODO: put `version` inside of the config object + * @param {string} version - the version of copay for which this wallet was generated (for example, 0.4.7) */ - function WalletFactory(config, version) { var self = this; config = config || {}; @@ -31,8 +47,20 @@ function WalletFactory(config, version) { this.networkName = config.networkName; this.walletDefaults = config.wallet; this.version = version; -} +}; +/** + * @desc + * Returns true if the storage instance can retrieve the following keys using a given walletId + *
    + *
  • publicKeyRing
  • + *
  • txProposals
  • + *
  • opts
  • + *
  • privateKey
  • + *
+ * @param {string} walletId + * @return {boolean} true if all the keys are present in the storage instance + */ WalletFactory.prototype._checkRead = function(walletId) { var s = this.storage; var ret = @@ -43,6 +71,12 @@ WalletFactory.prototype._checkRead = function(walletId) { return !!ret; }; +/** + * @desc Deserialize an object to a Wallet + * @param {Object} obj + * @param {string[]} skipFields - fields to skip when importing + * @return {Wallet} + */ WalletFactory.prototype.fromObj = function(obj, skipFields) { // not stored options @@ -67,6 +101,13 @@ WalletFactory.prototype.fromObj = function(obj, skipFields) { return w; }; +/** + * @desc Imports a wallet from an encrypted base64 object + * @param {string} base64 - the base64 encoded object + * @param {string} password - password to decrypt it + * @param {string[]} skipFields - fields to ignore when importing + * @return {Wallet} + */ WalletFactory.prototype.fromEncryptedObj = function(base64, password, skipFields) { this.storage._setPassphrase(password); var walletObj = this.storage.import(base64); @@ -75,14 +116,29 @@ WalletFactory.prototype.fromEncryptedObj = function(base64, password, skipFields return w; }; +/** + * @TODO: import is a reserved keyword! DONT USE IT + * @TODO: this is essentialy the same method as {@link WalletFactory#fromEncryptedObj}! + * @desc Imports a wallet from an encrypted base64 object + * @param {string} base64 - the base64 encoded object + * @param {string} password - password to decrypt it + * @param {string[]} skipFields - fields to ignore when importing + * @return {Wallet} + */ WalletFactory.prototype.import = function(base64, password, skipFields) { var self = this; var w = self.fromEncryptedObj(base64, password, skipFields); if (!w) throw new Error('Wrong password'); return w; -} +}; +/** + * @desc Retrieve a wallet from storage + * @param {string} walletId - the wallet id + * @param {string[]} skipFields - parameters to ignore when importing + * @return {Wallet} + */ WalletFactory.prototype.read = function(walletId, skipFields) { if (!this._checkRead(walletId)) return false; @@ -103,6 +159,25 @@ WalletFactory.prototype.read = function(walletId, skipFields) { return w; }; +/** + * @desc This method instantiates a wallet + * + * @param {Object} opts + * @param {string} opts.id + * @param {PrivateKey=} opts.privateKey + * @param {string=} opts.privateKeyHex + * @param {number} opts.requiredCopayers + * @param {number} opts.totalCopayers + * @param {PublicKeyRing=} opts.publicKeyRing + * @param {string} opts.nickname + * @param {string} opts.passphrase + * @TODO: Figure out what is this parameter + * @param {?} opts.spendUnconfirmed this.walletDefaults.spendUnconfirmed ?? + * @TODO: Figure out in what unit is this reconnect delay. + * @param {number} opts.reconnectDelay milliseconds? + * @param {number=} opts.version + * @return {Wallet} + */ WalletFactory.prototype.create = function(opts) { opts = opts || {}; log.debug('### CREATING NEW WALLET.' + (opts.id ? ' USING ID: ' + opts.id : ' NEW ID') + (opts.privateKey ? ' USING PrivateKey: ' + opts.privateKey.getId() : ' NEW PrivateKey')); @@ -156,7 +231,11 @@ WalletFactory.prototype.create = function(opts) { return w; }; - +/** + * @desc Checks if a version is compatible with the current version + * @param {string} inVersion - a version, with major, minor, and revision, period-separated (x.y.z) + * @throws {Error} if there's a major version difference + */ WalletFactory.prototype._checkVersion = function(inVersion) { var thisV = this.version.split('.'); var thisV0 = parseInt(thisV[0]); @@ -172,14 +251,23 @@ WalletFactory.prototype._checkVersion = function(inVersion) { } }; - +/** + * @desc Throw an error if the network name is different to {@link WalletFactory#networkName} + * @param {string} inNetworkName - the network name to check + * @throws {Error} + */ WalletFactory.prototype._checkNetwork = function(inNetworkName) { if (this.networkName !== inNetworkName) { throw new Error('This Wallet is configured for ' + inNetworkName + ' while currently Copay is configured for: ' + this.networkName + '. Check your settings.'); } }; - +/** + * @desc Retrieve a wallet from the storage + * @param {string} walletId - the id of the wallet + * @param {string} passphrase - the passphrase to decode it + * @return {Wallet} + */ WalletFactory.prototype.open = function(walletId, passphrase) { this.storage._setPassphrase(passphrase); var w = this.read(walletId); @@ -190,6 +278,10 @@ WalletFactory.prototype.open = function(walletId, passphrase) { return w; }; +/** + * @desc Retrieve all wallets stored without encription in the storage instance + * @returns {Wallet[]} + */ WalletFactory.prototype.getWallets = function() { var ret = this.storage.getWallets(); ret.forEach(function(i) { @@ -198,6 +290,14 @@ WalletFactory.prototype.getWallets = function() { return ret; }; +/** + * @desc Deletes this wallet. This involves removing it from the storage instance + * @TODO: delete is a reserved javascript keyword. NEVER USE IT. + * @param {string} walletId + * @TODO: Why is there a callback? + * @callback cb + * @return {?} the result of the callback + */ WalletFactory.prototype.delete = function(walletId, cb) { var s = this.storage; s.deleteWallet(walletId); @@ -205,6 +305,9 @@ WalletFactory.prototype.delete = function(walletId, cb) { return cb(); }; +/** + * @desc Pass through to {@link Wallet#secret} + */ WalletFactory.prototype.decodeSecret = function(secret) { try { return Wallet.decodeSecret(secret); @@ -213,7 +316,26 @@ WalletFactory.prototype.decodeSecret = function(secret) { } }; +/** + * @callback walletCreationCallback + * @param {?=} err - an error, if any, that happened during the wallet creation + * @param {Wallet=} wallet - the wallet created + */ +/** + * @desc Start the network functionality. + * + * Start up the Network instance and try to join a wallet defined by the + * parameter secret using the parameter nickname. Encode + * information locally using passphrase. privateHex is the + * private extended master key. cb has two params: error and wallet. + * + * @param {string} secret - the wallet secret + * @param {string} nickname - a nickname for the current user + * @param {string} passphrase - a passphrase to use to encrypt the wallet for persistance + * @param {string} privateHex - the private extended master key + * @param {walletCreationCallback} cb - a callback + */ WalletFactory.prototype.joinCreateSession = function(secret, nickname, passphrase, privateHex, cb) { var self = this; var s = self.decodeSecret(secret); From f397d1f94f1b4d79d8094ea72ca0cf481eba879b Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Fri, 5 Sep 2014 11:44:06 -0300 Subject: [PATCH 06/13] JSDoc for wallet.js --- js/models/core/Wallet.js | 638 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 593 insertions(+), 45 deletions(-) diff --git a/js/models/core/Wallet.js b/js/models/core/Wallet.js index 32fd75c69..fb448d506 100644 --- a/js/models/core/Wallet.js +++ b/js/models/core/Wallet.js @@ -4,7 +4,8 @@ var EventEmitter = require('events').EventEmitter; var _ = require('underscore'); var async = require('async'); var preconditions = require('preconditions').singleton(); -var util = require('util'); +var inherits = require('inherits'); +var events = require('events'); var bitcore = require('bitcore'); var bignum = bitcore.Bignum; @@ -26,6 +27,33 @@ var PrivateKey = require('./PrivateKey'); var WalletLock = require('./WalletLock'); var copayConfig = require('../../../config'); +/** + * @desc + * Wallet manages a private key for Copay, network, storage of the wallet for + * persistance, and blockchain information. + * + * @TODO: Split this leviathan. + * + * @param {Object} opts + * @param {Storage} opts.storage - an object that can persist the wallet + * @param {Network} opts.network - used to send and retrieve messages from + * copayers + * @param {Blockchain} opts.blockchain - source of truth for what happens in + * the blockchain (utxos, balances) + * @param {number} opts.requiredCopayers - number of required copayers to + * release funds + * @param {number} opts.totalCopayers - number of copayers in the wallet + * @param {boolean} opts.spendUnconfirmed - whether it's safe to spend + * unconfirmed outputs or not + * @param {PublicKeyRing} opts.publicKeyRing - an instance of {@link PublicKeyRing} + * @param {TxProposals} opts.txProposals - an instance of {@link TxProposals} + * @param {PrivateKey} opts.privateKey - an instance of {@link PrivateKey} + * @param {string} opts.version - the version of copay where this wallet was + * created + * @TODO: figure out if reconnectDelay is set in milliseconds + * @param {number} opts.reconnectDelay - amount of seconds to wait before + * attempting to reconnect + */ function Wallet(opts) { var self = this; @@ -66,8 +94,17 @@ function Wallet(opts) { this.network.setHexNonces(opts.networkNonces); } -util.inherits(Wallet, EventEmitter); +inherits(Wallet, events.EventEmitter); +/** + * @TODO: Document this. Its usage is kind of weird + * + * @static + * @property lockTime + * @property signhash + * @property fee + * @property feeSat + */ Wallet.builderOpts = { lockTime: null, signhash: bitcore.Transaction.SIGNHASH_ALL, @@ -75,20 +112,45 @@ Wallet.builderOpts = { feeSat: null, }; +/** + * @desc Retrieve a random id for the wallet + * @TODO: Discuss changing to a UUID + * @return {string} 8 bytes, hexa encoded + */ Wallet.getRandomId = function() { var r = bitcore.SecureRandom.getPseudoRandomBuffer(8).toString('hex'); return r; }; +/** + * @desc Get a random 8 byte number and encode it as a hexa string + * @return {string} + */ Wallet.getRandomNumber = function() { var r = bitcore.SecureRandom.getPseudoRandomBuffer(5).toString('hex'); return r; }; +/** + * @desc Set the copayer id for the owner of this wallet + * @param {string} pubkey - the pubkey to set to the {@link Wallet#seededCopayerId} property + */ Wallet.prototype.seedCopayer = function(pubKey) { this.seededCopayerId = pubKey; }; +/** + * @desc Handles an 'indexes' message. + * + * Processes the data using {@link HDParams#fromList} and merges it with the + * {@link Wallet#publicKeyRing}. + * + * Triggers a {@link Wallet#store} if the internal state has changed. + * + * @param {string} senderId - the sender id + * @param {Object} data - the data recived, {@see HDParams#fromList} + * @emits {publicKeyRingUpdated} + */ Wallet.prototype._onIndexes = function(senderId, data) { log.debug('RECV INDEXES:', data); var inIndexes = HDParams.fromList(data.indexes); @@ -99,6 +161,26 @@ Wallet.prototype._onIndexes = function(senderId, data) { } }; +/** + * @desc + * Handles a 'PUBLICKEYRING' message from senderId. + * + * data.publicKeyRing is expected to be processed correctly by + * {@link PublicKeyRing#fromObj}. + * + * After successful deserialization, {@link Wallet#publicKeyRing} is merged + * with the received data, a call to {@link Wallet#store} is performed if the + * internal state has changed. + * + * This locks new incoming connections in case the public key ring is completed + * + * @param {string} senderId - the sender id + * @param {Object} data - the data recived, {@see HDParams#fromList} + * @param {Object} data.publicKeyRing - data to be deserialized into a {@link PublicKeyRing} + * using {@link PublicKeyRing#fromObj} + * @emits {publicKeyRingUpdated} + * @emits {connectionError} + */ Wallet.prototype._onPublicKeyRing = function(senderId, data) { log.debug('RECV PUBLICKEYRING:', data); @@ -126,7 +208,14 @@ Wallet.prototype._onPublicKeyRing = function(senderId, data) { } }; - +/** + * @desc + * Demultiplexes calls to update TxProposal updates + * + * @param {string} senderId - the copayer that sent this event + * @param {Object} m - the data received + * @emits {txProposalEvent} + */ Wallet.prototype._processProposalEvents = function(senderId, m) { var ev; if (m) { @@ -153,7 +242,6 @@ Wallet.prototype._processProposalEvents = function(senderId, m) { }; - /* OTDO events.push({ type: 'signed', @@ -161,6 +249,14 @@ cId: k, txId: ntxid }); */ +/** + * @desc + * Retrieves a keymap from from a transaction proposal set extracts a maps from + * public key to cosignerId for each signed input of the transaction proposal. + * + * @param {TxProposals} txp - the transaction proposals + * @return {Object} + */ Wallet.prototype._getKeyMap = function(txp) { preconditions.checkArgument(txp); var inSig0, keyMapAll = {}; @@ -191,7 +287,18 @@ Wallet.prototype._getKeyMap = function(txp) { return keyMapAll; }; +/** + * @callback transactionCallback + * @param {false|Transaction} returnValue + */ +/** + * @desc + * Asyncchronously check with the blockchain if a given transaction was sent. + * + * @param {string} ntxid - the transaction + * @param {transactionCallback} cb + */ Wallet.prototype._checkSentTx = function(ntxid, cb) { var txp = this.txProposals.get(ntxid); var tx = txp.builder.build(); @@ -204,7 +311,15 @@ Wallet.prototype._checkSentTx = function(ntxid, cb) { }); }; - +/** + * @desc + * Handles a 'TXPROPOSAL' network message + * + * @param {string} senderId - the id of the sender + * @param {Object} data - the data received + * @param {Object} data.txProposal - first parameter for {@link TxProposals#merge} + * @emits txProposalsUpdated + */ Wallet.prototype._onTxProposal = function(senderId, data) { var self = this; log.debug('RECV TXPROPOSAL: ', data); @@ -241,7 +356,16 @@ Wallet.prototype._onTxProposal = function(senderId, data) { this._processProposalEvents(senderId, m); }; - +/** + * @desc + * Handle a REJECT message received + * + * @param {string} senderId + * @param {Object} data + * @param {string} data.ntxid + * @emits txProposalsUpdated + * @emits txProposalEvent + */ Wallet.prototype._onReject = function(senderId, data) { preconditions.checkState(data.ntxid); log.debug('RECV REJECT:', data); @@ -265,6 +389,16 @@ Wallet.prototype._onReject = function(senderId, data) { }); }; +/** + * @desc + * Handle a SEEN message received + * + * @param {string} senderId + * @param {Object} data + * @param {string} data.ntxid + * @emits txProposalsUpdated + * @emits txProposalEvent + */ Wallet.prototype._onSeen = function(senderId, data) { preconditions.checkState(data.ntxid); log.debug('RECV SEEN:', data); @@ -281,8 +415,18 @@ Wallet.prototype._onSeen = function(senderId, data) { }; - - +/** + * @desc + * Handle a ADDRESSBOOK message received + * + * {@see Wallet#verifyAddressbookEntry} + * + * @param {string} senderId + * @param {Object} data + * @param {Object} data.addressBook + * @emits addressBookUpdated + * @emits txProposalEvent + */ Wallet.prototype._onAddressBook = function(senderId, data) { preconditions.checkState(data.addressBook); log.debug('RECV ADDRESSBOOK:', data); @@ -303,7 +447,10 @@ Wallet.prototype._onAddressBook = function(senderId, data) { } }; - +/** + * @desc Updates the wallet's last modified timestamp and triggers a save + * @param {number} ts - the timestamp + */ Wallet.prototype.updateTimestamp = function(ts) { preconditions.checkArgument(ts); preconditions.checkArgument(_.isNumber(ts)); @@ -311,12 +458,24 @@ Wallet.prototype.updateTimestamp = function(ts) { this.store(); }; - +/** + * @desc Called when there are no messages in the server + * Triggers a call to {@link Wallet#sendWalletReady} + */ Wallet.prototype._onNoMessages = function() { log.debug('No messages at the server. Requesting sync'); //TODO this.sendWalletReady(); }; +/** + * @desc Demultiplexes a new message received through the wire + * + * @param {string} senderId - the sender id + * @param {Object} data - the received object + * @param {number} ts - the timestamp when this object was received + * + * @emits corrupt + */ Wallet.prototype._onData = function(senderId, data, ts) { preconditions.checkArgument(senderId); preconditions.checkArgument(data); @@ -363,7 +522,7 @@ Wallet.prototype._onData = function(senderId, data, ts) { case 'addressbook': this._onAddressBook(senderId, data); break; - // unused messages + // unused messages case 'disconnect': //case 'an other unused message': break; @@ -375,6 +534,11 @@ Wallet.prototype._onData = function(senderId, data, ts) { this.updateTimestamp(ts); }; +/** + * @desc Handles a connect message + * @param {string} newCopayerId - the new copayer in the wallet + * @emits connect + */ Wallet.prototype._onConnect = function(newCopayerId) { if (newCopayerId) { log.debug('#### Setting new COPAYER:', newCopayerId); @@ -384,10 +548,20 @@ Wallet.prototype._onConnect = function(newCopayerId) { this.emit('connect', peerID); }; +/** + * @desc Returns the network name for this wallet ('testnet' or 'livenet') + * @return {string} + */ Wallet.prototype.getNetworkName = function() { return this.publicKeyRing.network.name; }; +/** + * @desc Serialize options into an object + * @return {Object} with keys id, spendUnconfirmed, + * requiredCopayers, totalCopayers, name, + * version + */ Wallet.prototype._optsToObj = function() { var obj = { id: this.id, @@ -401,35 +575,57 @@ Wallet.prototype._optsToObj = function() { return obj; }; - +/** + * @desc Retrieve the copayerId pubkey for a given index + * @param {number=} index - the index of the copayer, ours by default + * @return {string} hex-encoded pubkey + */ Wallet.prototype.getCopayerId = function(index) { return this.publicKeyRing.getCopayerId(index || 0); }; - +/** + * @desc Get my own pubkey + * @return {string} hex-encoded pubkey + */ Wallet.prototype.getMyCopayerId = function() { return this.getCopayerId(0); //copayer id is hex of a public key }; +/** + * @desc Retrieve my private key + * @return {string} hex-encoded private key + */ Wallet.prototype.getMyCopayerIdPriv = function() { return this.privateKey.getIdPriv(); //copayer idpriv is hex of a private key }; - +/** + * @desc Returns the secret value for other users to join this wallet + * @return {string} my own pubkey, base58 encoded + */ Wallet.prototype.getSecretNumber = function() { if (this.secretNumber) return this.secretNumber; this.secretNumber = Wallet.getRandomNumber(); return this.secretNumber; }; +/** + * @desc Returns the secret number used to prevent MitM attacks from Insight + * @return {string} + */ Wallet.prototype.getSecret = function() { var buf = new Buffer(this.getMyCopayerId() + this.getSecretNumber(), 'hex'); var str = Base58Check.encode(buf); return str; }; - - +/** + * @desc Returns an object with a pubKey value, an hex representation + * of a public key + * @param {string} secretB - the secret to be base58-decoded + * @return {Object} + */ Wallet.decodeSecret = function(secretB) { var secret = Base58Check.decode(secretB); var pubKeyBuf = secret.slice(0, 33); @@ -440,12 +636,28 @@ Wallet.decodeSecret = function(secretB) { } }; - +/** + * @desc Locks other sessions from connecting to the wallet + * @see Async#lockIncommingConnections + */ Wallet.prototype._lockIncomming = function() { this.network.lockIncommingConnections(this.publicKeyRing.getAllCopayerIds()); }; -Wallet.prototype.netStart = function(callback) { +/** + * @desc Sets up the networking with other peers. + * + * @emits connect + * @emits data + * + * @emits ready + * @emits publicKeyRingUpdated + * @emits txProposalsUpdated + * + * @TODO: FIX PROTOCOL -- emit with a space is shitty + * @emits no messages + */ +Wallet.prototype.netStart = function() { var self = this; var net = this.network; @@ -479,6 +691,10 @@ Wallet.prototype.netStart = function(callback) { }); }; +/** + * @desc Retrieves the public keys of all the copayers in the ring + * @return {string[]} hex-encoded public keys of copayers + */ Wallet.prototype.getRegisteredCopayerIds = function() { var l = this.publicKeyRing.registeredCopayers(); var copayers = []; @@ -489,6 +705,12 @@ Wallet.prototype.getRegisteredCopayerIds = function() { return copayers; }; +/** + * @desc Retrieves the public keys of all the peers in the network + * @TODO: Isn't this deprecated? Now that we don't use peerjs + * + * @return {string[]} hex-encoded public keys of copayers + */ Wallet.prototype.getRegisteredPeerIds = function() { var l = this.publicKeyRing.registeredCopayers(); if (this.registeredPeerIds.length !== l) { @@ -508,6 +730,12 @@ Wallet.prototype.getRegisteredPeerIds = function() { return this.registeredPeerIds; }; +/** + * @TODO: Review design of this call + * @desc Send a keepalive to this wallet's {@link WalletLock} instance. + * + * @emits locked - in case the wallet is opened in another instance + */ Wallet.prototype.keepAlive = function() { try { this.lock.keepAlive(); @@ -517,6 +745,9 @@ Wallet.prototype.keepAlive = function() { } }; +/** + * @desc Store the wallet's state + */ Wallet.prototype.store = function() { this.keepAlive(); @@ -525,6 +756,10 @@ Wallet.prototype.store = function() { log.debug('Wallet stored'); }; +/** + * @desc Serialize the wallet into a plain object. + * @return {Object} + */ Wallet.prototype.toObj = function() { var optsObj = this._optsToObj(); @@ -545,8 +780,24 @@ Wallet.prototype.toObj = function() { return walletObj; }; -// fromObj => from a trusted source +/** + * @desc Retrieve the wallet state from a trusted object + * + * @param {Object} o + * @param {Object[]} o.addressBook - Stores known associations of bitcoin addresses to names + * @param {Object} o.privateKey - Private key to be deserialized by {@link PrivateKey#fromObj} + * @param {string} o.networkName - 'livenet' or 'testnet' + * @param {Object} o.publicKeyRing - PublicKeyRing to be deserialized by {@link PublicKeyRing#fromObj} + * @param {number} o.lastTimestamp - last time this wallet object was deserialized + * @param {Object} o.txProposals - TxProposals to be deserialized by {@link TxProposals#fromObj} + * @param {string} o.nickname - user's nickname + * @param {Storage} storage - a Storage instance to store the data of the wallet + * @param {Network} network - a Network instance to communicate with peers + * @param {Blockchain} blockchain - a Blockchain instance to retrieve state from the blockchain + */ Wallet.fromObj = function(o, storage, network, blockchain) { + + // TODO: What is this supposed to do? var opts = JSON.parse(JSON.stringify(o.opts)); opts.addressBook = o.addressBook; @@ -590,15 +841,28 @@ Wallet.fromObj = function(o, storage, network, blockchain) { return new Wallet(opts); }; +/** + * @desc Return a base64 encrypted version of the wallet + * @return {string} base64 encoded string + */ Wallet.prototype.toEncryptedObj = function() { var walletObj = this.toObj(); return this.storage.export(walletObj); }; +/** + * @desc Send a message to other peers + * @param {string[]} recipients - the pubkey of the recipients of the message + * @param {Object} obj - the data to be sent to them + */ Wallet.prototype.send = function(recipients, obj) { this.network.send(recipients, obj); }; +/** + * @desc Send the set of TxProposals to some peers + * @param {string[]} recipients - the pubkeys of the recipients + */ Wallet.prototype.sendAllTxProposals = function(recipients) { var ntxids = this.txProposals.getNtxids(), that = this; @@ -607,6 +871,11 @@ Wallet.prototype.sendAllTxProposals = function(recipients) { }); }; +/** + * @desc Send a TxProposal identified by transaction id to a set of recipients + * @param {string} ntxid - the transaction proposal id + * @param {string[]=} recipients - the pubkeys of the recipients + */ Wallet.prototype.sendTxProposal = function(ntxid, recipients) { preconditions.checkArgument(ntxid); @@ -618,6 +887,10 @@ Wallet.prototype.sendTxProposal = function(ntxid, recipients) { }); }; +/** + * @desc Notifies other peers that a transaction proposal was seen + * @param {string} ntxid + */ Wallet.prototype.sendSeen = function(ntxid) { preconditions.checkArgument(ntxid); log.debug('### SENDING seen: ' + ntxid + ' TO: All'); @@ -628,6 +901,10 @@ Wallet.prototype.sendSeen = function(ntxid) { }); }; +/** + * @desc Notifies other peers that a transaction proposal was rejected + * @param {string} ntxid + */ Wallet.prototype.sendReject = function(ntxid) { preconditions.checkArgument(ntxid); log.debug('### SENDING reject: ' + ntxid + ' TO: All'); @@ -638,7 +915,10 @@ Wallet.prototype.sendReject = function(ntxid) { }); }; - +/** + * @desc Notify other peers that a wallet has been backed up and it's ready to be used + * @param {string[]=} recipients - the pubkeys of the recipients + */ Wallet.prototype.sendWalletReady = function(recipients) { log.debug('### SENDING WalletReady TO:', recipients || 'All'); @@ -648,6 +928,11 @@ Wallet.prototype.sendWalletReady = function(recipients) { }); }; +/** + * @desc Notify other peers of the walletId + * @TODO: Why is this needed? Can't everybody just calculate the walletId? + * @param {string[]=} recipients - the pubkeys of the recipients + */ Wallet.prototype.sendWalletId = function(recipients) { log.debug('### SENDING walletId TO:', recipients || 'All', this.id); @@ -659,7 +944,10 @@ Wallet.prototype.sendWalletId = function(recipients) { }); }; - +/** + * @desc Send the current PublicKeyRing to other recipients + * @param {string[]=} recipients - the pubkeys of the recipients + */ Wallet.prototype.sendPublicKeyRing = function(recipients) { log.debug('### SENDING publicKeyRing TO:', recipients || 'All', this.publicKeyRing.toObj()); var publicKeyRing = this.publicKeyRing.toObj(); @@ -670,6 +958,11 @@ Wallet.prototype.sendPublicKeyRing = function(recipients) { walletId: this.id, }); }; + +/** + * @desc Send the current indexes of our public key ring to other peers + * @param {string[]=} recipients - the pubkeys of the recipients + */ Wallet.prototype.sendIndexes = function(recipients) { var indexes = HDParams.serialize(this.publicKeyRing.indexes); log.debug('### INDEXES TO:', recipients || 'All', indexes); @@ -681,6 +974,10 @@ Wallet.prototype.sendIndexes = function(recipients) { }); }; +/** + * @desc Send our addressBook to other recipients + * @param {string[]=} recipients - the pubkeys of the recipients + */ Wallet.prototype.sendAddressBook = function(recipients) { log.debug('### SENDING addressBook TO:', recipients || 'All', this.addressBook); this.send(recipients, { @@ -690,15 +987,33 @@ Wallet.prototype.sendAddressBook = function(recipients) { }); }; +/** + * @desc Retrieve this wallet's name + * @return {string} + */ Wallet.prototype.getName = function() { return this.name || this.id; }; +/** + * @desc Generate a new address + * @param {boolean} isChange - whether to generate a change address or a receive address + * @return {string[]} a list of all the addresses generated so far for the wallet + */ Wallet.prototype._doGenerateAddress = function(isChange) { return this.publicKeyRing.generateAddress(isChange, this.publicKey); }; - +/** + * @callback addressCallback + * @param {string} addr - all the addresses of the wallet + */ +/** + * @desc Generate a new address + * @param {boolean} isChange - whether to generate a change address or a receive address + * @param {addressCallback} cb + * @return {string[]} a list of all the addresses generated so far for the wallet + */ Wallet.prototype.generateAddress = function(isChange, cb) { var addr = this._doGenerateAddress(isChange); this.sendIndexes(); @@ -707,7 +1022,12 @@ Wallet.prototype.generateAddress = function(isChange, cb) { return addr; }; - +/** + * @desc Retrieve all the Transaction proposals (see {@link TxProposals}) + * @return {Object[]} each object returned represents a transaction proposal, with two additional + * booleans: signedByUs and rejectedByUs. An optional third boolean signals + * whether the transaction was finally rejected (finallyRejected set to true). + */ Wallet.prototype.getTxProposals = function() { var ret = []; var copayers = this.getRegisteredCopayerIds(); @@ -726,6 +1046,11 @@ Wallet.prototype.getTxProposals = function() { return ret; }; +/** + * @desc Removes old transactions + * @param {boolean} deleteAll - if true, remove all the transactions + * @return {number} the number of deleted proposals + */ Wallet.prototype.purgeTxProposals = function(deleteAll) { var m = this.txProposals.length(); @@ -740,6 +1065,11 @@ Wallet.prototype.purgeTxProposals = function(deleteAll) { return m - n; }; +/** + * @desc Reject a proposal + * @param {string} ntxid the id of the transaction proposal to reject + * @emits txProposalsUpdated + */ Wallet.prototype.reject = function(ntxid) { var txp = this.txProposals.reject(ntxid, this.getMyCopayerId()); this.sendReject(ntxid); @@ -747,6 +1077,16 @@ Wallet.prototype.reject = function(ntxid) { this.emit('txProposalsUpdated'); }; +/** + * @callback signCallback + * @param {boolean} ret - true if it was successfully signed + */ +/** + * @desc Sign a proposal + * @param {string} ntxid the id of the transaction proposal to sign + * @param {signCallback} cb - a callback to be called on successful signing + * @emits txProposalsUpdated + */ Wallet.prototype.sign = function(ntxid, cb) { preconditions.checkState(!_.isUndefined(this.getMyCopayerId())); var self = this; @@ -783,7 +1123,15 @@ Wallet.prototype.sign = function(ntxid, cb) { }, 10); }; - +/** + * @callback broadcastCallback + * @param {string} txid - the transaction id on the blockchain + */ +/** + * @desc Broadcasts a transaction to the blockchain + * @param {string} ntxid - the transaction proposal id + * @param {broadcastCallback} cb + */ Wallet.prototype.sendTx = function(ntxid, cb) { var txp = this.txProposals.get(ntxid); @@ -820,6 +1168,12 @@ Wallet.prototype.sendTx = function(ntxid, cb) { }); }; +/** + * @desc Create a Payment Protocol transaction + * @param {Object|string} options - if it's a string, parse it as the uri + * @param {string} options.uri the url for the transaction + * @param {Function} cb + */ Wallet.prototype.createPaymentTx = function(options, cb) { var self = this; @@ -863,6 +1217,13 @@ Wallet.prototype.createPaymentTx = function(options, cb) { }); }; +/** + * @desc Creates a Payment TxProposal from a uri + * @param {Object} options + * @param {string=} options.uri + * @param {string=} options.url + * @param {Function} cb + */ Wallet.prototype.fetchPaymentTx = function(options, cb) { var self = this; @@ -890,6 +1251,19 @@ Wallet.prototype.fetchPaymentTx = function(options, cb) { }); }; +/** + * @desc Analyzes a payment request and generates a transaction proposal for it. + * @param {Object} options + * @param {PayProRequest} pr + * @param {string} pr.payment_details_version + * @param {string} pr.pki_type + * @param {Object} pr.data + * @param {string} pr.serialized_payment_details + * @param {string} pr.signature + * @param {string} options.memo + * @param {string} options.comment + * @param {Function} cb + */ Wallet.prototype.receivePaymentRequest = function(options, pr, cb) { var self = this; @@ -1007,6 +1381,18 @@ Wallet.prototype.receivePaymentRequest = function(options, pr, cb) { }); }; +/** + * @desc Send a payment transaction to a server, complying with BIP70 + * + * @TODO: Get this out of here. + * + * @param {string} ntxid - the transaction proposal id + * @param {Object} options + * @param {string} options.refund_to + * @param {string} options.memo + * @param {string} options.comment + * @param {Function} cb + */ Wallet.prototype.sendPaymentTx = function(ntxid, options, cb) { var self = this; @@ -1115,6 +1501,9 @@ Wallet.prototype.sendPaymentTx = function(ntxid, options, cb) { }); }; +/** + * @desc Handles a PaymentRequestACK from the server + */ Wallet.prototype.receivePaymentRequestACK = function(ntxid, tx, txp, ack, cb) { var self = this; @@ -1162,6 +1551,10 @@ Wallet.prototype.receivePaymentRequestACK = function(ntxid, tx, txp, ack, cb) { return cb(txid, txp.merchant); }; +/** + * @desc Create a Payment Transaction Sync (see BIP70) + * @TODO: Document better + */ Wallet.prototype.createPaymentTxSync = function(options, merchantData, unspent) { var self = this; var priv = this.privateKey; @@ -1281,10 +1674,14 @@ Wallet.prototype.createPaymentTxSync = function(options, merchantData, unspent) return ntxid; }; -// This essentially ensures that a copayer hasn't tampered with a -// PaymentRequest message from a payment server. It verifies the signature -// based on the cert, and checks to ensure the desired outputs are the same as -// the ones on the tx proposal. +/** + * @desc Verifies a PaymentRequest sent by another peer + * This essentially ensures that a copayer hasn't tampered with a + * PaymentRequest message from a payment server. It verifies the signature + * based on the cert, and checks to ensure the desired outputs are the same as + * the ones on the tx proposal. + * @TODO: Document better + */ Wallet.prototype.verifyPaymentRequest = function(ntxid) { if (!ntxid) return false; @@ -1429,6 +1826,10 @@ Wallet.prototype.verifyPaymentRequest = function(ntxid) { return true; }; +/** + * @desc Mark that a user has seen a given TxProposal + * @return {boolean} true if the internal state has changed + */ Wallet.prototype.addSeenToTxProposals = function() { var ret = false; var myId = this.getMyCopayerId(); @@ -1444,27 +1845,54 @@ Wallet.prototype.addSeenToTxProposals = function() { return ret; }; -// TODO: remove this method and use getAddressesInfo everywhere +/** + * @desc Alias for {@link PublicKeyRing#getAddresses} + * @TODO: remove this method and use getAddressesInfo everywhere + * @return {Buffer[]} + */ Wallet.prototype.getAddresses = function(opts) { return this.publicKeyRing.getAddresses(opts); }; +/** + * @desc Retrieves all addresses as strings. + * + * @param {Object} opts - Same options as {@link PublicKeyRing#getAddresses} + * @return {string[]} + */ Wallet.prototype.getAddressesStr = function(opts) { return this.getAddresses(opts).map(function(a) { return a.toString(); }); }; +/** + * @desc Alias for {@link PublicKeyRing#getAddressesInfo} + */ Wallet.prototype.getAddressesInfo = function(opts) { return this.publicKeyRing.getAddressesInfo(opts, this.publicKey); }; - +/** + * @desc Returns true if a given address was generated by deriving our master public key + * @return {boolean} + */ Wallet.prototype.addressIsOwn = function(addrStr, opts) { var addrList = this.getAddressesStr(opts); return _.any(addrList, function(value) { return value === addrStr; }); }; -//retunrs values in SATOSHIs + +/** + * @callback {getBalanceCallback} + * @param {string=} err - an error, if any + * @param {number} balance - total number of satoshis for all addresses + * @param {Object} balanceByAddr - maps string addresses to satoshis + * @param {number} safeBalance - total number of satoshis in UTXOs that are not part of any TxProposal + */ +/** + * @desc Returns the balances for all addresses in Satoshis + * @param {getBalanceCallback} cb + */ Wallet.prototype.getBalance = function(cb) { var balance = 0; var safeBalance = 0; @@ -1502,16 +1930,30 @@ Wallet.prototype.getBalance = function(cb) { }; -// See +// See // https://github.com/bitpay/copay/issues/1056 // // maxRejectCount should equal requiredCopayers -// strictly. -// -Wallet.prototype.maxRejectCount = function(cb) { +// strictly. +/** + * @desc Get the number of copayers that need to reject a transaction so it can't be signed + * @return {number} + */ +Wallet.prototype.maxRejectCount = function() { return this.totalCopayers - this.requiredCopayers; }; +/** + * @callback getUnspentCallback + * @TODO: Document this better + * @param {string} error + * @param {Object[]} safeUnspendList + * @param {Object[]} unspentList + */ +/** + * @desc Get a list of unspent transaction outputs + * @param {getUnspentCallback} cb + */ Wallet.prototype.getUnspent = function(cb) { var self = this; this.blockchain.getUnspent(this.getAddressesStr(), function(err, unspentList) { @@ -1581,6 +2023,10 @@ Wallet.prototype.removeTxWithSpentInputs = function(cb) { }; +/** + * @desc Create a transaction proposal + * @TODO: Document more + */ Wallet.prototype.createTx = function(toAddress, amountSatStr, comment, opts, cb) { var self = this; @@ -1624,6 +2070,10 @@ Wallet.prototype.createTx = function(toAddress, amountSatStr, comment, opts, cb) }); }; +/** + * @desc Create a transaction proposal + * @TODO: Document more + */ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos, opts) { var pkr = this.publicKeyRing; var priv = this.privateKey; @@ -1687,6 +2137,13 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos return ntxid; }; +/** + * @desc Updates all the indexes for the current publicKeyRing + * + * Triggers a wallet {@link Wallet#store} call + * @param {Function} callback - called when all indexes have been updated. Receives an error, if any, as first argument + * @emits publicKeyRingUpdated + */ Wallet.prototype.updateIndexes = function(callback) { var self = this; log.debug('Updating indexes...'); @@ -1704,8 +2161,13 @@ Wallet.prototype.updateIndexes = function(callback) { self.store(); callback(); }); -} +}; +/** + * @desc Updates the lastly used index + * @param {Object} index - an index, as used by {@link PublicKeyRing} + * @param {Function} callback - called with no arguments when done updating + */ Wallet.prototype.updateIndex = function(index, callback) { var self = this; var SCANN_WINDOW = 20; @@ -1721,8 +2183,17 @@ Wallet.prototype.updateIndex = function(index, callback) { callback(); }); }); -} +}; +/** + * @desc Derive addresses using the given parameters + * + * @param {number} index - the index to start with + * @param {number} amount - number of addresses to derive + * @param {boolean} isChange - derive change addresses or receive addresses + * @param {number} copayerIndex - the index of the copayer for whom to derive addresses + * @return {string[]} the result of calling {@link PublicKeyRing#getAddress} + */ Wallet.prototype.deriveAddresses = function(index, amount, isChange, copayerIndex) { preconditions.checkArgument(amount); preconditions.shouldBeDefined(copayerIndex); @@ -1732,11 +2203,26 @@ Wallet.prototype.deriveAddresses = function(index, amount, isChange, copayerInde ret[i] = this.publicKeyRing.getAddress(index + i, isChange, copayerIndex).toString(); } return ret; -} +}; -// This function scans the publicKeyRing branch starting at index @start and reports the index with last activity, -// using a scan window of @gap. The argument @change defines the branch to scan: internal or external. -// Returns -1 if no activity is found in range. +/** + * @callback {indexDiscoveryCallback} + * @param {?} err + * @param {number} lastActivityIndex + */ +/** + * @desc Scans the block chain for the last index with activity for a copayer + * + * This function scans the publicKeyRing branch starting at index @start and reports the index with last activity, + * using a scan window of @gap. The argument @change defines the branch to scan: internal or external. + * Returns -1 if no activity is found in range. + * @param {number} start - the number for which to start scanning + * @param {boolean} change - whether to search for in the change branch or the receive branch + * @param {number} copayerIndex - the index of the copayer + * @param {number} gap - the maximum number of addresses to scan after the last active address + * @param {indexDiscoveryCallback} cb - callback + * @return {number} -1 if there's no activity in the range provided + */ Wallet.prototype.indexDiscovery = function(start, change, copayerIndex, gap, cb) { preconditions.shouldBeDefined(copayerIndex); preconditions.checkArgument(gap); @@ -1771,9 +2257,11 @@ Wallet.prototype.indexDiscovery = function(start, change, copayerIndex, gap, cb) cb(null, lastActive); } ); -} - +}; +/** + * @desc Closes the wallet and disconnects all services + */ Wallet.prototype.close = function() { log.debug('## CLOSING'); this.lock.release(); @@ -1781,16 +2269,30 @@ Wallet.prototype.close = function() { this.blockchain.destroy(); }; +/** + * @desc Returns the name of the network ('livenet' or 'testnet') + * @return {string} + */ Wallet.prototype.getNetwork = function() { return this.network; }; +/** + * @desc Throws an error if an address already exists in the address book + * @private + */ Wallet.prototype._checkAddressBook = function(key) { if (this.addressBook[key] && this.addressBook[key].copayerId != -1) { throw new Error('This address already exists in your Address Book: ' + address); } }; +/** + * @desc Add an entry to the address book + * + * @param {string} key - the address to be added + * @param {string} label - a name for the address + */ Wallet.prototype.setAddressBook = function(key, label) { this._checkAddressBook(key); var copayerId = this.getMyCopayerId(); @@ -1813,6 +2315,14 @@ Wallet.prototype.setAddressBook = function(key, label) { this.store(); }; +/** + * @desc Verifies that an addressbook entry is correctly signed by a copayer + * + * @param {Object} rcvEntry - the entry in the address book + * @param {string} senderId - the pubkey of a copayer + * @param {string} key - the base58 encoded address + * @return {boolean} true if the signature matches + */ Wallet.prototype.verifyAddressbookEntry = function(rcvEntry, senderId, key) { if (!key) throw new Error('Keys are required'); var signature = rcvEntry.signature; @@ -1823,29 +2333,53 @@ Wallet.prototype.verifyAddressbookEntry = function(rcvEntry, senderId, key) { createdTs: rcvEntry.createdTs }; return this.verifySignedJson(senderId, payload, signature); -} +}; +/** + * @desc Hides or unhides an address book entry + * @param {string} key - the address in the addressbook + */ Wallet.prototype.toggleAddressBookEntry = function(key) { if (!key) throw new Error('Key is required'); this.addressBook[key].hidden = !this.addressBook[key].hidden; this.store(); }; +/** + * @desc Returns true if there are more than one cosigners + * @return {boolean} + */ Wallet.prototype.isShared = function() { return this.totalCopayers > 1; -} +}; +/** + * @desc Returns true if the keyring is complete and all users have backed up the wallet + * @return {boolean} + */ Wallet.prototype.isReady = function() { var ret = this.publicKeyRing.isComplete() && this.publicKeyRing.isFullyBackup(); return ret; }; +/** + * @desc Mark that our backup is ready and send a sync to other users. + * + * Also backs up the wallet + */ Wallet.prototype.setBackupReady = function() { this.publicKeyRing.setBackupReady(); this.sendPublicKeyRing(); this.store(); }; +/** + * @desc Sign a JSON + * + * @TODO: THIS WON'T WORK ALLWAYS! JSON.stringify doesn't warants an order + * @param {Object} payload - the payload to verify + * @return {string} base64 encoded string + */ Wallet.prototype.signJson = function(payload) { var key = new bitcore.Key(); key.private = new Buffer(this.getMyCopayerIdPriv(), 'hex'); @@ -1854,6 +2388,16 @@ Wallet.prototype.signJson = function(payload) { return sign.toString('hex'); } +/** + * @desc Verify that a JSON object is correctly signed + * + * @TODO: THIS WON'T WORK ALLWAYS! JSON.stringify doesn't warants an order + * + * @param {string} senderId - a sender's public key, hex encoded + * @param {Object} payload - the object to verify + * @param {string} signature - a sender's public key, hex encoded + * @return {boolean} + */ Wallet.prototype.verifySignedJson = function(senderId, payload, signature) { var pubkey = new Buffer(senderId, 'hex'); var sign = new Buffer(signature, 'hex'); @@ -1870,6 +2414,10 @@ Wallet.prototype.verifySignedJson = function(senderId, payload, signature) { // var $http = angular.bootstrap().get('$http'); // } +/** + * @desc Create a HTTP request + * @TODO: This shouldn't be a wallet responsibility + */ Wallet.request = function(options, callback) { if (_.isString(options)) { options = { From ca260e6a009a58ba879efb40a03af576d50ca2d4 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Fri, 5 Sep 2014 11:58:02 -0300 Subject: [PATCH 07/13] Fixes dependency that was not needed --- bower.json | 2 +- karma.conf.js | 2 +- package.json | 2 +- util/build.js | 3 --- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/bower.json b/bower.json index ffd5637f6..e5060c602 100644 --- a/bower.json +++ b/bower.json @@ -23,7 +23,7 @@ "zeroclipboard": "~1.3.5", "ng-idle": "*", "underscore": "~1.7.0", - "assert": "~0.1.0" + "inherits": "~0.0.1" }, "resolutions": { "angular": "=1.2.19" diff --git a/karma.conf.js b/karma.conf.js index ba13832ef..331f325ac 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -28,7 +28,7 @@ module.exports = function(config) { 'lib/angular-route/angular-route.min.js', 'lib/angular-foundation/mm-foundation.min.js', 'lib/angular-foundation/mm-foundation-tpls.min.js', - 'lib/assert/assert.js', + 'lib/inherits/inherits.js', 'lib/bitcore.js', 'lib/underscore/underscore.js', 'lib/crypto-js/rollups/sha256.js', diff --git a/package.json b/package.json index 10ffd2da7..97c4375c4 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ }, "version": "0.4.7", "dependencies": { - "assert": "^1.1.2", "browser-request": "^0.3.2", + "inherits": "^2.0.1", "mocha": "^1.18.2", "mocha-lcov-reporter": "0.0.1", "optimist": "^0.6.1", diff --git a/util/build.js b/util/build.js index d585219d6..6f15143ef 100644 --- a/util/build.js +++ b/util/build.js @@ -46,9 +46,6 @@ var createBundle = function(opts) { b.require('underscore', { expose: 'underscore' }); - b.require('assert', { - expose: 'assert' - }); b.require('./copay', { expose: 'copay' From 323bc2bde0d1edbe3200bb7dcb5c25b672d2e111 Mon Sep 17 00:00:00 2001 From: Gustavo Maximiliano Cortez Date: Mon, 8 Sep 2014 10:25:16 -0300 Subject: [PATCH 08/13] Fixes camera when joining --- js/controllers/join.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/controllers/join.js b/js/controllers/join.js index fcf9fd8d4..34d4a7231 100644 --- a/js/controllers/join.js +++ b/js/controllers/join.js @@ -17,7 +17,7 @@ angular.module('copayApp.controllers').controller('JoinController', $scope.hideAdv=true; - + navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; var _scan = function(evt) { if (localMediaStream) { From 9543fa851dd0e357331b9526884643d8fe34d65f Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Tue, 2 Sep 2014 22:27:47 -0300 Subject: [PATCH 09/13] JSDocs and beautifycation for HDPath --- js/models/core/HDPath.js | 110 +++++++++++++++++++++++++++++++-------- 1 file changed, 87 insertions(+), 23 deletions(-) diff --git a/js/models/core/HDPath.js b/js/models/core/HDPath.js index a2afdbc14..3960284bf 100644 --- a/js/models/core/HDPath.js +++ b/js/models/core/HDPath.js @@ -1,53 +1,117 @@ 'use strict'; +// 90.2% typed (by google's closure-compiler account) + var preconditions = require('preconditions').singleton(); +var _ = require('underscore'); -function HDPath() {} - -/* +/** + * @namespace + * + * HDPath contains helper functions to handle BIP32 branches as + * Copay uses them. + * * Based on https://github.com/maraoz/bips/blob/master/bip-NNNN.mediawiki - * m / purpose' / copayerIndex / change / addressIndex + *
+ * m / purpose' / copayerIndex / change:boolean / addressIndex
+ * 
*/ -var PURPOSE = 45; -var MAX_NON_HARDENED = 0x80000000 - 1; +var HDPath = {}; -var SHARED_INDEX = MAX_NON_HARDENED - 0; -var ID_INDEX = MAX_NON_HARDENED - 1; +/** + * @desc Copay's BIP45 purpose code + * @const + * @type number + */ +HDPath.PURPOSE = 45; -var BIP45_PUBLIC_PREFIX = 'm/' + PURPOSE + '\''; -HDPath.BIP45_PUBLIC_PREFIX = BIP45_PUBLIC_PREFIX; +/** + * @desc Maximum number for non-hardened values (BIP32) + * @const + * @type number + */ +HDPath.MAX_NON_HARDENED = 0x80000000 - 1; +/** + * @desc Shared Index: used for creating addresses for no particular purpose + * @const + * @type number + */ +HDPath.SHARED_INDEX = HDPath.MAX_NON_HARDENED - 0; + +/** + * @desc ??? + * @const + * @type number + */ +HDPath.ID_INDEX = HDPath.MAX_NON_HARDENED - 1; + +/** + * @desc BIP45 prefix for COPAY + * @const + * @type string + */ +HDPath.BIP45_PUBLIC_PREFIX = 'm/' + HDPath.PURPOSE + '\''; + +/** + * @desc Retrieve a string to be used with bitcore representing a Copay branch + * @param {number} addressIndex - the last value of the HD derivation + * @param {boolean} isChange - whether this is a change address or a receive + * @param {number} copayerIndex - the index of the copayer in the pubkeyring + * @return {string} - the path for the HD derivation + */ HDPath.Branch = function(addressIndex, isChange, copayerIndex) { - preconditions.shouldBeNumber(addressIndex); - preconditions.shouldBeBoolean(isChange); + preconditions.checkArgument(_.isNumber(addressIndex)); + preconditions.checkArgument(_.isBoolean(isChange)); + var ret = 'm/' + - (typeof copayerIndex !== 'undefined' ? copayerIndex : SHARED_INDEX) + '/' + + (typeof copayerIndex !== 'undefined' ? copayerIndex : HDPath.SHARED_INDEX) + '/' + (isChange ? 1 : 0) + '/' + addressIndex; return ret; }; +/** + * @desc ??? + * @param {number} addressIndex - the last value of the HD derivation + * @param {boolean} isChange - whether this is a change address or a receive + * @param {number} copayerIndex - the index of the copayer in the pubkeyring + * @return {string} - the path for the HD derivation + */ HDPath.FullBranch = function(addressIndex, isChange, copayerIndex) { + preconditions.checkArgument(_.isNumber(addressIndex)); + preconditions.checkArgument(_.isBoolean(isChange)); + var sub = HDPath.Branch(addressIndex, isChange, copayerIndex); sub = sub.substring(2); - return BIP45_PUBLIC_PREFIX + '/' + sub; + return HDPath.BIP45_PUBLIC_PREFIX + '/' + sub; }; +/** + * @desc + * Decompose a string and retrieve its arguments as if it where a Copay address. + * @param {string} path - the HD path + * @returns {Object} an object with three keys: addressIndex, isChange, and + * copayerIndex + */ HDPath.indexesForPath = function(path) { - preconditions.shouldBeString(path); + preconditions.checkArgument(_.isString(path)); + var s = path.split('/'); return { isChange: s[3] === '1', - addressIndex: parseInt(s[4]), - copayerIndex: parseInt(s[2]) + addressIndex: parseInt(s[4], 10), + copayerIndex: parseInt(s[2], 10) }; }; -HDPath.IdFullBranch = HDPath.FullBranch(0, false, ID_INDEX); -HDPath.IdBranch = HDPath.Branch(0, false, ID_INDEX); -HDPath.PURPOSE = PURPOSE; -HDPath.MAX_NON_HARDENED = MAX_NON_HARDENED; -HDPath.SHARED_INDEX = SHARED_INDEX; -HDPath.ID_INDEX = ID_INDEX; +/** + * @desc The ID for a shared branch + */ +HDPath.IdFullBranch = HDPath.FullBranch(0, false, HDPath.ID_INDEX); +/** + * @desc Partial ID for a shared branch + */ +HDPath.IdBranch = HDPath.Branch(0, false, HDPath.ID_INDEX); module.exports = HDPath; From 6fa100561568a0d07177550768247b45774334ef Mon Sep 17 00:00:00 2001 From: Matias Pando Date: Fri, 5 Sep 2014 16:17:54 -0300 Subject: [PATCH 10/13] Subscribe again to receive messages --- js/models/network/Async.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/models/network/Async.js b/js/models/network/Async.js index 19cf047aa..483e3fce9 100644 --- a/js/models/network/Async.js +++ b/js/models/network/Async.js @@ -235,7 +235,9 @@ Network.prototype._setupConnectionHandlers = function(cb) { self.socket.on('connect', function() { self.socket.on('disconnect', function() { - self.cleanUp(); + //self.cleanUp(); + var pubKey = self.getKey().public.toString('hex'); + self.socket.emit('subscribe', pubKey); }); if (typeof cb === 'function') cb(); From d96d9b8d61ff31634b841bb54651e125ccb60d6d Mon Sep 17 00:00:00 2001 From: Matias Pando Date: Mon, 8 Sep 2014 12:11:35 -0300 Subject: [PATCH 11/13] Delete commented line --- js/models/network/Async.js | 1 - 1 file changed, 1 deletion(-) diff --git a/js/models/network/Async.js b/js/models/network/Async.js index 483e3fce9..ad4a5630e 100644 --- a/js/models/network/Async.js +++ b/js/models/network/Async.js @@ -235,7 +235,6 @@ Network.prototype._setupConnectionHandlers = function(cb) { self.socket.on('connect', function() { self.socket.on('disconnect', function() { - //self.cleanUp(); var pubKey = self.getKey().public.toString('hex'); self.socket.emit('subscribe', pubKey); }); From 664c02dc84227e55f2096619732a7b33be6c8457 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 8 Sep 2014 14:27:58 -0300 Subject: [PATCH 12/13] Set seen on incoming tx proposal --- js/models/core/Wallet.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js/models/core/Wallet.js b/js/models/core/Wallet.js index 6ab8e9def..745146129 100644 --- a/js/models/core/Wallet.js +++ b/js/models/core/Wallet.js @@ -337,6 +337,7 @@ Wallet.prototype._onTxProposal = function(senderId, data) { if (m) { if (m.hasChanged) { + m.txp.setSeen(this.getMyCopayerId()); this.sendSeen(m.ntxid); var tx = m.txp.builder.build(); if (tx.isComplete()) { From 2b51394841b72149238a74f9ecb89028120c939c Mon Sep 17 00:00:00 2001 From: Gustavo Maximiliano Cortez Date: Mon, 8 Sep 2014 15:00:08 -0300 Subject: [PATCH 13/13] postinstall: grunt --- package.json | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 97c4375c4..6a3c094fe 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "test": "sh test/run.sh", "dist": "node shell/scripts/dist.js", "sign": "gpg -u 1112CFA1 --output browser-extensions/firefox/copay.xpi.sig --detach-sig browser-extensions/firefox/copay.xpi; gpg -u 1112CFA1 --output browser-extensions/chrome/copay-chrome-extension.zip.sig --detach-sig browser-extensions/chrome/copay-chrome-extension.zip", - "verify": "gpg --verify browser-extensions/firefox/copay.xpi.sig browser-extensions/firefox/copay.xpi; gpg --verify browser-extensions/chrome/copay-chrome-extension.zip.sig browser-extensions/chrome/copay-chrome-extension.zip" + "verify": "gpg --verify browser-extensions/firefox/copay.xpi.sig browser-extensions/firefox/copay.xpi; gpg --verify browser-extensions/chrome/copay-chrome-extension.zip.sig browser-extensions/chrome/copay-chrome-extension.zip", + "postinstall": "./node_modules/.bin/grunt" }, "keywords": [ "wallet", @@ -41,23 +42,27 @@ ], "devDependencies": { "async": "0.9.0", + "bitcore": "0.1.35", "blanket": "1.1.6", "browser-pack": "2.0.1", + "browser-request": "0.3.2", "browserify": "3.32.1", "buffertools": "2.0.1", "chai": "1.9.1", "cli-color": "0.3.2", "commander": "2.1.0", "coveralls": "2.10.0", + "crypto-js": "3.1.2", "express": "4.0.0", "github-releases": "0.2.0", + "grunt": "^0.4.5", "grunt-browserify": "2.0.8", + "grunt-cli": "^0.1.13", "grunt-contrib-concat": "0.5.0", "grunt-contrib-cssmin": "0.10.0", "grunt-contrib-uglify": "^0.5.1", "grunt-contrib-watch": "0.5.3", "grunt-markdown": "0.5.0", - "bitcore": "0.1.35", "grunt-mocha-test": "0.8.2", "grunt-shell": "0.6.4", "istanbul": "0.2.10", @@ -70,13 +75,11 @@ "mocha-lcov-reporter": "0.0.1", "mock-fs": "^2.3.1", "node-cryptojs-aes": "0.4.0", + "request": "2.40.0", + "shelljs": "0.3.0", "socket.io-client": "1.0.6", "travis-cov": "0.2.5", - "uglifyify": "1.2.3", - "crypto-js": "3.1.2", - "shelljs": "0.3.0", - "browser-request": "0.3.2", - "request": "2.40.0" + "uglifyify": "1.2.3" }, "main": "app.js", "homepage": "https://github.com/bitpay/copay",