fix indexes, inheritance
This commit is contained in:
parent
7b68c14fb7
commit
6227bb1e5f
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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({
|
||||
|
|
Loading…
Reference in New Issue