Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add keyIdentifier.derive() #99

Merged
merged 2 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion libraries/key-identifier/__tests__/key-identifier.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ describe('KeyIdentifier', () => {
assetName: 0,
derivationPath: "m/44'/60'/0'/0/0",
},

// Non-existing assetNames
// {
// derivationAlgorithm: 'BIP32',
Expand Down Expand Up @@ -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 derived = keyId.derive([1, 5])

expect(derived).toEqual({
derivationAlgorithm: 'BIP32',
assetName: 'ethereum',
keyType: 'secp256k1',
})

expect(derived.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({
Expand Down
2 changes: 1 addition & 1 deletion libraries/key-identifier/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
32 changes: 25 additions & 7 deletions libraries/key-identifier/src/key-identifier.d.ts
Original file line number Diff line number Diff line change
@@ -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<ConstructorParams>

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
*/
derive(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
}
45 changes: 33 additions & 12 deletions libraries/key-identifier/src/key-identifier.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
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'
Comment on lines +7 to +10
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to avoid instanceof - maybe worth porting to key-utils


export default class KeyIdentifier {
/** @type {DerivationPath} */
#derivationPath

constructor({ derivationAlgorithm, derivationPath, assetName, keyType }) {
assert(typeof derivationAlgorithm === 'string', 'derivationAlgorithm not a string')
assert(
SUPPORTED_KDFS.has(derivationAlgorithm),
`${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')
Expand All @@ -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()
}

derive(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
Expand Down
16 changes: 14 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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"
Expand Down
Loading