Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions .syncpackrc
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
],
"dependencies": [
"@metamask/account-**",
"@metamask/hw-wallet-sdk",
"@metamask/keyring-**",
"@metamask/eth-**-keyring"
],
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ linkStyle default opacity:0.5
eth_hd_keyring --> keyring_api;
eth_hd_keyring --> keyring_utils;
eth_hd_keyring --> account_api;
eth_ledger_bridge_keyring --> hw_wallet_sdk;
eth_ledger_bridge_keyring --> keyring_api;
eth_ledger_bridge_keyring --> keyring_utils;
eth_ledger_bridge_keyring --> account_api;
Expand Down
5 changes: 5 additions & 0 deletions packages/keyring-eth-ledger-bridge/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add `LedgerKeyringV2` class implementing `KeyringV2` interface ([#416](https://github.com/MetaMask/accounts/pull/416))
- Wraps legacy `LedgerKeyring` to expose accounts via the unified `KeyringV2` API and the `KeyringAccount` type.
- Extends `EthKeyringWrapper` for common Ethereum logic.
- Integrate `@metamask/hw-wallet-sdk` for standardized hardware wallet error handling ([#446](https://github.com/MetaMask/accounts/pull/446))
- Replace custom error handling with `HardwareWalletError` from the SDK.
- Use `LEDGER_ERROR_MAPPINGS` from the SDK for consistent error code mapping.
- Re-export `HardwareWalletError`, `ErrorCode`, `Severity`, `Category`, and error mappings for consumer convenience.
- Deprecate `LedgerStatusError` in favor of `HardwareWalletError`.
Comment thread
montelaidev marked this conversation as resolved.
Outdated

## [11.1.2]

Expand Down
13 changes: 8 additions & 5 deletions packages/keyring-eth-ledger-bridge/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,21 @@ module.exports = merge(baseConfig, {
displayName,

// An array of regexp pattern strings used to skip coverage collection
coveragePathIgnorePatterns: ['./src/tests'],
coveragePathIgnorePatterns: [
'./src/tests',
'./src/type.ts', // Deprecated, kept for backwards compatibility
],

// The glob patterns Jest uses to detect test files
testMatch: ['**/*.test.[jt]s?(x)'],

// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 93.58,
functions: 98.16,
lines: 97.75,
statements: 97.76,
branches: 93.75,
functions: 98.26,
lines: 97.84,
statements: 97.85,
},
},
});
1 change: 1 addition & 0 deletions packages/keyring-eth-ledger-bridge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@ethereumjs/util": "^9.1.0",
"@ledgerhq/hw-app-eth": "^6.42.0",
"@metamask/eth-sig-util": "^8.2.0",
"@metamask/hw-wallet-sdk": "workspace:^",
"@metamask/keyring-api": "workspace:^",
"@metamask/keyring-utils": "workspace:^",
"hdkey": "^2.1.0"
Expand Down
97 changes: 97 additions & 0 deletions packages/keyring-eth-ledger-bridge/src/errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {
HardwareWalletError,
ErrorCode as ErrorCodeEnum,
} from '@metamask/hw-wallet-sdk';

import {
createLedgerError,
isKnownLedgerError,
getLedgerErrorMapping,
} from './errors';

describe('createLedgerError', () => {
describe('known error codes', () => {
it('creates HardwareWalletError for user rejection (0x6985)', () => {
const error = createLedgerError('0x6985');

expect(error).toBeInstanceOf(HardwareWalletError);
expect(error.code).toBe(ErrorCodeEnum.UserRejected);
expect(error.message).toBe('User rejected action on device');
});

it('creates HardwareWalletError for device locked (0x5515)', () => {
const error = createLedgerError('0x5515');

expect(error).toBeInstanceOf(HardwareWalletError);
expect(error.code).toBe(ErrorCodeEnum.AuthenticationDeviceLocked);
expect(error.message).toBe('Device is locked');
});

it('includes context in message when provided', () => {
const error = createLedgerError('0x6985', 'during sign transaction');

expect(error.message).toBe(
'User rejected action on device (during sign transaction)',
);
});

it('uses error message as userMessage when userMessage is not provided in mapping', () => {
// 0x9000 is success code which doesn't have a userMessage in the mapping
const error = createLedgerError('0x9000');

expect(error).toBeInstanceOf(HardwareWalletError);
// userMessage should fallback to the message
expect(error.userMessage).toBeDefined();
});
});

describe('unknown error codes', () => {
it('creates fallback error for unknown code without context', () => {
const error = createLedgerError('0x9999');

expect(error).toBeInstanceOf(HardwareWalletError);
expect(error.code).toBe(ErrorCodeEnum.Unknown);
expect(error.message).toBe('Unknown Ledger error: 0x9999');
});

it('creates fallback error for unknown code with context', () => {
const error = createLedgerError('0x9999', 'during operation');

expect(error).toBeInstanceOf(HardwareWalletError);
expect(error.code).toBe(ErrorCodeEnum.Unknown);
expect(error.message).toBe(
'Unknown Ledger error: 0x9999 (during operation)',
);
});
});
});

describe('isKnownLedgerError', () => {
it('returns true for known error codes', () => {
expect(isKnownLedgerError('0x6985')).toBe(true);
expect(isKnownLedgerError('0x5515')).toBe(true);
expect(isKnownLedgerError('0x6a80')).toBe(true);
});

it('returns false for unknown error codes', () => {
expect(isKnownLedgerError('0x9999')).toBe(false);
expect(isKnownLedgerError('invalid')).toBe(false);
expect(isKnownLedgerError('')).toBe(false);
});
});

describe('getLedgerErrorMapping', () => {
it('returns mapping for known error codes', () => {
const mapping = getLedgerErrorMapping('0x6985');

expect(mapping).toBeDefined();
expect(mapping?.code).toBe(ErrorCodeEnum.UserRejected);
expect(mapping?.message).toBe('User rejected action on device');
});

it('returns undefined for unknown error codes', () => {
const mapping = getLedgerErrorMapping('0x9999');

expect(mapping).toBeUndefined();
});
});
85 changes: 85 additions & 0 deletions packages/keyring-eth-ledger-bridge/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
type ErrorCode,
type Severity,
type Category,
HardwareWalletError,
LEDGER_ERROR_MAPPINGS,
ErrorCode as ErrorCodeEnum,
Severity as SeverityEnum,
Category as CategoryEnum,
} from '@metamask/hw-wallet-sdk';

type LedgerErrorMapping = {
code: ErrorCode;
message: string;
severity: Severity;
category: Category;
userMessage?: string;
};
Comment thread
montelaidev marked this conversation as resolved.
Outdated

/**
* Factory function to create a HardwareWalletError from a Ledger error code.
*
* @param ledgerErrorCode - The Ledger error code (e.g., '0x6985', '0x5515')
* @param context - Optional additional context to append to the error message
* @returns A HardwareWalletError instance with mapped error details
*/
export function createLedgerError(
ledgerErrorCode: string,
context?: string,
): HardwareWalletError {
const mappings = LEDGER_ERROR_MAPPINGS as {
[key: string]: LedgerErrorMapping;
};
const errorMapping = mappings[ledgerErrorCode];
Comment thread
montelaidev marked this conversation as resolved.
Outdated

if (errorMapping) {
const message = context
? `${errorMapping.message} (${context})`
: errorMapping.message;

return new HardwareWalletError(message, {
code: errorMapping.code,
severity: errorMapping.severity,
category: errorMapping.category,
userMessage: errorMapping.userMessage ?? message,
});
}

// Fallback for unknown error codes
const fallbackMessage = context
? `Unknown Ledger error: ${ledgerErrorCode} (${context})`
: `Unknown Ledger error: ${ledgerErrorCode}`;

return new HardwareWalletError(fallbackMessage, {
code: ErrorCodeEnum.Unknown,
severity: SeverityEnum.Err,
category: CategoryEnum.Unknown,
userMessage: fallbackMessage,
});
}

/**
* Checks if a Ledger error code exists in the error mappings.
*
* @param ledgerErrorCode - The Ledger error code to check
* @returns True if the error code is mapped, false otherwise
*/
export function isKnownLedgerError(ledgerErrorCode: string): boolean {
return ledgerErrorCode in LEDGER_ERROR_MAPPINGS;
}

/**
* Gets the error mapping details for a Ledger error code without creating an error instance.
*
* @param ledgerErrorCode - The Ledger error code to look up
* @returns The error mapping details or undefined if not found
*/
export function getLedgerErrorMapping(
ledgerErrorCode: string,
): LedgerErrorMapping | undefined {
const mappings = LEDGER_ERROR_MAPPINGS as {
[key: string]: LedgerErrorMapping;
};
return mappings[ledgerErrorCode];
}
14 changes: 14 additions & 0 deletions packages/keyring-eth-ledger-bridge/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,17 @@ export type * from './ledger-bridge';
export * from './ledger-transport-middleware';
export type * from './type';
export * from './ledger-hw-app';
export * from './errors';
export * from './ledger-error-handler';

// Re-export HardwareWalletError and related types from hw-wallet-sdk for convenience
export {
HardwareWalletError,
ErrorCode,
Severity,
Category,
LEDGER_ERROR_MAPPINGS,
BLE_ERROR_MAPPINGS,
MOBILE_ERROR_MAPPINGS,
} from '@metamask/hw-wallet-sdk';
export type { HardwareWalletErrorOptions } from '@metamask/hw-wallet-sdk';
Comment thread
montelaidev marked this conversation as resolved.
Outdated
24 changes: 24 additions & 0 deletions packages/keyring-eth-ledger-bridge/src/ledger-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ export type LedgerSignTypedDataResponse = Awaited<
ReturnType<LedgerHwAppEth['signEIP712HashedMessage']>
>;

export type GetAppNameAndVersionResponse = {
appName: string;
version: string;
};

export type AppConfigurationResponse = {
arbitraryDataEnabled: number; // this is the blind signing support
erc20ProvisioningNecessary: number;
starkEnabled: number;
starkv2Supported: number;
version: string;
};

export type LedgerBridgeOptions = Record<string, unknown>;

export type LedgerBridge<T extends LedgerBridgeOptions> = {
Expand Down Expand Up @@ -52,6 +65,10 @@ export type LedgerBridge<T extends LedgerBridgeOptions> = {

getPublicKey(params: GetPublicKeyParams): Promise<GetPublicKeyResponse>;

getAppNameAndVersion(): Promise<GetAppNameAndVersionResponse>;
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

getAppConfiguration(): Promise<AppConfigurationResponse>;

deviceSignTransaction(
params: LedgerSignTransactionParams,
): Promise<LedgerSignTransactionResponse>;
Expand All @@ -63,4 +80,11 @@ export type LedgerBridge<T extends LedgerBridgeOptions> = {
deviceSignTypedData(
params: LedgerSignTypedDataParams,
): Promise<LedgerSignTypedDataResponse>;

/**
* Method to retrieve the name and version of the running application on the Ledger device.
*
* @returns An object containing appName and version.
*/
getAppNameAndVersion(): Promise<GetAppNameAndVersionResponse>;
Comment thread
montelaidev marked this conversation as resolved.
};
Loading