diff --git a/src/BIP44Node.ts b/src/BIP44Node.ts index b7f93eb3..30c0590f 100644 --- a/src/BIP44Node.ts +++ b/src/BIP44Node.ts @@ -14,12 +14,7 @@ import { MIN_BIP_44_DEPTH, } from './constants'; import type { SupportedCurve } from './curves'; -import { - decodeExtendedKey, - encodeExtendedKey, - PRIVATE_KEY_VERSION, - PUBLIC_KEY_VERSION, -} from './extended-keys'; +import { decodeExtendedKey, PRIVATE_KEY_VERSION } from './extended-keys'; import { SLIP10Node, validateBIP32Depth } from './SLIP10Node'; import { isHardened } from './utils'; @@ -279,26 +274,7 @@ export class BIP44Node implements BIP44NodeInterface { } public get extendedKey(): string { - const data = { - depth: this.depth, - parentFingerprint: this.parentFingerprint, - index: this.index, - chainCode: this.chainCodeBytes, - }; - - if (this.privateKeyBytes) { - return encodeExtendedKey({ - ...data, - version: PRIVATE_KEY_VERSION, - privateKey: this.privateKeyBytes, - }); - } - - return encodeExtendedKey({ - ...data, - version: PUBLIC_KEY_VERSION, - publicKey: this.publicKeyBytes, - }); + return this.#node.extendedKey; } public get curve(): SupportedCurve { diff --git a/src/SLIP10Node.test.ts b/src/SLIP10Node.test.ts index d86d4041..a5384a22 100644 --- a/src/SLIP10Node.test.ts +++ b/src/SLIP10Node.test.ts @@ -5,6 +5,7 @@ import { BIP44PurposeNodeToken } from './constants'; import { ed25519, secp256k1 } from './curves'; import { compressPublicKey } from './curves/secp256k1'; import { createBip39KeyFromSeed, deriveChildKey } from './derivers/bip39'; +import { encodeExtendedKey, PRIVATE_KEY_VERSION } from './extended-keys'; import { SLIP10Node } from './SLIP10Node'; import { hexStringToBytes, mnemonicPhraseToBytes } from './utils'; @@ -38,353 +39,454 @@ describe('SLIP10Node', () => { }); describe('fromExtendedKey', () => { - it('initializes a new node from a private key', async () => { - const { privateKey, chainCode } = await deriveChildKey({ - path: fixtures.local.mnemonic, - curve: secp256k1, - }); + describe('using an object', () => { + it('initializes a new node from a private key', async () => { + const { privateKeyBytes, chainCodeBytes } = await deriveChildKey({ + path: fixtures.local.mnemonic, + curve: secp256k1, + }); - const node = await SLIP10Node.fromExtendedKey({ - privateKey, - chainCode, - depth: 0, - parentFingerprint: 0, - index: 0, - curve: 'secp256k1', + const node = await SLIP10Node.fromExtendedKey({ + privateKey: privateKeyBytes, + chainCode: chainCodeBytes, + depth: 0, + parentFingerprint: 0, + index: 0, + curve: 'secp256k1', + }); + + expect(node.depth).toBe(0); + expect(node.privateKeyBytes).toHaveLength(32); + expect(node.publicKeyBytes).toHaveLength(65); }); - expect(node.depth).toBe(0); - expect(node.privateKeyBytes).toHaveLength(32); - expect(node.publicKeyBytes).toHaveLength(65); - }); + it('initializes a new node from a hexadecimal private key and chain code', async () => { + const { privateKey, chainCode } = await deriveChildKey({ + path: fixtures.local.mnemonic, + curve: secp256k1, + }); - it('initializes a new node from a hexadecimal private key and chain code', async () => { - const { privateKey, chainCode } = await deriveChildKey({ - path: fixtures.local.mnemonic, - curve: secp256k1, - }); + const node = await SLIP10Node.fromExtendedKey({ + privateKey, + chainCode, + depth: 0, + parentFingerprint: 0, + index: 0, + curve: 'secp256k1', + }); - const node = await SLIP10Node.fromExtendedKey({ - privateKey, - chainCode, - depth: 0, - parentFingerprint: 0, - index: 0, - curve: 'secp256k1', + expect(node.depth).toBe(0); + expect(node.privateKeyBytes).toHaveLength(32); + expect(node.publicKeyBytes).toHaveLength(65); }); - expect(node.depth).toBe(0); - expect(node.privateKeyBytes).toHaveLength(32); - expect(node.publicKeyBytes).toHaveLength(65); - }); + it('initializes a new ed25519 node from a private key', async () => { + const { privateKey, chainCode } = await deriveChildKey({ + path: fixtures.local.mnemonic, + curve: ed25519, + }); - it('initializes a new ed25519 node from a private key', async () => { - const { privateKey, chainCode } = await deriveChildKey({ - path: fixtures.local.mnemonic, - curve: ed25519, - }); + const node = await SLIP10Node.fromExtendedKey({ + privateKey, + chainCode, + depth: 0, + parentFingerprint: 0, + index: 0, + curve: 'ed25519', + }); - const node = await SLIP10Node.fromExtendedKey({ - privateKey, - chainCode, - depth: 0, - parentFingerprint: 0, - index: 0, - curve: 'ed25519', + expect(node.depth).toBe(0); + expect(node.privateKeyBytes).toHaveLength(32); + expect(node.publicKeyBytes).toHaveLength(33); }); - expect(node.depth).toBe(0); - expect(node.privateKeyBytes).toHaveLength(32); - expect(node.publicKeyBytes).toHaveLength(33); - }); + it('initializes a new ed25519 node from a zero private key', async () => { + const node = await SLIP10Node.fromExtendedKey({ + privateKey: new Uint8Array(32).fill(0), + chainCode: new Uint8Array(32).fill(1), + depth: 0, + parentFingerprint: 0, + index: 0, + curve: 'ed25519', + }); - it('initializes a new ed25519 node from a zero private key', async () => { - const node = await SLIP10Node.fromExtendedKey({ - privateKey: new Uint8Array(32).fill(0), - chainCode: new Uint8Array(32).fill(1), - depth: 0, - parentFingerprint: 0, - index: 0, - curve: 'ed25519', + expect(node.depth).toBe(0); + expect(node.privateKeyBytes).toStrictEqual(new Uint8Array(32).fill(0)); + expect(node.publicKeyBytes).toHaveLength(33); }); - expect(node.depth).toBe(0); - expect(node.privateKeyBytes).toStrictEqual(new Uint8Array(32).fill(0)); - expect(node.publicKeyBytes).toHaveLength(33); - }); + it('initializes a new node from a public key', async () => { + const { publicKeyBytes, chainCodeBytes } = await deriveChildKey({ + path: fixtures.local.mnemonic, + curve: secp256k1, + }); - it('initializes a new node from a public key', async () => { - const { publicKeyBytes, chainCodeBytes } = await deriveChildKey({ - path: fixtures.local.mnemonic, - curve: secp256k1, - }); + const node = await SLIP10Node.fromExtendedKey({ + publicKey: publicKeyBytes, + chainCode: chainCodeBytes, + depth: 0, + parentFingerprint: 0, + index: 0, + curve: 'secp256k1', + }); - const node = await SLIP10Node.fromExtendedKey({ - publicKey: publicKeyBytes, - chainCode: chainCodeBytes, - depth: 0, - parentFingerprint: 0, - index: 0, - curve: 'secp256k1', + expect(node.depth).toBe(0); + expect(node.privateKeyBytes).toBeUndefined(); + expect(node.publicKeyBytes).toHaveLength(65); }); - expect(node.depth).toBe(0); - expect(node.privateKeyBytes).toBeUndefined(); - expect(node.publicKeyBytes).toHaveLength(65); - }); + it('initializes a new ed25519 node from a public key', async () => { + const { publicKeyBytes, chainCodeBytes } = await deriveChildKey({ + path: fixtures.local.mnemonic, + curve: ed25519, + }); - it('initializes a new ed25519 node from a public key', async () => { - const { publicKeyBytes, chainCodeBytes } = await deriveChildKey({ - path: fixtures.local.mnemonic, - curve: ed25519, - }); + const node = await SLIP10Node.fromExtendedKey({ + publicKey: publicKeyBytes, + chainCode: chainCodeBytes, + depth: 0, + parentFingerprint: 0, + index: 0, + curve: 'ed25519', + }); - const node = await SLIP10Node.fromExtendedKey({ - publicKey: publicKeyBytes, - chainCode: chainCodeBytes, - depth: 0, - parentFingerprint: 0, - index: 0, - curve: 'ed25519', + expect(node.depth).toBe(0); + expect(node.privateKeyBytes).toBeUndefined(); + expect(node.publicKeyBytes).toHaveLength(33); }); - expect(node.depth).toBe(0); - expect(node.privateKeyBytes).toBeUndefined(); - expect(node.publicKeyBytes).toHaveLength(33); - }); + it('initializes a new node from a hexadecimal public key and chain code', async () => { + const { publicKey, chainCode } = await deriveChildKey({ + path: fixtures.local.mnemonic, + curve: secp256k1, + }); - it('initializes a new node from a hexadecimal public key and chain code', async () => { - const { publicKey, chainCode } = await deriveChildKey({ - path: fixtures.local.mnemonic, - curve: secp256k1, - }); + const node = await SLIP10Node.fromExtendedKey({ + publicKey, + chainCode, + depth: 0, + parentFingerprint: 0, + index: 0, + curve: 'secp256k1', + }); - const node = await SLIP10Node.fromExtendedKey({ - publicKey, - chainCode, - depth: 0, - parentFingerprint: 0, - index: 0, - curve: 'secp256k1', + expect(node.depth).toBe(0); + expect(node.privateKeyBytes).toBeUndefined(); + expect(node.publicKeyBytes).toHaveLength(65); }); - expect(node.depth).toBe(0); - expect(node.privateKeyBytes).toBeUndefined(); - expect(node.publicKeyBytes).toHaveLength(65); - }); + it('initializes a new node from JSON', async () => { + const node = await deriveChildKey({ + path: fixtures.local.mnemonic, + curve: secp256k1, + }); - it('initializes a new node from JSON', async () => { - const node = await deriveChildKey({ - path: fixtures.local.mnemonic, - curve: secp256k1, + expect(await SLIP10Node.fromJSON(node.toJSON())).toStrictEqual(node); }); - expect(await SLIP10Node.fromJSON(node.toJSON())).toStrictEqual(node); - }); + it('initializes a new node from JSON with a public key', async () => { + const { privateKey, chainCode } = await deriveChildKey({ + path: fixtures.local.mnemonic, + curve: secp256k1, + }); - it('initializes a new node from JSON with a public key', async () => { - const { privateKey, chainCode } = await deriveChildKey({ - path: fixtures.local.mnemonic, - curve: secp256k1, + const node = await SLIP10Node.fromExtendedKey({ + privateKey, + chainCode, + depth: 0, + parentFingerprint: 0, + index: 0, + curve: 'secp256k1', + }); + + const neuteredNode = node.neuter(); + + expect(await SLIP10Node.fromJSON(neuteredNode.toJSON())).toStrictEqual( + neuteredNode, + ); }); - const node = await SLIP10Node.fromExtendedKey({ - privateKey, - chainCode, - depth: 0, - parentFingerprint: 0, - index: 0, - curve: 'secp256k1', + it('throws if no public or private key is specified', async () => { + await expect( + SLIP10Node.fromExtendedKey({ + chainCode: new Uint8Array(32).fill(1), + depth: 0, + parentFingerprint: 0, + index: 0, + curve: 'secp256k1', + }), + ).rejects.toThrow( + 'Invalid options: Must provide either a private key or a public key.', + ); }); - const neuteredNode = node.neuter(); + it('throws if the depth is invalid', async () => { + const inputs = [ + -1, + 0.1, + -0.1, + NaN, + Infinity, + '0', + 'zero', + {}, + null, + undefined, + ]; + + for (const input of inputs) { + await expect( + SLIP10Node.fromExtendedKey({ + depth: input as any, + parentFingerprint: 0, + index: 0, + publicKey: new Uint8Array(65).fill(1), + chainCode: new Uint8Array(32).fill(1), + curve: 'secp256k1', + }), + ).rejects.toThrow( + `Invalid HD tree path depth: The depth must be a positive integer. Received: "${String( + input, + )}"`, + ); + } + }); - expect(await SLIP10Node.fromJSON(neuteredNode.toJSON())).toStrictEqual( - neuteredNode, - ); - }); + it('throws if the parent fingerprint is invalid', async () => { + const inputs = [ + -1, + 0.1, + -0.1, + NaN, + Infinity, + '0', + 'zero', + {}, + null, + undefined, + ]; + + for (const input of inputs) { + await expect( + SLIP10Node.fromExtendedKey({ + depth: 0, + parentFingerprint: input as any, + index: 0, + publicKey: new Uint8Array(65).fill(1), + chainCode: new Uint8Array(32).fill(1), + curve: 'secp256k1', + }), + ).rejects.toThrow( + `Invalid parent fingerprint: The fingerprint must be a positive integer. Received: "${String( + input, + )}"`, + ); + } + }); - it('throws if no public or private key is specified', async () => { - await expect( - SLIP10Node.fromExtendedKey({ - chainCode: new Uint8Array(32).fill(1), - depth: 0, - parentFingerprint: 0, - index: 0, - curve: 'secp256k1', - }), - ).rejects.toThrow( - 'Invalid options: Must provide either a private key or a public key.', - ); - }); + it('throws if the private key is invalid', async () => { + await expect( + SLIP10Node.fromExtendedKey({ + privateKey: 'foo', + chainCode: new Uint8Array(32).fill(1), + depth: 0, + parentFingerprint: 0, + index: 0, + curve: 'secp256k1', + }), + ).rejects.toThrow('Value must be a hexadecimal string.'); + }); - it('throws if the depth is invalid', async () => { - const inputs = [ - -1, - 0.1, - -0.1, - NaN, - Infinity, - '0', - 'zero', - {}, - null, - undefined, - ]; - - for (const input of inputs) { + it('throws if the private key is not a Uint8Array or hexadecimal string', async () => { await expect( + // @ts-expect-error Invalid private key type. SLIP10Node.fromExtendedKey({ - depth: input as any, + privateKey: 123, + chainCode: new Uint8Array(32).fill(1), + depth: 0, parentFingerprint: 0, index: 0, - publicKey: new Uint8Array(65).fill(1), + curve: 'secp256k1', + }), + ).rejects.toThrow( + 'Invalid value: Expected an instance of Uint8Array or hexadecimal string.', + ); + }); + + it('throws if the private key is zero for secp256k1', async () => { + await expect( + SLIP10Node.fromExtendedKey({ + privateKey: new Uint8Array(32).fill(0), chainCode: new Uint8Array(32).fill(1), + depth: 0, + parentFingerprint: 0, + index: 0, curve: 'secp256k1', }), ).rejects.toThrow( - `Invalid HD tree path depth: The depth must be a positive integer. Received: "${String( - input, - )}"`, + 'Invalid private key: Value is not a valid secp256k1 private key.', ); - } - }); + }); - it('throws if the parent fingerprint is invalid', async () => { - const inputs = [ - -1, - 0.1, - -0.1, - NaN, - Infinity, - '0', - 'zero', - {}, - null, - undefined, - ]; - - for (const input of inputs) { + it('throws if the depth is zero and the parent fingerprint is not zero', async () => { await expect( SLIP10Node.fromExtendedKey({ + privateKey: new Uint8Array(32).fill(1), + chainCode: new Uint8Array(32).fill(1), depth: 0, - parentFingerprint: input as any, + parentFingerprint: 1, index: 0, - publicKey: new Uint8Array(65).fill(1), + curve: 'secp256k1', + }), + ).rejects.toThrow( + 'Invalid parent fingerprint: The fingerprint of the root node must be 0. Received: "1".', + ); + }); + + it('throws if the depth is not zero and the parent fingerprint is zero', async () => { + await expect( + SLIP10Node.fromExtendedKey({ + privateKey: new Uint8Array(32).fill(1), chainCode: new Uint8Array(32).fill(1), + depth: 1, + parentFingerprint: 0, + index: 0, curve: 'secp256k1', }), ).rejects.toThrow( - `Invalid parent fingerprint: The fingerprint must be a positive integer. Received: "${String( - input, - )}"`, + 'Invalid parent fingerprint: The fingerprint of a child node must not be 0. Received: "0".', ); - } - }); + }); - it('throws if the private key is invalid', async () => { - await expect( - SLIP10Node.fromExtendedKey({ - privateKey: 'foo', - chainCode: new Uint8Array(32).fill(1), - depth: 0, - parentFingerprint: 0, - index: 0, - curve: 'secp256k1', - }), - ).rejects.toThrow('Value must be a hexadecimal string.'); - }); + it('throws if the depth is >= 2 and the parent fingerprint is equal to the master fingerprint', async () => { + await expect( + SLIP10Node.fromExtendedKey({ + privateKey: new Uint8Array(32).fill(1), + chainCode: new Uint8Array(32).fill(1), + depth: 2, + parentFingerprint: 1, + masterFingerprint: 1, + index: 0, + curve: 'secp256k1', + }), + ).rejects.toThrow( + 'Invalid parent fingerprint: The fingerprint of a child node cannot be equal to the master fingerprint. Received: "1".', + ); + }); - it('throws if the private key is not a Uint8Array or hexadecimal string', async () => { - await expect( - SLIP10Node.fromExtendedKey({ - // @ts-expect-error Invalid private key type. - privateKey: 123, - chainCode: new Uint8Array(32).fill(1), - depth: 0, - parentFingerprint: 0, - index: 0, - curve: 'secp256k1', - }), - ).rejects.toThrow( - 'Invalid value: Expected an instance of Uint8Array or hexadecimal string.', - ); + it('throws if the depth is zero and the index is not zero', async () => { + await expect( + SLIP10Node.fromExtendedKey({ + privateKey: new Uint8Array(32).fill(1), + chainCode: new Uint8Array(32).fill(1), + depth: 0, + parentFingerprint: 0, + index: 1, + curve: 'secp256k1', + }), + ).rejects.toThrow( + 'Invalid index: The index of the root node must be 0. Received: "1".', + ); + }); }); - it('throws if the private key is zero for secp256k1', async () => { - await expect( - SLIP10Node.fromExtendedKey({ - privateKey: new Uint8Array(32).fill(0), - chainCode: new Uint8Array(32).fill(1), + describe('using a BIP-32 serialised extended key', () => { + it('initializes a new node from a private key', async () => { + const { extendedKey, privateKey, chainCode } = await deriveChildKey({ + path: fixtures.local.mnemonic, + curve: secp256k1, + }); + + const node = await SLIP10Node.fromExtendedKey(extendedKey); + + expect(node.depth).toBe(0); + expect(node.privateKeyBytes).toHaveLength(32); + expect(node.publicKeyBytes).toHaveLength(65); + expect(node.privateKey).toBe(privateKey); + expect(node.chainCode).toBe(chainCode); + }); + + it('initializes a new node from a public key', async () => { + const baseNode = await deriveChildKey({ + path: fixtures.local.mnemonic, + curve: secp256k1, + }); + + const { extendedKey, publicKey } = baseNode.neuter(); + + const node = await SLIP10Node.fromExtendedKey(extendedKey); + + expect(node.depth).toBe(0); + expect(node.privateKeyBytes).toBeUndefined(); + expect(node.publicKeyBytes).toHaveLength(65); + expect(node.publicKey).toBe(publicKey); + }); + + it('throws if the extended key is invalid', async () => { + await expect(SLIP10Node.fromExtendedKey('foo')).rejects.toThrow( + 'Invalid extended key: Value is not base58-encoded, or the checksum is invalid.', + ); + }); + + it('throws if the private key is zero', async () => { + const extendedKey = encodeExtendedKey({ + version: PRIVATE_KEY_VERSION, depth: 0, parentFingerprint: 0, index: 0, - curve: 'secp256k1', - }), - ).rejects.toThrow( - 'Invalid private key: Value is not a valid secp256k1 private key.', - ); - }); - - it('throws if the depth is zero and the parent fingerprint is not zero', async () => { - await expect( - SLIP10Node.fromExtendedKey({ - privateKey: new Uint8Array(32).fill(1), chainCode: new Uint8Array(32).fill(1), + privateKey: new Uint8Array(32).fill(0), + }); + + await expect(SLIP10Node.fromExtendedKey(extendedKey)).rejects.toThrow( + 'Invalid extended key: Key must be a 33-byte non-zero byte array.', + ); + }); + + it('throws if the depth is zero and the parent fingerprint is not zero', async () => { + const extendedKey = encodeExtendedKey({ + version: PRIVATE_KEY_VERSION, depth: 0, parentFingerprint: 1, index: 0, - curve: 'secp256k1', - }), - ).rejects.toThrow( - 'Invalid parent fingerprint: The fingerprint of the root node must be 0. Received: "1".', - ); - }); - - it('throws if the depth is not zero and the parent fingerprint is zero', async () => { - await expect( - SLIP10Node.fromExtendedKey({ - privateKey: new Uint8Array(32).fill(1), chainCode: new Uint8Array(32).fill(1), + privateKey: new Uint8Array(32).fill(1), + }); + + await expect(SLIP10Node.fromExtendedKey(extendedKey)).rejects.toThrow( + 'Invalid parent fingerprint: The fingerprint of the root node must be 0. Received: "1".', + ); + }); + + it('throws if the depth is not zero and the parent fingerprint is zero', async () => { + const extendedKey = encodeExtendedKey({ + version: PRIVATE_KEY_VERSION, depth: 1, parentFingerprint: 0, index: 0, - curve: 'secp256k1', - }), - ).rejects.toThrow( - 'Invalid parent fingerprint: The fingerprint of a child node must not be 0. Received: "0".', - ); - }); - - it('throws if the depth is >= 2 and the parent fingerprint is equal to the master fingerprint', async () => { - await expect( - SLIP10Node.fromExtendedKey({ - privateKey: new Uint8Array(32).fill(1), chainCode: new Uint8Array(32).fill(1), - depth: 2, - parentFingerprint: 1, - masterFingerprint: 1, - index: 0, - curve: 'secp256k1', - }), - ).rejects.toThrow( - 'Invalid parent fingerprint: The fingerprint of a child node cannot be equal to the master fingerprint. Received: "1".', - ); - }); - - it('throws if the depth is zero and the index is not zero', async () => { - await expect( - SLIP10Node.fromExtendedKey({ privateKey: new Uint8Array(32).fill(1), - chainCode: new Uint8Array(32).fill(1), + }); + + await expect(SLIP10Node.fromExtendedKey(extendedKey)).rejects.toThrow( + 'Invalid parent fingerprint: The fingerprint of a child node must not be 0. Received: "0".', + ); + }); + + it('throws if the depth is zero and the index is not zero', async () => { + const extendedKey = encodeExtendedKey({ + version: PRIVATE_KEY_VERSION, depth: 0, parentFingerprint: 0, index: 1, - curve: 'secp256k1', - }), - ).rejects.toThrow( - 'Invalid index: The index of the root node must be 0. Received: "1".', - ); + chainCode: new Uint8Array(32).fill(1), + privateKey: new Uint8Array(32).fill(1), + }); + + await expect(SLIP10Node.fromExtendedKey(extendedKey)).rejects.toThrow( + 'Invalid index: The index of the root node must be 0. Received: "1".', + ); + }); }); }); @@ -793,6 +895,76 @@ describe('SLIP10Node', () => { }); }); + describe('extendedKey', () => { + it.each(fixtures.bip32)( + 'returns the extended private key for an secp256k1 node', + async ({ hexSeed, keys }) => { + const { privateKey, chainCode } = await createBip39KeyFromSeed( + hexStringToBytes(hexSeed), + secp256k1, + ); + + for (const { path, extPrivKey } of keys) { + const node = await SLIP10Node.fromExtendedKey({ + privateKey, + chainCode, + curve: 'secp256k1', + depth: 0, + parentFingerprint: 0, + index: 0, + }); + + if (path.ours.tuple.length === 0) { + continue; + } + + const childNode = await node.derive(path.ours.tuple); + expect(childNode.extendedKey).toBe(extPrivKey); + } + }, + ); + + it.each(fixtures.bip32)( + 'returns the extended public key for an secp256k1 node', + async ({ hexSeed, keys }) => { + const { privateKey, chainCode } = await createBip39KeyFromSeed( + hexStringToBytes(hexSeed), + secp256k1, + ); + + for (const { path, extPubKey } of keys) { + const node = await SLIP10Node.fromExtendedKey({ + privateKey, + chainCode, + curve: 'secp256k1', + depth: 0, + parentFingerprint: 0, + index: 0, + }); + + if (path.ours.tuple.length === 0) { + continue; + } + + const childNode = await node.derive(path.ours.tuple); + const neuteredNode = childNode.neuter(); + expect(neuteredNode.extendedKey).toBe(extPubKey); + } + }, + ); + + it('throws when trying to get an extended key for an ed25519 node', async () => { + const node = await SLIP10Node.fromDerivationPath({ + derivationPath: [defaultBip39NodeToken, `slip10:44'`, `slip10:60'`], + curve: 'ed25519', + }); + + expect(() => node.extendedKey).toThrow( + 'Unable to get extended key for this node: Only secp256k1 is supported.', + ); + }); + }); + describe('neuter', () => { it('returns a SLIP-10 node without a private key', async () => { const node = await SLIP10Node.fromDerivationPath({ diff --git a/src/SLIP10Node.ts b/src/SLIP10Node.ts index af1b2009..dddb0eb9 100644 --- a/src/SLIP10Node.ts +++ b/src/SLIP10Node.ts @@ -8,6 +8,12 @@ import type { SupportedCurve } from './curves'; import { getCurveByName } from './curves'; import { deriveKeyFromPath } from './derivation'; import { publicKeyToEthAddress } from './derivers/bip32'; +import { + decodeExtendedKey, + encodeExtendedKey, + PRIVATE_KEY_VERSION, + PUBLIC_KEY_VERSION, +} from './extended-keys'; import { getBytes, getBytesUnsafe, @@ -123,6 +129,18 @@ export class SLIP10Node implements SLIP10NodeInterface { return SLIP10Node.fromExtendedKey(json); } + /** + * Create a new SLIP-10 node from a BIP-32 serialised extended key string. + * The key may be either public or private. Note that `secp256k1` is assumed + * as the curve for the key. + * + * All parameters are stringently validated, and an error is thrown if + * validation fails. + * + * @param extendedKey - The BIP-32 extended key string. + */ + static async fromExtendedKey(extendedKey: string): Promise; + /** * Create a new SLIP-10 node from a key and chain code. You must specify * either a private key or a public key. When specifying a private key, @@ -145,16 +163,83 @@ export class SLIP10Node implements SLIP10NodeInterface { * @param options.chainCode - The chain code for the node. * @param options.curve - The curve used by the node. */ - static async fromExtendedKey({ - depth, - masterFingerprint, - parentFingerprint, - index, - privateKey, - publicKey, - chainCode, - curve, - }: SLIP10ExtendedKeyOptions) { + static async fromExtendedKey( + // These signatures could technically be combined, but it's easier to + // document them separately. + // eslint-disable-next-line @typescript-eslint/unified-signatures + options: SLIP10ExtendedKeyOptions, + ): Promise; + + /** + * Create a new SLIP-10 node from a key and chain code. You must specify + * either a private key or a public key. When specifying a private key, + * the public key will be derived from the private key. + * + * All parameters are stringently validated, and an error is thrown if + * validation fails. + * + * @param options - The options for the new node. This can be an object + * containing the extended key options, or a string containing the extended + * key. + * @param options.depth - The depth of the node. + * @param options.masterFingerprint - The fingerprint of the master node, i.e., the + * node at depth 0. May be undefined if this node was created from an extended + * key. + * @param options.parentFingerprint - The fingerprint of the parent key, or 0 if + * the node is a master node. + * @param options.index - The index of the node, or 0 if the node is a master node. + * @param options.privateKey - The private key for the node. + * @param options.publicKey - The public key for the node. If a private key is + * specified, this parameter is ignored. + * @param options.chainCode - The chain code for the node. + * @param options.curve - The curve used by the node. + */ + static async fromExtendedKey( + options: SLIP10ExtendedKeyOptions | string, + ): Promise { + if (typeof options === 'string') { + const extendedKey = decodeExtendedKey(options); + + const { chainCode, depth, parentFingerprint, index } = extendedKey; + + if (extendedKey.version === PRIVATE_KEY_VERSION) { + const { privateKey } = extendedKey; + + return SLIP10Node.fromExtendedKey({ + depth, + parentFingerprint, + index, + privateKey, + chainCode, + // BIP-32 key serialisation assumes `secp256k1`. + curve: 'secp256k1', + }); + } + + const { publicKey } = extendedKey; + + return SLIP10Node.fromExtendedKey({ + depth, + parentFingerprint, + index, + publicKey, + chainCode, + // BIP-32 key serialisation assumes `secp256k1`. + curve: 'secp256k1', + }); + } + + const { + depth, + masterFingerprint, + parentFingerprint, + index, + privateKey, + publicKey, + chainCode, + curve, + } = options; + const chainCodeBytes = getBytes(chainCode, BYTES_KEY_LENGTH); validateCurve(curve); @@ -353,6 +438,43 @@ export class SLIP10Node implements SLIP10NodeInterface { ); } + /** + * Get the extended public or private key for the SLIP-10 node. SLIP-10 + * doesn't specify a format for extended keys, so we use the BIP-32 format. + * + * This property is only supported for `secp256k1` nodes, as other curves + * don't specify a standard format for extended keys. + * + * @returns The extended public or private key for the node. + */ + public get extendedKey(): string { + assert( + this.curve === 'secp256k1', + 'Unable to get extended key for this node: Only secp256k1 is supported.', + ); + + const data = { + depth: this.depth, + parentFingerprint: this.parentFingerprint, + index: this.index, + chainCode: this.chainCodeBytes, + }; + + if (this.privateKeyBytes) { + return encodeExtendedKey({ + ...data, + version: PRIVATE_KEY_VERSION, + privateKey: this.privateKeyBytes, + }); + } + + return encodeExtendedKey({ + ...data, + version: PUBLIC_KEY_VERSION, + publicKey: this.publicKeyBytes, + }); + } + /** * Get a neutered version of this node, i.e. a node without a private key. * diff --git a/src/extended-keys.ts b/src/extended-keys.ts index d4b32a2b..f9c8f5b4 100644 --- a/src/extended-keys.ts +++ b/src/extended-keys.ts @@ -37,8 +37,8 @@ type ExtendedPrivateKey = ExtendedKeyLike & { export type ExtendedKey = ExtendedPublicKey | ExtendedPrivateKey; /** - * Decodes an extended public or private key. In the case of an extended public key, the public key - * is returned in the uncompressed form. + * Decode an extended public or private key. In the case of an extended public + * key, the public key is returned in the uncompressed form. * * Throws an error if the extended key is invalid. *