From e0280b05648b0b77bb13ba7524f6481124f2e02a Mon Sep 17 00:00:00 2001 From: Jan W Date: Tue, 7 May 2024 09:57:18 +0200 Subject: [PATCH 1/2] feat: add method to extend KeyIdentifier's derivation path --- .../__tests__/key-identifier.test.js | 48 ++++++++++++++++++- libraries/key-identifier/package.json | 2 +- .../key-identifier/src/key-identifier.d.ts | 32 ++++++++++--- .../key-identifier/src/key-identifier.js | 45 ++++++++++++----- yarn.lock | 16 ++++++- 5 files changed, 120 insertions(+), 23 deletions(-) diff --git a/libraries/key-identifier/__tests__/key-identifier.test.js b/libraries/key-identifier/__tests__/key-identifier.test.js index 14cd228e..6704a64a 100644 --- a/libraries/key-identifier/__tests__/key-identifier.test.js +++ b/libraries/key-identifier/__tests__/key-identifier.test.js @@ -22,7 +22,6 @@ describe('KeyIdentifier', () => { assetName: 0, derivationPath: "m/44'/60'/0'/0/0", }, - // Non-existing assetNames // { // derivationAlgorithm: 'BIP32', @@ -70,6 +69,53 @@ describe('KeyIdentifier', () => { invalid.forEach((item) => expect(KeyIdentifier.validate(item)).toEqual(false)) }) + it('supports passing the derivation path as array of path indices', () => { + const keyId = new KeyIdentifier({ + derivationAlgorithm: 'BIP32', + assetName: 'ethereum', + derivationPath: ['m', "44'", "60'", "0'", '0', '0'], + }) + + expect(keyId.derivationPath).toBe("m/44'/60'/0'/0/0") + }) + + describe('.extend()', () => { + test('extends derivation path', () => { + const keyId = new KeyIdentifier({ + derivationAlgorithm: 'BIP32', + assetName: 'ethereum', + derivationPath: "m/44'/60'/0'", + }) + + const extended = keyId.extend([1, 5]) + + expect(extended).toEqual({ + derivationAlgorithm: 'BIP32', + assetName: 'ethereum', + keyType: 'secp256k1', + }) + + expect(extended.derivationPath).toBe("m/44'/60'/0'/1/5") + }) + }) + + describe('.toJSON()', () => { + test('includes derivation path', () => { + const keyId = new KeyIdentifier({ + derivationAlgorithm: 'BIP32', + assetName: 'ethereum', + derivationPath: "m/44'/60'/0'", + }) + + expect(keyId.toJSON()).toEqual({ + derivationAlgorithm: 'BIP32', + assetName: 'ethereum', + keyType: 'secp256k1', + derivationPath: "m/44'/60'/0'", + }) + }) + }) + describe('.compare()', () => { it('should return true when equal', () => { const keyIdA = new KeyIdentifier({ diff --git a/libraries/key-identifier/package.json b/libraries/key-identifier/package.json index c681cb3e..a73a60e7 100644 --- a/libraries/key-identifier/package.json +++ b/libraries/key-identifier/package.json @@ -25,7 +25,7 @@ "todo:reenable:test:integration": "jest --testMatch='**/*.integration-test.js'" }, "dependencies": { - "@exodus/key-utils": "^3.1.0", + "@exodus/key-utils": "^3.3.0", "minimalistic-assert": "^1.0.1" }, "devDependencies": { diff --git a/libraries/key-identifier/src/key-identifier.d.ts b/libraries/key-identifier/src/key-identifier.d.ts index 7df6f4c3..085ec880 100644 --- a/libraries/key-identifier/src/key-identifier.d.ts +++ b/libraries/key-identifier/src/key-identifier.d.ts @@ -1,20 +1,38 @@ +type PathIndex = number | string +type KeyType = 'legacy' | 'nacl' | 'secp2561' +type DerivationAlgorithm = 'BIP32' | 'SLIP10' + type ConstructorParams = { - derivationAlgorithm: 'BIP32' | 'SLIP10' - derivationPath: string + derivationAlgorithm: DerivationAlgorithm + derivationPath: string | PathIndex[] assetName?: string - keyType: 'legacy' | 'nacl' | 'secp2561' + keyType: KeyType } type KeyIdentifierLike = Partial export default class KeyIdentifier { - derivationAlgorithm: ConstructorParams['derivationAlgorithm'] - derivationPath: ConstructorParams['derivationPath'] - assetName: ConstructorParams['assetName'] - keyType: ConstructorParams['keyType'] + derivationAlgorithm: DerivationAlgorithm + keyType: KeyType + assetName?: string constructor(params: ConstructorParams) + get derivationPath(): string + + /** + * Returns a new KeyIdentifier instance that has an updated derivation path extended with + * the path indices or partial derivation path supplied to this method + */ + extend(pathLike: string | PathIndex[]): KeyIdentifier + + toJSON(): { + assetName?: string + derivationAlgorithm: DerivationAlgorithm + keyType: KeyType + derivationPath: string + } + static validate(potentialKeyIdentifier: KeyIdentifierLike): boolean static compare(a: KeyIdentifierLike, b: KeyIdentifierLike): boolean } diff --git a/libraries/key-identifier/src/key-identifier.js b/libraries/key-identifier/src/key-identifier.js index ad6d5dcc..c13ae5f1 100644 --- a/libraries/key-identifier/src/key-identifier.js +++ b/libraries/key-identifier/src/key-identifier.js @@ -1,17 +1,18 @@ import assert from 'minimalistic-assert' -import { assertValidDerivationPath } from '@exodus/key-utils' - -// key identifier example: -// { -// derivationAlgorithm: 'BIP32', -// derivationPath: `m/44'/60'/0'/0/0` -// // needed because some assets like `ethereum` and `matic` share the same constant but may have other differences -// assetName: 'solana' -// } +import { DerivationPath } from '@exodus/key-utils' + const SUPPORTED_KDFS = new Set(['BIP32', 'SLIP10']) const SUPPORTED_KEY_TYPES = new Set(['legacy', 'nacl', 'secp256k1']) +const isDerivationPath = (derivationPath) => + typeof derivationPath === 'object' && + Symbol.toStringTag in derivationPath && + derivationPath[Symbol.toStringTag]() === 'DerivationPath' + export default class KeyIdentifier { + /** @type {DerivationPath} */ + #derivationPath + constructor({ derivationAlgorithm, derivationPath, assetName, keyType }) { assert(typeof derivationAlgorithm === 'string', 'derivationAlgorithm not a string') assert( @@ -19,8 +20,6 @@ export default class KeyIdentifier { `${derivationAlgorithm} is not a valid derivationAlgorithm` ) - assertValidDerivationPath(derivationPath) - assert(['string', 'undefined'].includes(typeof assetName), 'assetName was not a string') keyType = keyType || (derivationAlgorithm === 'SLIP10' ? 'nacl' : 'secp256k1') @@ -34,19 +33,41 @@ export default class KeyIdentifier { } this.derivationAlgorithm = derivationAlgorithm - this.derivationPath = derivationPath this.assetName = assetName this.keyType = keyType + this.#derivationPath = isDerivationPath(derivationPath) + ? derivationPath + : DerivationPath.from(derivationPath) // Freeze the object on construction, disallow tampering with derivation path. // Ensures immutability of key identifiers passed to keychain. Object.freeze(this) } + get derivationPath() { + return this.#derivationPath.toString() + } + + extend(pathLike) { + return new KeyIdentifier({ + ...this, + derivationPath: this.#derivationPath.extend(pathLike), + }) + } + toString() { return `${this.derivationPath} (${this.derivationAlgorithm})` } + toJSON() { + return { + derivationAlgorithm: this.derivationAlgorithm, + assetName: this.assetName, + keyType: this.keyType, + derivationPath: this.derivationPath, + } + } + static validate = (potentialKeyIdentifier) => { try { // eslint-disable-next-line no-new diff --git a/yarn.lock b/yarn.lock index faabf434..0acfbec9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1975,7 +1975,7 @@ __metadata: resolution: "@exodus/key-identifier@workspace:libraries/key-identifier" dependencies: "@exodus/key-ids": ^1.2.0 - "@exodus/key-utils": ^3.1.0 + "@exodus/key-utils": ^3.3.0 "@exodus/keychain": "workspace:^" minimalistic-assert: ^1.0.1 languageName: unknown @@ -2001,7 +2001,7 @@ __metadata: languageName: node linkType: hard -"@exodus/key-utils@npm:^3.0.0, @exodus/key-utils@npm:^3.1.0": +"@exodus/key-utils@npm:^3.0.0": version: 3.1.0 resolution: "@exodus/key-utils@npm:3.1.0" dependencies: @@ -2012,6 +2012,18 @@ __metadata: languageName: node linkType: hard +"@exodus/key-utils@npm:^3.3.0": + version: 3.3.0 + resolution: "@exodus/key-utils@npm:3.3.0" + dependencies: + "@exodus/bip32": ^2.1.0 + "@exodus/hdkey": ^2.1.0-exodus.0 + bip32-path: ^0.4.2 + minimalistic-assert: ^1.0.1 + checksum: 741e47ac4e2fe285cd984c41dd27508cf2ece46ad53c7afd2f8a6358b9952ef1cedb05839b4f1a3949da19850f051409829f30e45d3fa137856852239018d953 + languageName: node + linkType: hard + "@exodus/keychain@npm:^5.0.1": version: 5.0.1 resolution: "@exodus/keychain@npm:5.0.1" From 43639dc0e621f933c1666aea93c2000de191b62e Mon Sep 17 00:00:00 2001 From: Jan W Date: Tue, 7 May 2024 10:00:03 +0200 Subject: [PATCH 2/2] refactor: rename extend => derive --- libraries/key-identifier/__tests__/key-identifier.test.js | 6 +++--- libraries/key-identifier/src/key-identifier.d.ts | 2 +- libraries/key-identifier/src/key-identifier.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/key-identifier/__tests__/key-identifier.test.js b/libraries/key-identifier/__tests__/key-identifier.test.js index 6704a64a..f6dce24a 100644 --- a/libraries/key-identifier/__tests__/key-identifier.test.js +++ b/libraries/key-identifier/__tests__/key-identifier.test.js @@ -87,15 +87,15 @@ describe('KeyIdentifier', () => { derivationPath: "m/44'/60'/0'", }) - const extended = keyId.extend([1, 5]) + const derived = keyId.derive([1, 5]) - expect(extended).toEqual({ + expect(derived).toEqual({ derivationAlgorithm: 'BIP32', assetName: 'ethereum', keyType: 'secp256k1', }) - expect(extended.derivationPath).toBe("m/44'/60'/0'/1/5") + expect(derived.derivationPath).toBe("m/44'/60'/0'/1/5") }) }) diff --git a/libraries/key-identifier/src/key-identifier.d.ts b/libraries/key-identifier/src/key-identifier.d.ts index 085ec880..001b20f0 100644 --- a/libraries/key-identifier/src/key-identifier.d.ts +++ b/libraries/key-identifier/src/key-identifier.d.ts @@ -24,7 +24,7 @@ export default class KeyIdentifier { * Returns a new KeyIdentifier instance that has an updated derivation path extended with * the path indices or partial derivation path supplied to this method */ - extend(pathLike: string | PathIndex[]): KeyIdentifier + derive(pathLike: string | PathIndex[]): KeyIdentifier toJSON(): { assetName?: string diff --git a/libraries/key-identifier/src/key-identifier.js b/libraries/key-identifier/src/key-identifier.js index c13ae5f1..e0c46296 100644 --- a/libraries/key-identifier/src/key-identifier.js +++ b/libraries/key-identifier/src/key-identifier.js @@ -48,7 +48,7 @@ export default class KeyIdentifier { return this.#derivationPath.toString() } - extend(pathLike) { + derive(pathLike) { return new KeyIdentifier({ ...this, derivationPath: this.#derivationPath.extend(pathLike),