diff --git a/src/hdnode.js b/src/hdnode.js index 62cac56..784acf5 100644 --- a/src/hdnode.js +++ b/src/hdnode.js @@ -291,6 +291,30 @@ HDNode.prototype.isNeutered = function () { return !(this.keyPair.d) } +HDNode.prototype.derivePath = function (path) { + typeforce(types.Bip32Path, path) + + var splitPath = path.split('/') + if (splitPath[0] === 'm') { + if (this.parentFingerprint) { + throw new Error('Not a master node') + } + + splitPath = splitPath.slice(1) + } + + return splitPath.reduce(function (prevHd, indexStr) { + var index + if (indexStr.slice(-1) === "'") { + index = parseInt(indexStr.slice(0, -1), 10) + return prevHd.deriveHardened(index) + } else { + index = parseInt(indexStr, 10) + return prevHd.derive(index) + } + }, this) +} + HDNode.prototype.toString = HDNode.prototype.toBase58 module.exports = HDNode diff --git a/src/types.js b/src/types.js index 7ddf8c8..69ab787 100644 --- a/src/types.js +++ b/src/types.js @@ -26,6 +26,11 @@ function UInt53 (value) { Math.floor(value) === value } +function Bip32Path (value) { + return typeforce.String(value) && + value.match(/^(m\/)?(\d+'?\/)*\d+'?$/) +} + // external dependent types var BigInt = typeforce.quacksLike('BigInteger') var ECPoint = typeforce.quacksLike('Point') @@ -57,7 +62,8 @@ var types = { UInt8: UInt8, UInt31: UInt31, UInt32: UInt32, - UInt53: UInt53 + UInt53: UInt53, + Bip32Path: Bip32Path } for (var typeName in typeforce) { diff --git a/test/fixtures/hdnode.json b/test/fixtures/hdnode.json index 93530d2..21ca41d 100644 --- a/test/fixtures/hdnode.json +++ b/test/fixtures/hdnode.json @@ -109,7 +109,7 @@ "address": "19EuDJdgfRkwCmRzbzVBHZWQG9QNWhftbZ" }, { - "description": "m/0/2147483647", + "description": "m/0/2147483647'", "m": 2147483647, "hardened": true, "wif": "L1m5VpbXmMp57P3knskwhoMTLdhAAaXiHvnGLMribbfwzVRpz2Sr", @@ -134,7 +134,7 @@ "address": "1BxrAr2pHpeBheusmd6fHDP2tSLAUa3qsW" }, { - "description": "m/0/2147483647'/1/2147483646", + "description": "m/0/2147483647'/1/2147483646'", "m": 2147483646, "hardened": true, "wif": "L5KhaMvPYRW1ZoFmRjUtxxPypQ94m6BcDrPhqArhggdaTbbAFJEF", @@ -232,6 +232,27 @@ "exception": "Point is not on the curve", "hex": "0488b21e000000000000000000873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508020045400697100007000037899988826500030092003000016366806305909050" } + ], + "deriveHardened": [ + 2147483648, + null, + "foo", + -1 + ], + "derive": [ + 4294967296, + null, + "foo", + -1 + ], + "derivePath": [ + 2, + [2, 3, 4], + "/", + "m/m/123", + "a/0/1/2", + "m/0/ 1 /2", + "m/0/1.5/2" ] } } diff --git a/test/hdnode.js b/test/hdnode.js index 4e9f1cf..c5a55fb 100644 --- a/test/hdnode.js +++ b/test/hdnode.js @@ -272,6 +272,7 @@ describe('HDNode', function () { fixtures.valid.forEach(function (f) { var network = NETWORKS[f.network] var hd = HDNode.fromSeedHex(f.master.seed, network) + var master = hd // FIXME: test data is only testing Private -> private for now f.children.forEach(function (c, i) { @@ -285,6 +286,42 @@ describe('HDNode', function () { verifyVector(hd, c, i + 1) }) }) + + // testing deriving path from master + f.children.forEach(function (c) { + it(c.description + ' from ' + f.master.fingerprint + ' by path', function () { + var path = c.description + var child = master.derivePath(path) + + var pathSplit = path.split('/').slice(1) + var pathNotM = pathSplit.join('/') + var childNotM = master.derivePath(pathNotM) + + verifyVector(child, c, pathSplit.length) + verifyVector(childNotM, c, pathSplit.length) + }) + }) + + // testing deriving path from children + f.children.forEach(function (c, i) { + var cn = master.derivePath(c.description) + + f.children.slice(i + 1).forEach(function (cc) { + it(cc.description + ' from ' + c.fingerprint + ' by path', function () { + var path = cc.description + + var pathSplit = path.split('/').slice(i + 2) + var pathEnd = pathSplit.join('/') + var pathEndM = 'm/' + pathEnd + var child = cn.derivePath(pathEnd) + verifyVector(child, cc, pathSplit.length + i + 1) + + assert.throws(function () { + cn.derivePath(pathEndM) + }, /Not a master node/) + }) + }) + }) }) it('works for Private -> public (neutered)', function () { @@ -328,46 +365,27 @@ describe('HDNode', function () { }, /Could not derive hardened child key/) }) - it('throws on negative indexes', function () { - var f = fixtures.valid[0] - var master = HDNode.fromBase58(f.master.base58, NETWORKS_LIST) - - assert.throws(function () { - master.deriveHardened(-1) - }, /Expected UInt31/) - assert.throws(function () { - master.derive(-1) - }, /Expected UInt32/) - }) - - it('throws on high indexes', function () { + it('throws on wrong types', function () { var f = fixtures.valid[0] var master = HDNode.fromBase58(f.master.base58, NETWORKS_LIST) - assert.throws(function () { - master.deriveHardened(0x80000000) - }, /Expected UInt31/) - assert.throws(function () { - master.derive(0x100000000) - }, /Expected UInt32/) - }) + fixtures.invalid.derive.forEach(function (fx) { + assert.throws(function () { + master.derive(fx) + }, /Expected UInt32/) + }) - it('throws on non-numbers', function () { - var f = fixtures.valid[0] - var master = HDNode.fromBase58(f.master.base58, NETWORKS_LIST) + fixtures.invalid.deriveHardened.forEach(function (fx) { + assert.throws(function () { + master.deriveHardened(fx) + }, /Expected UInt31/) + }) - assert.throws(function () { - master.deriveHardened() - }, /Expected UInt31/) - assert.throws(function () { - master.derive() - }, /Expected UInt32/) - assert.throws(function () { - master.deriveHardened('foo') - }, /Expected UInt31/) - assert.throws(function () { - master.derive('foo') - }, /Expected UInt32/) + fixtures.invalid.derivePath.forEach(function (fx) { + assert.throws(function () { + master.derivePath(fx) + }, /Expected Bip32Path/) + }) }) }) })