UNPKG

@metamask/eth-ledger-bridge-keyring

Version:
316 lines 12.8 kB
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var _LedgerKeyring_instances, _LedgerKeyring_getChecksumHexAddress, _LedgerKeyring_parseDerivationPath, _LedgerKeyring_getIndexForAddress, _LedgerKeyring_createKeyringAccount; import { EthAccountType, EthMethod, EthScope, KeyringAccountEntropyTypeOption } from "@metamask/keyring-api"; import { KeyringType } from "@metamask/keyring-api/v2"; import { EthKeyringMethod, EthKeyringWrapper } from "@metamask/keyring-sdk/v2"; import { add0x, getChecksumAddress } from "@metamask/utils"; /** * Methods supported by Ledger keyring EOA accounts. * Ledger keyrings support a subset of signing methods (no encryption or app keys). */ const LEDGER_KEYRING_METHODS = [ EthMethod.SignTransaction, EthMethod.PersonalSign, EthMethod.SignTypedDataV4, EthKeyringMethod.SignEip7702Authorization, ]; const ledgerKeyringCapabilities = { scopes: [EthScope.Eoa], bip44: { deriveIndex: true, derivePath: true, }, }; /** * Ledger Live HD path constant. */ const LEDGER_LIVE_HD_PATH = `m/44'/60'/0'/0/0`; /** * BIP-44 standard HD path prefix constant for Ethereum. */ const BIP44_HD_PATH_PREFIX = `m/44'/60'/0'/0`; /** * Regex pattern for validating and parsing Ledger Live derivation paths. * Format: m/44'/60'/{index}'/0/0 */ const LEDGER_LIVE_PATH_PATTERN = /^m\/44'\/60'\/(\d+)'\/0\/0$/u; /** * Regex pattern for validating and parsing non-Ledger-Live derivation paths. * Supports Legacy (m/44'/60'/0'/{index}), BIP44 (m/44'/60'/0'/0/{index}), * and custom paths that follow the m/44'/60'/... pattern. */ const INDEX_AT_END_PATH_PATTERN = /^(m\/44'\/60'(?:\/\d+'?)*)\/(\d+)$/u; export class LedgerKeyring extends EthKeyringWrapper { constructor(options) { super({ type: KeyringType.Ledger, inner: options.legacyKeyring, capabilities: ledgerKeyringCapabilities, }); _LedgerKeyring_instances.add(this); this.entropySource = options.entropySource; } async getAccounts() { const addresses = await this.inner.getAccounts(); if (addresses.length === 0) { return []; } return addresses.map((address) => { // Check if we already have this account in the registry const existingId = this.registry.getAccountId(address); if (existingId) { const cached = this.registry.get(existingId); if (cached) { return cached; } } const addressIndex = __classPrivateFieldGet(this, _LedgerKeyring_instances, "m", _LedgerKeyring_getIndexForAddress).call(this, address); return __classPrivateFieldGet(this, _LedgerKeyring_instances, "m", _LedgerKeyring_createKeyringAccount).call(this, address, addressIndex); }); } async createAccounts(options) { return this.withLock(async () => { if (options.type === 'bip44:derive-path' || options.type === 'bip44:derive-index') { // Validate that the entropy source matches this keyring's entropy source if (options.entropySource !== this.entropySource) { throw new Error(`Entropy source mismatch: expected '${this.entropySource}', got '${options.entropySource}'`); } } else { throw new Error(`Unsupported account creation type for LedgerKeyring: ${String(options.type)}`); } // Check if an account at this index already exists with the same derivation path const currentAccounts = await this.getAccounts(); let targetIndex; let basePath; let derivationPath; if (options.type === 'bip44:derive-path') { // Parse the derivation path to extract base path and index const parsed = __classPrivateFieldGet(this, _LedgerKeyring_instances, "m", _LedgerKeyring_parseDerivationPath).call(this, options.derivationPath); targetIndex = parsed.index; basePath = parsed.basePath; derivationPath = options.derivationPath; } else { // derive-index uses BIP-44 standard path by default targetIndex = options.groupIndex; basePath = BIP44_HD_PATH_PREFIX; derivationPath = `${basePath}/${targetIndex}`; } const existingAccount = currentAccounts.find((account) => { return (account.options.entropy.groupIndex === targetIndex && account.options.entropy.derivationPath === derivationPath); }); if (existingAccount) { return [existingAccount]; } // Derive the account at the specified index this.inner.setHdPath(basePath); this.inner.setAccountToUnlock(targetIndex); const [newAddress] = await this.inner.addAccounts(1); if (!newAddress) { throw new Error('Failed to create new account'); } const newAccount = __classPrivateFieldGet(this, _LedgerKeyring_instances, "m", _LedgerKeyring_createKeyringAccount).call(this, newAddress, targetIndex); return [newAccount]; }); } /** * Delete an account from the keyring. * * @param accountId - The account ID to delete. */ async deleteAccount(accountId) { await this.withLock(async () => { const { address } = await this.getAccount(accountId); const hexAddress = this.toHexAddress(address); // Remove from the legacy keyring this.inner.removeAccount(hexAddress); // Remove from the registry this.registry.delete(accountId); }); } /** * @returns The current derivation path used by the inner keyring. */ get hdPath() { return this.inner.hdPath; } /** * @returns The bridge instance used by the inner keyring to communicate * with the device. */ get bridge() { return this.inner.bridge; } /** * @returns The device ID for the paired Ledger device. */ getDeviceId() { return this.inner.getDeviceId(); } /** * Set the device ID for the paired Ledger device. * * @param deviceId - The device ID to set. */ setDeviceId(deviceId) { this.inner.setDeviceId(deviceId); } /** * Set the derivation path on the inner keyring. * * @param hdPath - The derivation path to set. */ setHdPath(hdPath) { this.inner.setHdPath(hdPath); } /** * Fetch the first page of candidate addresses from the device. * * @returns The first page of accounts. */ async getFirstPage() { return this.inner.getFirstPage(); } /** * Fetch the next page of candidate addresses from the device. * * @returns The next page of accounts. */ async getNextPage() { return this.inner.getNextPage(); } /** * Fetch the previous page of candidate addresses from the device. * * @returns The previous page of accounts. */ async getPreviousPage() { return this.inner.getPreviousPage(); } /** * Clear the inner keyring's device-pairing state and accounts, and reset * the V2 account registry to keep them in sync. */ async forgetDevice() { await this.withLock(async () => { this.inner.forgetDevice(); this.registry.clear(); }); } /** * @returns Whether the inner keyring has an unlocked HD key. */ isUnlocked() { return this.inner.isUnlocked(); } /** * Attempt to open the Ethereum app on the connected Ledger device. * * @returns Whether the app was opened. */ async attemptMakeApp() { return this.inner.attemptMakeApp(); } /** * @returns The app name and version currently running on the connected * Ledger device. */ async getAppNameAndVersion() { return this.inner.getAppNameAndVersion(); } } _LedgerKeyring_instances = new WeakSet(), _LedgerKeyring_getChecksumHexAddress = function _LedgerKeyring_getChecksumHexAddress(address) { return getChecksumAddress(add0x(address)); }, _LedgerKeyring_parseDerivationPath = function _LedgerKeyring_parseDerivationPath(derivationPath) { // Try Ledger Live format first: m/44'/60'/{index}'/0/0 const ledgerLiveMatch = derivationPath.match(LEDGER_LIVE_PATH_PATTERN); if (ledgerLiveMatch?.[1]) { return { // This constant is used by `inner.setHdPath` to determine which derivation // mode we should use (Ledger Live derivation mode here). basePath: LEDGER_LIVE_HD_PATH, index: parseInt(ledgerLiveMatch[1], 10), }; } // Try index-at-end format: m/44'/60'/.../{index} const indexAtEndMatch = derivationPath.match(INDEX_AT_END_PATH_PATTERN); if (indexAtEndMatch) { // If the condition is true, indexAtEndMatch[1] and indexAtEndMatch[2] are defined, so // we can safely cast them to string. // This is necessary to get 100% code coverage. return { // Here, we use a derivation path prefix for `inner.setHdPath` // (prefix + index derivation mode). basePath: indexAtEndMatch[1], index: parseInt(indexAtEndMatch[2], 10), }; } throw new Error(`Invalid derivation path format: ${derivationPath}. ` + `Expected Ledger Live (m/44'/60'/{index}'/0/0) or index-at-end (m/44'/60'/.../{index}) format.`); }, _LedgerKeyring_getIndexForAddress = function _LedgerKeyring_getIndexForAddress(address) { const checksummedAddress = __classPrivateFieldGet(this, _LedgerKeyring_instances, "m", _LedgerKeyring_getChecksumHexAddress).call(this, address); const details = this.inner.accountDetails[checksummedAddress]; if (!details) { throw new Error(`Address ${checksummedAddress} not found in account details`); } // Extract index from hdPath const { hdPath } = details; if (!hdPath) { throw new Error(`No HD path found for address ${checksummedAddress}`); } // Ledger supports multiple derivation path formats: // - Ledger Live (bip44: true): m/44'/60'/{index}'/0/0 - index at position 3 // - Other paths (bip44: false): {hdPath}/{index} - index at end // - BIP44: m/44'/60'/0'/0/{index} // - Legacy: m/44'/60'/0'/{index} // - Custom paths via setHdPath // // We use the `bip44` flag to determine which extraction pattern to use. if (details.bip44) { // Ledger Live format: m/44'/60'/{index}'/0/0 const match = hdPath.match(LEDGER_LIVE_PATH_PATTERN); if (match?.[1]) { return parseInt(match[1], 10); } } else { // Index-at-end format: m/44'/60'/.../{index} const match = hdPath.match(INDEX_AT_END_PATH_PATTERN); if (match?.[2]) { return parseInt(match[2], 10); } } throw new Error(`Could not extract index from HD path: ${hdPath}`); }, _LedgerKeyring_createKeyringAccount = function _LedgerKeyring_createKeyringAccount(address, addressIndex) { const id = this.registry.register(address); const checksummedAddress = __classPrivateFieldGet(this, _LedgerKeyring_instances, "m", _LedgerKeyring_getChecksumHexAddress).call(this, address); const details = this.inner.accountDetails[checksummedAddress]; if (!details?.hdPath) { throw new Error(`No HD path found for address ${checksummedAddress}. Cannot create account.`); } const account = { id, type: EthAccountType.Eoa, address, scopes: [...this.capabilities.scopes], methods: [...LEDGER_KEYRING_METHODS], options: { entropy: { type: KeyringAccountEntropyTypeOption.Mnemonic, id: this.entropySource, groupIndex: addressIndex, derivationPath: details.hdPath, }, }, }; this.registry.set(account); return account; }; //# sourceMappingURL=ledger-keyring.mjs.map