diff --git a/README.md b/README.md index 654e97a..d772a8e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Features not supported: - signing transactions - managing storage (neither in node.js or the browser) -## API +## Wallet API Constructors: @@ -54,6 +54,27 @@ Instance methods: All of the above instance methods return a Buffer or JSON. Use the `String` suffixed versions for a string output, such as `getPrivateKeyString()`. +## HD Wallet API + +To use BIP32 HD wallets, first include the `hdkey` submodule: + +`var hdkey = require('ethereumjs-wallet/hdkey')` + +Constructors: + +* `fromMasterSeed(seed)` - create an instance based on a seed +* `formExtendedKey(key)` - create an instance based on a BIP32 extended private or public key + +For the seed we suggest to use [bip39](https://npmjs.org/packages/bip39) to create one from a BIP39 mnemonic. + +Instance methods: + +* `privateExtendedKey()` - return a BIP32 extended private key +* `publicExtendedKey()` - return a BIP32 extended private key +* `derivePath(path)` - derive a node based on a path (e.g. m/44'/0'/0/1) +* `deriveChild(index)` - derive a node based on a child index +* `getWallet()` - return a `Wallet` instance as seen above + ### Remarks about `toV3` The `options` is an optional object hash, where all the serialization parameters can be fine tuned: @@ -82,3 +103,9 @@ The following settings are favoured by the Go Ethereum implementation and we def - `r`: `8` - `p`: `1` - `cipher`: `aes-128-ctr` + +## License + +MIT License + +Copyright (C) 2016 Alex Beregszaszi diff --git a/hdkey.js b/hdkey.js new file mode 100644 index 0000000..5089d0d --- /dev/null +++ b/hdkey.js @@ -0,0 +1,48 @@ +const HDKey = require('hdkey') +const Wallet = require('./index.js') + +function EthereumHDKey () { +} + +/* + * Horrible wrapping. + */ +function fromHDKey (hdkey) { + var ret = new EthereumHDKey() + ret._hdkey = hdkey + return ret +} + +EthereumHDKey.fromMasterSeed = function (seedBuffer) { + return fromHDKey(HDKey.fromMasterSeed(seedBuffer)) +} + +EthereumHDKey.fromExtendedKey = function (base58key) { + return fromHDKey(HDKey.fromExtendedKey(base58key)) +} + +EthereumHDKey.prototype.privateExtendedKey = function () { + return this._hdkey.privateExtendedKey +} + +EthereumHDKey.prototype.publicExtendedKey = function () { + return this._hdkey.publicExtendedKey +} + +EthereumHDKey.prototype.derivePath = function (path) { + return fromHDKey(this._hdkey.derive(path)) +} + +EthereumHDKey.prototype.deriveChild = function (index) { + return fromHDKey(this._hdkey.deriveChild(index)) +} + +EthereumHDKey.prototype.getWallet = function () { + if (this._hdkey._privateKey) { + return Wallet.fromPrivateKey(this._hdkey._privateKey) + } else { + return Wallet.fromPublicKey(this._hdkey._publicKey) + } +} + +module.exports = EthereumHDKey diff --git a/package.json b/package.json index ab59fd0..59356cf 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "lint": "standard", "prepublish": "npm run lint && npm run test", - "test": "mocha ./test/index.js" + "test": "mocha ./test/index.js && mocha ./test/hdkey.js" }, "repository": { "type": "git", @@ -27,6 +27,7 @@ "aes-js": "^0.2.3", "bs58check": "^1.0.8", "ethereumjs-util": "^4.3.0", + "hdkey": "git+https://github.com/axic/hdkey#feature/secp256k1", "scrypt.js": "^0.1.0", "secp256k1": "^3.0.1", "uuid": "^2.0.1", diff --git a/test/hdkey.js b/test/hdkey.js new file mode 100644 index 0000000..950c273 --- /dev/null +++ b/test/hdkey.js @@ -0,0 +1,74 @@ +var assert = require('assert') +var HDKey = require('../hdkey.js') + +// from BIP39 mnemonic: awake book subject inch gentle blur grant damage process float month clown +var fixtureseed = new Buffer('747f302d9c916698912d5f70be53a6cf53bc495803a5523d3a7c3afa2afba94ec3803f838b3e1929ab5481f9da35441372283690fdcf27372c38f40ba134fe03', 'hex') +var fixturehd = HDKey.fromMasterSeed(fixtureseed) + +describe('.fromMasterSeed()', function () { + it('should work', function () { + assert.doesNotThrow(function () { + HDKey.fromMasterSeed(fixtureseed) + }) + }) +}) + +describe('.privateExtendedKey()', function () { + it('should work', function () { + assert.equal(fixturehd.privateExtendedKey(), 'xprv9s21ZrQH143K4KqQx9Zrf1eN8EaPQVFxM2Ast8mdHn7GKiDWzNEyNdduJhWXToy8MpkGcKjxeFWd8oBSvsz4PCYamxR7TX49pSpp3bmHVAY') + }) +}) + +describe('.publicExtendedKey()', function () { + it('should work', function () { + assert.equal(fixturehd.publicExtendedKey(), 'xpub661MyMwAqRbcGout4B6s29b6gGQsowyoiF6UgXBEr7eFCWYfXuZDvRxP9zEh1Kwq3TLqDQMbkbaRpSnoC28oWvjLeshoQz1StZ9YHM1EpcJ') + }) +}) + +describe('.fromExtendedKey()', function () { + it('should work with public', function () { + var hdnode = HDKey.fromExtendedKey('xpub661MyMwAqRbcGout4B6s29b6gGQsowyoiF6UgXBEr7eFCWYfXuZDvRxP9zEh1Kwq3TLqDQMbkbaRpSnoC28oWvjLeshoQz1StZ9YHM1EpcJ') + assert.equal(hdnode.publicExtendedKey(), 'xpub661MyMwAqRbcGout4B6s29b6gGQsowyoiF6UgXBEr7eFCWYfXuZDvRxP9zEh1Kwq3TLqDQMbkbaRpSnoC28oWvjLeshoQz1StZ9YHM1EpcJ') + assert.throws(function () { + hdnode.privateExtendedKey() + }) + }) + it('should work with private', function () { + var hdnode = HDKey.fromExtendedKey('xprv9s21ZrQH143K4KqQx9Zrf1eN8EaPQVFxM2Ast8mdHn7GKiDWzNEyNdduJhWXToy8MpkGcKjxeFWd8oBSvsz4PCYamxR7TX49pSpp3bmHVAY') + assert.equal(hdnode.publicExtendedKey(), 'xpub661MyMwAqRbcGout4B6s29b6gGQsowyoiF6UgXBEr7eFCWYfXuZDvRxP9zEh1Kwq3TLqDQMbkbaRpSnoC28oWvjLeshoQz1StZ9YHM1EpcJ') + assert.equal(hdnode.privateExtendedKey(), 'xprv9s21ZrQH143K4KqQx9Zrf1eN8EaPQVFxM2Ast8mdHn7GKiDWzNEyNdduJhWXToy8MpkGcKjxeFWd8oBSvsz4PCYamxR7TX49pSpp3bmHVAY') + }) +}) + +describe('.deriveChild()', function () { + it('should work', function () { + var hdnode = fixturehd.deriveChild(1) + assert.equal(hdnode.privateExtendedKey(), 'xprv9vYSvrg3eR5FaKbQE4Ao2vHdyvfFL27aWMyH6X818mKWMsqqQZAN6HmRqYDGDPLArzaqbLExRsxFwtx2B2X2QKkC9uoKsiBNi22tLPKZHNS') + }) +}) + +describe('.derivePath()', function () { + it('should work with m', function () { + var hdnode = fixturehd.derivePath('m') + assert.equal(hdnode.privateExtendedKey(), 'xprv9s21ZrQH143K4KqQx9Zrf1eN8EaPQVFxM2Ast8mdHn7GKiDWzNEyNdduJhWXToy8MpkGcKjxeFWd8oBSvsz4PCYamxR7TX49pSpp3bmHVAY') + }) + it('should work with m/44\'/0\'/0/1', function () { + var hdnode = fixturehd.derivePath('m/44\'/0\'/0/1') + assert.equal(hdnode.privateExtendedKey(), 'xprvA1ErCzsuXhpB8iDTsbmgpkA2P8ggu97hMZbAXTZCdGYeaUrDhyR8fEw47BNEgLExsWCVzFYuGyeDZJLiFJ9kwBzGojQ6NB718tjVJrVBSrG') + }) +}) + +describe('.getWallet()', function () { + it('should work', function () { + assert.equal(fixturehd.getWallet().getPrivateKeyString(), '0x26cc9417b89cd77c4acdbe2e3cd286070a015d8e380f9cd1244ae103b7d89d81') + assert.equal(fixturehd.getWallet().getPublicKeyString(), + '0x0639797f6cc72aea0f3d309730844a9e67d9f1866e55845c5f7e0ab48402973defa5cb69df462bcc6d73c31e1c663c225650e80ef14a507b203f2a12aea55bc1') + }) + it('should work with public nodes', function () { + var hdnode = HDKey.fromExtendedKey('xpub661MyMwAqRbcGout4B6s29b6gGQsowyoiF6UgXBEr7eFCWYfXuZDvRxP9zEh1Kwq3TLqDQMbkbaRpSnoC28oWvjLeshoQz1StZ9YHM1EpcJ') + assert.throws(function () { + hdnode.getWallet().getPrivateKeyString() + }) + assert.equal(hdnode.getWallet().getPublicKeyString(), '0x030639797f6cc72aea0f3d309730844a9e67d9f1866e55845c5f7e0ab48402973d') + }) +}) diff --git a/test/index.js b/test/index.js index b52b93d..19e3583 100644 --- a/test/index.js +++ b/test/index.js @@ -87,7 +87,6 @@ describe('.fromExtendedPublicKey()', function () { }) }) - describe('.generate()', function () { it('should generate an account', function () { assert.equal(Wallet.generate().getPrivateKey().length, 32)