fix indexes, inheritance

This commit is contained in:
Matias Alejo Garcia 2015-02-02 11:55:03 -03:00
parent 7b68c14fb7
commit 6227bb1e5f
7 changed files with 277 additions and 34 deletions

34
lib/model/addressable.js Normal file
View File

@ -0,0 +1,34 @@
var _ = require('lodash');
var HDPath = require('./hdpath');
function Addressable (opts) {
this.receiveAddressIndex = 0;
this.changeAddressIndex = 0;
this.copayerIndex = ( opts && _.isNumber(opts.copayerIndex)) ? opts.copayerIndex : HDPath.SHARED_INDEX;
};
Addressable.prototype.fromObj = function (obj) {
this.receiveAddressIndex = obj.receiveAddressIndex;
this.changeAddressIndex = obj.changeAddressIndex;
this.copayerIndex = obj.copayerIndex;
};
Addressable.prototype.addAddress = function (isChange) {
if (isChange) {
this.changeAddressIndex++;
} else {
this.receiveAddressIndex++;
}
};
Addressable.prototype.getCurrentAddressPath = function (isChange) {
return HDPath.Branch(isChange ? this.changeAddressIndex : this.receiveAddressIndex, isChange, this.copayerIndex);
};
Addressable.prototype.getNewAddressPath = function (isChange) {
this.addAddress(isChange);
return this.currentAddressPath(isChange);
};
module.exports = Addressable;

View File

