UNPKG

@metamask/eth-trezor-keyring

Version:

A MetaMask compatible keyring, for trezor hardware wallets

313 lines 13 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 _TrezorKeyring_instances, _TrezorKeyring_parseDerivationPath, _TrezorKeyring_createKeyringAccount; import { EthAccountType, EthMethod, EthScope, KeyringAccountEntropyTypeOption } from "@metamask/keyring-api"; import { KeyringType } from "@metamask/keyring-api/v2"; import { EthKeyringWrapper } from "@metamask/keyring-sdk/v2"; /** * Methods supported by Trezor keyring EOA accounts. * Trezor keyrings support a subset of signing methods (no encryption, app keys, or EIP-7702). */ const TREZOR_KEYRING_METHODS = [ EthMethod.SignTransaction, EthMethod.PersonalSign, EthMethod.SignTypedDataV3, EthMethod.SignTypedDataV4, ]; const trezorKeyringCapabilities = { scopes: [EthScope.Eoa], bip44: { deriveIndex: true, derivePath: true, }, }; /** * BIP-44 standard HD path prefix constant for Ethereum. * Used as default for derive-index operations. */ export const BIP44_HD_PATH_PREFIX = `m/44'/60'/0'/0`; /** * SLIP-0044 testnet HD path prefix constant. */ export const SLIP0044_TESTNET_PATH_PREFIX = `m/44'/1'/0'/0`; /** * Legacy MEW (MyEtherWallet) HD path prefix constant. */ export const LEGACY_MEW_PATH_PREFIX = `m/44'/60'/0'`; /** * Allowed HD paths for Trezor keyring. * These must match the keys in ALLOWED_HD_PATHS from trezor-keyring.ts. */ const ALLOWED_HD_PATHS = [ BIP44_HD_PATH_PREFIX, SLIP0044_TESTNET_PATH_PREFIX, LEGACY_MEW_PATH_PREFIX, ]; /** * Regex pattern for validating and parsing Trezor derivation paths. * Matches BIP-44 style paths: m/44'/{coin}'/{segments}/{index} * where coin is 60' (Ethereum) or 1' (testnet). * Captures: [1] = base path prefix, [2] = index * The prefix is then validated against ALLOWED_HD_PATHS. */ const DERIVATION_PATH_PATTERN = /^(m\/44'\/(?:60'|1')(?:\/\d+'?)*)\/(\d+)$/u; export class TrezorKeyring extends EthKeyringWrapper { constructor(options) { var _a; super({ type: (_a = options.type) !== null && _a !== void 0 ? _a : KeyringType.Trezor, inner: options.legacyKeyring, capabilities: trezorKeyringCapabilities, }); _TrezorKeyring_instances.add(this); this.entropySource = options.entropySource; } /** * Hydrate the underlying keyring from a previously serialized state. * * Overrides the base class implementation to avoid calling `getAccounts()` * when the Trezor device is locked. The base class calls `getAccounts()` to * rebuild the registry, but for Trezor keyrings this requires the HDKey to * be initialized (via `unlock()`). Since the device may not be connected * during deserialization, we skip the registry rebuild here. The registry * will be populated on the first call to `getAccounts()` after the device * is unlocked. * * @param state - The serialized keyring state. */ async deserialize(state) { await this.withLock(async () => { // Clear the registry when deserializing this.registry.clear(); // Deserialize the legacy keyring state only. // We intentionally skip calling getAccounts() here because the Trezor // device may be locked (HDKey not initialized). The TrezorKeyring's // deserialize restores the accounts array, but not the paths map, so // getIndexForAddress would need to derive addresses which requires an // initialized HDKey. The registry will be populated lazily when // getAccounts() is called after the device is unlocked. await this.inner.deserialize(state); }); } async getAccounts() { const addresses = await this.inner.getAccounts(); if (addresses.length === 0) { return []; } // If the device is locked, we cannot derive addresses to find indices. // Return cached accounts if available, otherwise throw a clear error. if (!this.inner.isUnlocked()) { const cachedAccounts = addresses .map((address) => { const existingId = this.registry.getAccountId(address); return existingId ? this.registry.get(existingId) : undefined; }) .filter((account) => account !== undefined); // If we have all accounts cached, return them if (cachedAccounts.length === addresses.length) { return cachedAccounts; } // Some accounts are not cached and device is locked throw new Error('Trezor device is locked. Please unlock the device to access accounts.'); } 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 = this.inner.getIndexForAddress(address); return __classPrivateFieldGet(this, _TrezorKeyring_instances, "m", _TrezorKeyring_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 TrezorKeyring: ${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, _TrezorKeyring_instances, "m", _TrezorKeyring_parseDerivationPath).call(this, options.derivationPath); targetIndex = parsed.index; basePath = parsed.basePath; // Use the normalized path to avoid mismatches with leading zeros // (e.g., "m/44'/60'/0'/0/007" becomes "m/44'/60'/0'/0/7") derivationPath = `${basePath}/${targetIndex}`; } else { // derive-index uses BIP-44 standard path by default if (options.groupIndex < 0) { throw new Error(`Invalid groupIndex: ${options.groupIndex}. Must be a non-negative integer.`); } 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. // If the HD path is changing, clear the registry to avoid stale accounts. // The TrezorKeyring operates on a single path at a time - accounts from // different paths cannot coexist in the inner keyring. if (basePath !== this.inner.hdPath) { this.registry.clear(); } 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, _TrezorKeyring_instances, "m", _TrezorKeyring_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 device model reported by the bridge, or `undefined` if no * device is paired. */ getModel() { return this.inner.getModel(); } /** * @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; } /** * Set the derivation path on the inner keyring. Must be one of the allowed * HD paths supported by the legacy Trezor 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(); } } _TrezorKeyring_instances = new WeakSet(), _TrezorKeyring_parseDerivationPath = function _TrezorKeyring_parseDerivationPath(derivationPath) { const match = derivationPath.match(DERIVATION_PATH_PATTERN); if (!(match === null || match === void 0 ? void 0 : match[1]) || !match[2]) { throw new Error(`Invalid derivation path: ${derivationPath}. ` + `Expected format: {base}/{index} where base is one of: ` + `${ALLOWED_HD_PATHS.join(', ')}.`); } const basePath = match[1]; const index = parseInt(match[2], 10); // Validate the base path is one of the allowed paths if (!ALLOWED_HD_PATHS.includes(basePath)) { throw new Error(`Invalid derivation path: ${derivationPath}. ` + `Expected format: {base}/{index} where base is one of: ` + `${ALLOWED_HD_PATHS.join(', ')}.`); } return { basePath: basePath, index, }; }, _TrezorKeyring_createKeyringAccount = function _TrezorKeyring_createKeyringAccount(address, addressIndex) { const id = this.registry.register(address); const derivationPath = `${this.inner.hdPath}/${addressIndex}`; const account = { id, type: EthAccountType.Eoa, address, scopes: [...this.capabilities.scopes], methods: [...TREZOR_KEYRING_METHODS], options: { entropy: { type: KeyringAccountEntropyTypeOption.Mnemonic, id: this.entropySource, groupIndex: addressIndex, derivationPath, }, }, }; this.registry.set(account); return account; }; //# sourceMappingURL=trezor-keyring.mjs.map