This commit is contained in:
Gustavo Maximiliano Cortez 2014-09-08 15:15:57 -03:00
commit 550d82cacd
15 changed files with 1269 additions and 259 deletions

View File

@ -24,7 +24,7 @@
"zeroclipboard": "~1.3.5",
"ng-idle": "*",
"underscore": "~1.7.0",
"assert": "~0.1.0"
"inherits": "~0.0.1"
},
"resolutions": {
"angular": "=1.2.19"

View File

@ -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) {

View File

@ -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;

View File

@ -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
* <pre>
* m / purpose' / copayerIndex / change:boolean / addressIndex
* </pre>
*/
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;

View File

@ -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 <tt>data</tt> 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;

View File

@ -4,7 +4,6 @@ var bitcore = require('bitcore');
var _ = require('underscore');
var util = bitcore.util;
var Transaction = bitcore.Transaction;
var BuilderMockV0 = require('./BuilderMockV0');;
var TransactionBuilder = bitcore.TransactionBuilder;
var Script = bitcore.Script;
var Key = bitcore.Key;
@ -134,12 +133,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("Invalid or Incompatible Backup Detected.");
}
return new TxProposal(o);
};

View File

@ -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;

File diff suppressed because it is too large Load Diff

View File

@ -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
* <ul>
* <li><tt>publicKeyRing</tt></li>
* <li><tt>txProposals</tt></li>
* <li><tt>opts</tt></li>
* <li><tt>privateKey</tt></li>
* </ul>
* @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 <tt>secret</tt> using the parameter <tt>nickname</tt>. Encode
* information locally using <tt>passphrase</tt>. <tt>privateHex</tt> is the
* private extended master key. <tt>cb</tt> 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);

View File

@ -235,7 +235,8 @@ 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);
});
if (typeof cb === 'function') cb();

View File

@ -29,7 +29,7 @@ module.exports = function(config) {
'lib/angular-foundation/mm-foundation.min.js',
'lib/angular-foundation/mm-foundation-tpls.min.js',
'lib/angular-gettext/dist/angular-gettext.min.js',
'lib/assert/assert.js',
'lib/inherits/inherits.js',
'lib/bitcore.js',
'lib/underscore/underscore.js',
'lib/crypto-js/rollups/sha256.js',

View File

@ -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",
@ -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",
"grunt-angular-gettext": "^0.2.15",
@ -71,13 +76,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",

View File

@ -133,7 +133,7 @@ describe('TxProposal', function() {
builderObj: b.toObj(),
inputChainPaths: ['m/1'],
});
}).should.throw('Invalid');
}).should.throw('Invalid or Incompatible Backup Detected');
});

View File

@ -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) {

View File

@ -46,9 +46,6 @@ var createBundle = function(opts) {
b.require('underscore', {
expose: 'underscore'
});
b.require('assert', {
expose: 'assert'
});
b.require('./copay', {
expose: 'copay'