@metamask/eth-trezor-keyring
Version:
A MetaMask compatible keyring, for trezor hardware wallets
313 lines • 13 kB
JavaScript
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