@ -1,13 +1,19 @@
'use strict';
var _ = require('lodash');
var util = require('util');
var Bitcore = require('bitcore');
var HDPublicKey = Bitcore.HDPublicKey;
var Addressable = require('./Addressable');
var VERSION = '1.0.0';
var MESSAGE_SIGNING_PATH = "m/1/0";
function Copayer(opts) {
Copayer.super_.apply(this, arguments);
opts = opts || {};
this.version = VERSION;
@ -19,6 +25,8 @@ function Copayer(opts) {
this.signingPubKey = opts.signingPubKey || this.getSigningPubKey();
};
util.inherits(Copayer, Addressable);
Copayer.prototype.getSigningPubKey = function () {
if (!this.xPubKey) return null;
return HDPublicKey.fromString(this.xPubKey).derive(MESSAGE_SIGNING_PATH).publicKey.toString();
@ -32,7 +40,6 @@ Copayer.prototype.addAddress = function (isChange) {
}
};
Copayer.prototype.getCurrentAddressPath = function (isChange) {
return
};
@ -51,7 +58,9 @@ Copayer.fromObj = function (obj) {
x.xPubKey = obj.xPubKey;
x.xPubKeySignature = obj.xPubKeySignature;
x.signingPubKey = obj.signingPubKey;
Wallet.super_.prototype.fromObj.apply(this, [obj]);
return x;
};
module.exports = Copayer;

116
lib/model/hdpath.js Normal file
View File

@ -0,0 +1,116 @@
'use strict';
// 90.2% typed (by google's closure-compiler account)
var preconditions = require('preconditions').singleton();
var _ = require('lodash');
/**
* @namespace
* @desc
* HDPath contains helper functions to handle BIP32 branches as
* Copay uses them.
* Based on https://github.com/maraoz/bips/blob/master/bip-NNNN.mediawiki
* <pre>
* m / purpose' / copayerIndex / change:boolean / addressIndex
* </pre>
*/
var HDPath = {};
/**
* @desc Copay's BIP45 purpose code
* @const
* @type number
*/
HDPath.PURPOSE = 45;
/**
* @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.checkArgument(_.isNumber(addressIndex));
preconditions.checkArgument(_.isBoolean(isChange));
var ret = 'm/' +
(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 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.checkArgument(_.isString(path));
var s = path.split('/');
return {
isChange: s[3] === '1',
addressIndex: parseInt(s[4], 10),
copayerIndex: parseInt(s[2], 10)
};
};
/**
* @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,11 +1,15 @@
'use strict';
var _ = require('lodash');
var util = require('util');
var Copayer = require('./copayer');
var Addressable = require('./Addressable');
var VERSION = '1.0.0';
function Wallet(opts) {
Wallet.super_.apply(this, arguments);
opts = opts || {};
this.version = VERSION;
@ -19,9 +23,6 @@ function Wallet(opts) {
this.addressIndex = 0;
this.copayers = [];
this.pubKey = opts.pubKey;
this.receiveAddressIndex = 0;
this.changeAddressIndex = 0;
};
/* For compressed keys, m*73 + n*34 <= 496 */
@ -39,6 +40,7 @@ Wallet.COPAYER_PAIR_LIMITS = {
11: 1,
12: 1,
};
util.inherits(Wallet, Addressable);
/**
* Get the maximum allowed number of required copayers.
@ -57,7 +59,6 @@ Wallet.verifyCopayerLimits = function (m, n) {
Wallet.fromObj = function (obj) {
var x = new Wallet();
x.version = obj.version;
x.createdOn = obj.createdOn;
x.id = obj.id;
x.name = obj.name;
@ -65,12 +66,12 @@ Wallet.fromObj = function (obj) {
x.n = obj.n;
x.status = obj.status;
x.publicKeyRing = obj.publicKeyRing;
x.addressIndex = obj.addressIndex;
x.copayers = _.map(obj.copayers, function (copayer) {
return new Copayer(copayer);
});
x.pubKey = obj.pubKey;
Wallet.super_.prototype.fromObj.apply(this, [obj]);
return x;
};

View File

@ -75,10 +75,10 @@ CopayServer.prototype.createWallet = function (opts, cb) {
name: opts.name,
m: opts.m,
n: opts.n,
network: network,
network: opts.network || 'livenet',
pubKey: pubKey,
});
self.storage.storeWallet(wallet, cb);
});
};
@ -87,7 +87,7 @@ CopayServer.prototype.createWallet = function (opts, cb) {
* Retrieves a wallet from storage.
* @param {Object} opts
* @param {string} opts.id - The wallet id.
* @returns {Object} wallet
* @returns {Object} wallet
*/
CopayServer.prototype.getWallet = function (opts, cb) {
var self = this;
@ -106,8 +106,8 @@ CopayServer.prototype.createWallet = function (opts, cb) {
* @param signature
* @param pubKey
*/
CopayServer.prototype._verifySignature = function (text, signature, pubKey) {
return SignUtils.verify( text, signature, pubKey);
CopayServer.prototype._verifySignature = function(text, signature, pubKey) {
return SignUtils.verify(text, signature, pubKey);
};
/**
@ -132,8 +132,11 @@ CopayServer.prototype._verifySignature = function (text, signature, pubKey) {
return cb('Bad request');
}
if (_.find(wallet.copayers, { xPubKey: opts.xPubKey })) return cb('Copayer already in wallet');
if (_.find(wallet.copayers, {
xPubKey: opts.xPubKey
})) return cb('Copayer already in wallet');
if (wallet.copayers.length == wallet.n) return cb('Wallet full');
var copayer = new Copayer({
id: opts.id,
name: opts.name,
@ -145,18 +148,12 @@ CopayServer.prototype._verifySignature = function (text, signature, pubKey) {
wallet.addCopayer(copayer);
self.storage.storeWallet(wallet, function (err) {
if (err) return cb(err);
return cb();
return cb(err);
});
});
});
};
CopayServer.prototype._doCreateAddress = function (pkr, index, isChange) {
throw 'not implemented';
};
/**
*
* TODO: How this is going to be authenticated?
@ -165,7 +162,7 @@ CopayServer.prototype._doCreateAddress = function (pkr, index, isChange) {
* @param {Object} opts
* @param {string} opts.walletId - The wallet id.
* @param {truthy} opts.isChange - Indicates whether this is a regular address or a change address.
* @returns {Address} address
* @returns {Address} address
*/
CopayServer.prototype.createAddress = function (opts, cb) {
var self = this;
@ -175,13 +172,12 @@ CopayServer.prototype._doCreateAddress = function (pkr, index, isChange) {
Utils.runLocked(opts.walletId, cb, function (cb) {
self.getWallet({ id: opts.walletId }, function (err, wallet) {
if (err) return cb(err);
var index = wallet.addressIndex++;
self.storage.storeWallet(wallet, function (err) {
self.storage.storeWallet(wallet, function(err) {
if (err) return cb(err);
var address = self._doCreateAddress(wallet.publicKeyRing, index, opts.isChange);
self.storage.storeAddress(opts.walletId, address, function (err) {
self.storage.storeAddress(opts.walletId, address, function(err) {
if (err) return cb(err);
return cb(null, address);
@ -268,7 +264,6 @@ CopayServer.prototype._getUtxos = function (opts, cb) {
dictionary[input].locked = true;
}
});
return cb(null, utxos);
});
});
@ -280,7 +275,7 @@ CopayServer.prototype._getUtxos = function (opts, cb) {
* Creates a new transaction proposal.
* @param {Object} opts
* @param {string} opts.walletId - The wallet id.
* @returns {Object} balance - Total amount & locked amount.
* @returns {Object} balance - Total amount & locked amount.
*/
CopayServer.prototype.getBalance = function (opts, cb) {
var self = this;
@ -299,7 +294,7 @@ CopayServer.prototype._getUtxos = function (opts, cb) {
};
CopayServer.prototype._createRawTx = function (txp) {
CopayServer.prototype._createRawTx = function(txp) {
var rawTx = new Bitcore.Transaction()
.from(tx.inputs)
.to(txp.toAddress, txp.amount)
@ -308,7 +303,7 @@ CopayServer.prototype._createRawTx = function (txp) {
return rawTx;
};
CopayServer.prototype._selectUtxos = function (txp, utxos) {
CopayServer.prototype._selectUtxos = function(txp, utxos) {
var i = 0;
var total = 0;
var selected = [];
@ -333,7 +328,7 @@ CopayServer.prototype._selectUtxos = function (txp, utxos) {
* @param {string} opts.toAddress - Destination address.
* @param {number} opts.amount - Amount to transfer in satoshi.
* @param {string} opts.message - A message to attach to this transaction.
* @returns {TxProposal} Transaction proposal.
* @returns {TxProposal} Transaction proposal.
*/
CopayServer.prototype.createTx = function (opts, cb) {
var self = this;
@ -414,7 +409,7 @@ CopayServer.prototype.signTx = function (opts, cb) {
});
});
});
};
};
/**
* Reject a transaction proposal.
@ -450,7 +445,7 @@ CopayServer.prototype.rejectTx = function (opts, cb) {
* Retrieves all pending transaction proposals.
* @param {Object} opts
* @param {string} opts.walletId - The wallet id.
* @returns {TxProposal[]} Transaction proposal.
* @returns {TxProposal[]} Transaction proposal.
*/
CopayServer.prototype.getPendingTxs = function (opts, cb) {
var self = this;
@ -461,7 +456,6 @@ CopayServer.prototype.getPendingTxs = function (opts, cb) {
if (err) return cb(err);
var pending = _.filter(txps, { status: 'pending' });
return cb(null, pending);
});
};

71
test/hdpath.js Normal file
View File

@ -0,0 +1,71 @@
'use strict';
var HDPath = require('../lib/model/hdpath');
describe('HDPath model', function() {
it('should have the correct constants', function() {
HDPath.MAX_NON_HARDENED.should.equal(Math.pow(2, 31) - 1);
HDPath.SHARED_INDEX.should.equal(HDPath.MAX_NON_HARDENED);
HDPath.ID_INDEX.should.equal(HDPath.SHARED_INDEX - 1);
HDPath.IdFullBranch.should.equal('m/45\'/2147483646/0/0');
});
it('should get the correct branches', function() {
// shared branch (no cosigner index specified)
HDPath.FullBranch(0, false).should.equal('m/45\'/2147483647/0/0');
// copayer 0, address 0, external address (receiving)
HDPath.FullBranch(0, false, 0).should.equal('m/45\'/0/0/0');
// copayer 0, address 10, external address (receiving)
HDPath.FullBranch(0, false, 10).should.equal('m/45\'/10/0/0');
// copayer 0, address 0, internal address (change)
HDPath.FullBranch(0, true, 0).should.equal('m/45\'/0/1/0');
// copayer 0, address 10, internal address (change)
HDPath.FullBranch(10, true, 0).should.equal('m/45\'/0/1/10');
// copayer 7, address 10, internal address (change)
HDPath.FullBranch(10, true, 7).should.equal('m/45\'/7/1/10');
});
[
['m/45\'/0/0/0', {
index: 0,
isChange: false
}],
['m/45\'/0/0/1', {
index: 1,
isChange: false
}],
['m/45\'/0/0/2', {
index: 2,
isChange: false
}],
['m/45\'/0/1/0', {
index: 0,
isChange: true
}],
['m/45\'/0/1/1', {
index: 1,
isChange: true
}],
['m/45\'/0/1/2', {
index: 2,
isChange: true
}],
['m/45\'/0/0/900', {
index: 900,
isChange: false
}],
].forEach(function(datum) {
var path = datum[0];
var result = datum[1];
it('should get the correct indexes for path ' + path, function() {
var i = HDPath.indexesForPath(path);
i.addressIndex.should.equal(result.index);
i.isChange.should.equal(result.isChange);
});
});
});

View File

@ -69,6 +69,7 @@ helpers.createAndJoinWallet = function(id, m, n, cb) {
if (err) return cb(err);
async.each(_.range(1, n + 1), function(i, cb) {
var copayerOpts = {
walletId: id,
id: '' + i,
@ -76,11 +77,13 @@ helpers.createAndJoinWallet = function(id, m, n, cb) {
xPubKey: someXPubKeys[i - 1],
xPubKeySignature: someXPubKeysSignatures[i - 1],
};
server.joinWallet(copayerOpts, function(err) {
return cb(err);
});
}, function(err) {
if (err) return cb(err);
server.getWallet({
id: id,
includeCopayers: true
@ -461,7 +464,20 @@ describe('Copay server', function() {
});
});
it('should set index in 1-1 wallet creation.', function(done) {
helpers.createAndJoinWallet('123', 1, 1, function(err, wallet) {
wallet.receiveAddressIndex.should.equal(0);
wallet.changeAddressIndex.should.equal(0);
wallet.copayerIndex.should.equal(0x80000000 - 1);
var copayer = wallet.copayers[0];
copayer.receiveAddressIndex.should.equal(0);
copayer.changeAddressIndex.should.equal(0);
copayer.copayerIndex.should.equal(0);
done();
});
});
it('should set pkr and status = complete on last copayer joining', function(done) {
helpers.createAndJoinWallet('123', 2, 3, function(err, wallet) {
@ -472,7 +488,10 @@ describe('Copay server', function() {
wallet.status.should.equal('complete');
wallet.publicKeyRing.length.should.equal(3);
_.each([0,1,2], function(i) {
wallet.copayers[i].copayerIndex.should.equal(i);
var copayer = wallet.copayers[i];
copayer.receiveAddressIndex.should.equal(0);
copayer.changeAddressIndex.should.equal(0);
copayer.copayerIndex.should.equal(i);
});
done();
});
@ -481,7 +500,6 @@ describe('Copay server', function() {
});
describe('#verifyMessageSignature', function() {
beforeEach(function() {
server = new CopayServer({