@metamask/eth-ledger-bridge-keyring
Version:
A MetaMask compatible keyring, for ledger hardware wallets
316 lines • 12.8 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 _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