'use strict'; // 83.8% typed (by google's closure-compiler account) var preconditions = require('preconditions').singleton(); var HDPath = require('./HDPath'); var _ = require('lodash'); /** * @desc * HDParams is a class that encapsulates information about the current indexes * of a copayer * * When a copayer creates a new wallet, his receiveIndex gets updated, and an * address is generated from everybody's public key, using this BIP32 path: *
m/copay'/{copayer}/0/{index}
*
* When a copayer generates a transaction proposal, his changeIndex gets
* updated, and all funds from that transaction proposal go to the multisig
* address generated from this BIP32 path for all the copayers:
* m/copay'/{copayer}/1/{changeIndex}
*
* There's a shared index, HDPath.SHARED_INDEX, that serves to
* generate addresses common to everybody.
*
* @TODO: Should opts.cosigner go?
*
* @constructor
*
* @param {Object} opts - options for the construction of this object
* @param {number=} opts.cosigner - backwards compatible index of a copayer
* @param {number} opts.copayerIndex - the copayer that generated this branch
* of addresses
* @param {number} opts.receiveIndex - the current index for a the last receive
* address generated for this copayer
* @param {number} opts.changeIndex - the current index for a the last change
* address generated for this copayer
*/
function HDParams(opts) {
opts = opts || {};
//opts.cosigner is for backwards compatibility only
/**
* @public
* @type number
*/
this.copayerIndex = _.isUndefined(opts.copayerIndex) ? opts.cosigner : opts.copayerIndex;
/**
* @public
* @type number
*/
this.changeIndex = opts.changeIndex || 0;
/**
* @public
* @type number
*/
this.receiveIndex = opts.receiveIndex || 0;
if (_.isUndefined(this.copayerIndex)) {
this.copayerIndex = HDPath.SHARED_INDEX;
}
}
/**
* @desc
* Creates a set of HDParams, with one HDParams structure for each copayer and
* a shared path.
*
* @static
* @param {number} totalCopayers - the number of copayers in a wallet
* @returns {HDParams[]} a list of HDParams generated for a new empty wallet
*/
HDParams.init = function(totalCopayers) {
preconditions.shouldBeNumber(totalCopayers);
var ret = [new HDParams({receiveIndex: 1})];
for (var i = 0 ; i < totalCopayers ; i++) {
ret.push(new HDParams({copayerIndex: i}));
}
return ret;
};
/**
* @desc
* Generates a set of HDParams from a list with a object-serialized version of
* HDParams.
*
* @static
* @param {Object[]} hdParams - a list with objects
* @returns {HDParams[]} builds a HDParams for each object literal found in the list
*/
HDParams.fromList = function(hdParams) {
return hdParams.map(function(i) { return HDParams.fromObj(i); });
};
/**
* @desc
* Generate a HDParams from an object.
*
* This essentialy only calls new HDParams(data), but it's the interface being
* used everywhere to encode/decode.
*
* @TODO: This should be clarified - Or abstracted away - as it's a pattern
* used in multiple places.
*
* @static
* @param {Object} data - a serialized version of HDParams
* @return {HDParams}
* @throws {BADDATA} - when the parameter data already is an instance of
* HDParams.
*/
HDParams.fromObj = function(data) {
if (data instanceof HDParams) {
throw new Error('BADDATA', 'bad data format: Did you use .toObj()?');
}
return new HDParams(data);
};
/**
* @desc
* Serializes a list of HDParams to a list of "plain" objects, according to each
* element's toObj() method.
*
* @TODO: There should be a list of Classes that share this behaviour.
*
* @static
* @param {HDParams[]} hdParams - a list of HDParams objects.
* @returns {Array} an array with the toObj() serialization of each
* element in hdParams
*/
HDParams.serialize = function(hdParams) {
return hdParams.map(function(i) { return i.toObj(); });
};
/**
* @desc
* Creates a new HDParams set with totalCopayers+1 elements.
*
* Sets the first (corresponding to the parameters for the shared addresses)
* HDParams object to match shared's values. Returns a serialized
* version of this set
*
*
* var updateResult = HDParams.update({changeIndex: 1, receiveIndex: 2}, 5);
* // All the following asserts succeed
* assert(_.isArray(updateResult));
* assert(_.all(updateResult, function (hd) { return !(hd instanceOf HDParams); }));
* assert(_.size(updateResult) === 6);
* assert(updateResult[0].changeIndex === 1);
* assert(updateResult[0].receiveIndex === 2);
*
*
* @TODO: This method is badly coded, it does something that is very specific
* and kind of strange. I couldn't figure out why would it be needed.
*
* @static
* @param {HDParams} shared - an instance of HDParams
* @param {number} totalCopayers
* @return {Object[]}
*/
HDParams.update = function(shared, totalCopayers) {
var hdParams = this.init(totalCopayers);
hdParams[0].changeIndex = shared.changeIndex;
hdParams[0].receiveIndex = shared.receiveIndex;
return this.serialize(hdParams);
};
/**
* @desc
* Serializes this object
*
* @TODO: I couldn't realize why would this be needed - calling, for example,
* JSON.stringify would have the same result on this object than on the
* original instance
*
* @returns {Object} a serialized version that should be equal (in a deep object
* comparison) to the this instance if passed to the
* HDParams() constructor.
*/
HDParams.prototype.toObj = function() {
return {
copayerIndex: this.copayerIndex,
changeIndex: this.changeIndex,
receiveIndex: this.receiveIndex
};
};
/**
* @desc
* Throws an error if a given index falls out of the range for known addresses.
*
* @TODO: This is not a good pattern, exceptions should be for exceptional things.
*
* @param {number} index the index to check for
* @param {boolean} isChange whether to check for the change index or the
* receive address index
*/
HDParams.prototype.checkRange = function(index, isChange) {
if ((isChange && index > this.changeIndex) ||
(!isChange && index > this.receiveIndex)) {
throw new Error('Out of bounds at index ' + index + ' isChange: ' + isChange);
}
};
/**
* @desc
* Return this instance's changeIndex value
*
* @TODO: Somebody did a lot of java. Not sure if we need to be so verbose. If
* anything, let's just declare changeIndex as a read only variable
* @returns {number} this HDParams current index to generate a change address
*/
HDParams.prototype.getChangeIndex = function() {
return this.changeIndex;
};
/**
* @desc
* Return this instance's receiveIndex value
*
* @TODO: Somebody did a lot of java. Not sure if we need to be so verbose. If
* anything, let's just declare changeIndex as a read only variable
* @returns {number} this HDParams current index to generate a receive address
*/
HDParams.prototype.getReceiveIndex = function() {
return this.receiveIndex;
};
/**
* @desc
* Increment this instance's changeIndex or receiveIndex value
*
* @TODO: Somebody did a lot of java. Not sure if we need to be so verbose.
*
* @param {boolean} isChange - if true, change changeIndex
*/
HDParams.prototype.increment = function(isChange) {
if (isChange) {
this.changeIndex++;
} else {
this.receiveIndex++;
}
};
/**
* @desc
* Merge this instance with another HDParams instance.
*
* @TODO: Device a general approach to merges.
*
* @param {Object} inHDParams - the object to merge to
* @param {number} inHDParams.copayerIndex - the object to merge to
* @returns {boolean} true if this object has changed
*/
HDParams.prototype.merge = function(inHDParams) {
preconditions.shouldBeObject(inHDParams);
preconditions.checkArgument(this.copayerIndex == inHDParams.copayerIndex);
var hasChanged = false;
if (inHDParams.changeIndex > this.changeIndex) {
this.changeIndex = inHDParams.changeIndex;
hasChanged = true;
}
if (inHDParams.receiveIndex > this.receiveIndex) {
this.receiveIndex = inHDParams.receiveIndex;
hasChanged = true;
}
return hasChanged;
};
module.exports = HDParams;