Ivan Socolsky
10 years ago
9 changed files with 460 additions and 47 deletions
@ -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.getCurrentAddressPath(isChange); |
||||
|
}; |
||||
|
|
||||
|
module.exports = Addressable; |
@ -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; |
@ -0,0 +1,28 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
var _ = require('lodash'); |
||||
|
var chai = require('chai'); |
||||
|
var sinon = require('sinon'); |
||||
|
var should = chai.should(); |
||||
|
var Copayer = require('../lib/model/copayer'); |
||||
|
|
||||
|
|
||||
|
describe('Copayer', function() { |
||||
|
|
||||
|
describe('#getCurrentAddressPath', function() { |
||||
|
it('return a valid BIP32 path for defaut copayer Index', function() { |
||||
|
var c = new Copayer(); |
||||
|
c.getCurrentAddressPath(false).should.equal('m/0/0/0'); |
||||
|
c.getCurrentAddressPath(true).should.equal('m/0/1/0'); |
||||
|
}); |
||||
|
|
||||
|
it('return a valid BIP32 path for given index', function() { |
||||
|
var c = new Copayer({ |
||||
|
copayerIndex: 4 |
||||
|
}); |
||||
|
c.getCurrentAddressPath(false).should.equal('m/4/0/0'); |
||||
|
c.getCurrentAddressPath(true).should.equal('m/4/1/0'); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
}); |
@ -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); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
@ -0,0 +1,91 @@ |
|||||
|
'use strict'; |
||||
|
|
||||
|
var _ = require('lodash'); |
||||
|
var chai = require('chai'); |
||||
|
var sinon = require('sinon'); |
||||
|
var should = chai.should(); |
||||
|
var Wallet = require('../lib/model/wallet'); |
||||
|
|
||||
|
|
||||
|
describe('Wallet', function() { |
||||
|
|
||||
|
describe('#fromObj', function() { |
||||
|
it('read a wallet', function() { |
||||
|
var w = Wallet.fromObj(testWallet); |
||||
|
w.status.should.equal('complete'); |
||||
|
}); |
||||
|
}); |
||||
|
describe('#createAddress', function() { |
||||
|
it('create an address', function() { |
||||
|
var w = Wallet.fromObj(testWallet); |
||||
|
var a = w.createAddress('m/1/1'); |
||||
|
a.address.should.equal('32HG4C9tWMhWoDoTHFvjmbV5sUJMjWs4vL'); |
||||
|
a.path.should.equal('m/1/1'); |
||||
|
a.createdOn.should.be.above(1); |
||||
|
}); |
||||
|
}); |
||||
|
describe('#getCurrentAddressPath', function() { |
||||
|
it('return a valid BIP32 path for defaut wallet Index', function() { |
||||
|
var w = new Wallet(); |
||||
|
w.getCurrentAddressPath(false).should.equal('m/2147483647/0/0'); |
||||
|
w.getCurrentAddressPath(true).should.equal('m/2147483647/1/0'); |
||||
|
}); |
||||
|
|
||||
|
|
||||
|
}); |
||||
|
|
||||
|
}); |
||||
|
|
||||
|
|
||||
|
var testWallet = { |
||||
|
receiveAddressIndex: 0, |
||||
|
changeAddressIndex: 0, |
||||
|
copayerIndex: 2147483647, |
||||
|
createdOn: 1422904188, |
||||
|
id: '123', |
||||
|
name: '123 wallet', |
||||
|
m: 2, |
||||
|
n: 3, |
||||
|
status: 'complete', |
||||
|
publicKeyRing: ['xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9', |
||||
|
'xpub661MyMwAqRbcEzHgVwwxoXksq21rRNsJsn7AFy4VD4PzsEmjjWwsyEiTjsdQviXbqZ5yHVWJR8zFUDgUKkq4R97su3UyNo36Z8hSaCPrv6o', |
||||
|
'xpub661MyMwAqRbcFXUfkjfSaRwxJbAPpzNUvTiNFjgZwDJ8sZuhyodkP24L4LvsrgThYAAwKkVVSSmL7Ts7o9EHEHPB3EE89roAra7njoSeiMd' |
||||
|
], |
||||
|
copayers: [{ |
||||
|
receiveAddressIndex: 0, |
||||
|
changeAddressIndex: 0, |
||||
|
copayerIndex: 0, |
||||
|
createdOn: 1422904189, |
||||
|
id: '1', |
||||
|
name: 'copayer 1', |
||||
|
xPubKey: 'xpub661MyMwAqRbcFLRkhYzK8eQdoywNHJVsJCMQNDoMks5bZymuMcyDgYfnVQYq2Q9npnVmdTAthYGc3N3uxm5sEdnTpSqBc4YYTAhNnoSxCm9', |
||||
|
xPubKeySignature: '30440220192ae7345d980f45f908bd63ccad60ce04270d07b91f1a9d92424a07a38af85202201591f0f71dd4e79d9206d2306862e6b8375e13a62c193953d768e884b6fb5a46', |
||||
|
version: '1.0.0', |
||||
|
signingPubKey: '03814ac7decf64321a3c6967bfb746112fdb5b583531cd512cc3787eaf578947dc' |
||||
|
}, { |
||||
|
receiveAddressIndex: 0, |
||||
|
changeAddressIndex: 0, |
||||
|
copayerIndex: 1, |
||||
|
createdOn: 1422904189, |
||||
|
id: '2', |
||||
|
name: 'copayer 2', |
||||
|
xPubKey: 'xpub661MyMwAqRbcEzHgVwwxoXksq21rRNsJsn7AFy4VD4PzsEmjjWwsyEiTjsdQviXbqZ5yHVWJR8zFUDgUKkq4R97su3UyNo36Z8hSaCPrv6o', |
||||
|
xPubKeySignature: '30440220134d13139323ba16ff26471c415035679ee18b2281bf85550ccdf6a370899153022066ef56ff97091b9be7dede8e40f50a3a8aad8205f2e3d8e194f39c20f3d15c62', |
||||
|
version: '1.0.0', |
||||
|
signingPubKey: '03fc086d2bd8b6507b1909b24c198c946e68775d745492ea4ca70adfce7be92a60' |
||||
|
}, { |
||||
|
receiveAddressIndex: 0, |
||||
|
changeAddressIndex: 0, |
||||
|
copayerIndex: 2, |
||||
|
createdOn: 1422904189, |
||||
|
id: '3', |
||||
|
name: 'copayer 3', |
||||
|
xPubKey: 'xpub661MyMwAqRbcFXUfkjfSaRwxJbAPpzNUvTiNFjgZwDJ8sZuhyodkP24L4LvsrgThYAAwKkVVSSmL7Ts7o9EHEHPB3EE89roAra7njoSeiMd', |
||||
|
xPubKeySignature: '304402207a4e7067d823a98fa634f9c9d991b8c42cd0f82da24f686992acf96cdeb5e387022021ceba729bf763fc8e4277f6851fc2b856a82a22b35f20d2eeb23d99c5f5a41c', |
||||
|
version: '1.0.0', |
||||
|
signingPubKey: '0246c30040eda1e36e02629ae8cd2a845fcfa947239c4c703f7ea7550d39cfb43a' |
||||
|
}], |
||||
|
version: '1.0.0', |
||||
|
pubKey: '{"x":"6092daeed8ecb2212869395770e956ffc9bf453f803e700f64ffa70c97a00d80","y":"ba5e7082351115af6f8a9eb218979c7ed1f8aa94214f627ae624ab00048b8650","compressed":true}', |
||||
|
isTestnet: false |
||||
|
}; |
Loading…
Reference in new issue