Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 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
4 changes: 4 additions & 0 deletions packages/hw-wallet-sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- export `ErrorMapping` type ([#446](https://github.com/MetaMask/accounts/pull/446))

## [0.2.0]

### Added
Expand Down
32 changes: 20 additions & 12 deletions packages/hw-wallet-sdk/src/hardware-error-mappings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@ import {
BLE_ERROR_MAPPINGS,
MOBILE_ERROR_MAPPINGS,
} from './hardware-error-mappings';
import type { ErrorMapping } from './hardware-error-mappings';
import { ErrorCode, Severity, Category } from './hardware-errors-enums';

describe('HARDWARE_ERROR_MAPPINGS', () => {
describe('Ledger mappings', () => {
const errorMappings = LEDGER_ERROR_MAPPINGS;
const getLedgerMapping = (code: string): ErrorMapping => {
const mapping = errorMappings[code];
if (!mapping) {
throw new Error(`Missing Ledger mapping for ${code}`);
}
return mapping;
};

it('has errorMappings object', () => {
expect(errorMappings).toBeDefined();
Expand All @@ -16,7 +24,7 @@ describe('HARDWARE_ERROR_MAPPINGS', () => {

describe('success codes', () => {
it('map 0x9000 to success', () => {
const mapping = errorMappings['0x9000'];
const mapping = getLedgerMapping('0x9000');
expect(mapping).toBeDefined();
expect(mapping.code).toBe(ErrorCode.Success);
expect(mapping.severity).toBe(Severity.Info);
Expand All @@ -26,58 +34,58 @@ describe('HARDWARE_ERROR_MAPPINGS', () => {

describe('authentication errors', () => {
it('map 0x6300 to authentication failed', () => {
const mapping = errorMappings['0x6300'];
const mapping = getLedgerMapping('0x6300');
expect(mapping.code).toBe(ErrorCode.AuthenticationFailed);
expect(mapping.severity).toBe(Severity.Err);
expect(mapping.category).toBe(Category.Authentication);
expect(mapping.userMessage).toBeDefined();
});

it('map 0x63c0 to PIN attempts remaining', () => {
const mapping = errorMappings['0x63c0'];
const mapping = getLedgerMapping('0x63c0');
expect(mapping.code).toBe(ErrorCode.AuthenticationPinAttemptsRemaining);
expect(mapping.severity).toBe(Severity.Warning);
});

it('map 0x5515 to device locked', () => {
const mapping = errorMappings['0x5515'];
const mapping = getLedgerMapping('0x5515');
expect(mapping.code).toBe(ErrorCode.AuthenticationDeviceLocked);
expect(mapping.severity).toBe(Severity.Err);
expect(mapping.userMessage).toContain('unlock');
});

it('map 0x9840 to device blocked', () => {
const mapping = errorMappings['0x9840'];
const mapping = getLedgerMapping('0x9840');
expect(mapping.code).toBe(ErrorCode.AuthenticationDeviceBlocked);
expect(mapping.severity).toBe(Severity.Critical);
});
});

describe('user action errors', () => {
it('map 0x6985 to user rejected', () => {
const mapping = errorMappings['0x6985'];
const mapping = getLedgerMapping('0x6985');
expect(mapping.code).toBe(ErrorCode.UserRejected);
expect(mapping.severity).toBe(Severity.Warning);
expect(mapping.category).toBe(Category.UserAction);
});

it('map 0x5501 to user refused', () => {
const mapping = errorMappings['0x5501'];
const mapping = getLedgerMapping('0x5501');
expect(mapping.code).toBe(ErrorCode.UserRejected);
expect(mapping.severity).toBe(Severity.Warning);
});
});
describe('connection errors', () => {
it('map 0x650f to connection issue', () => {
const mapping = errorMappings['0x650f'];
const mapping = getLedgerMapping('0x650f');
expect(mapping.code).toBe(ErrorCode.ConnectionClosed);
expect(mapping.category).toBe(Category.Connection);
});
});

describe('device state errors', () => {
it('maps 0x6f00 to device unresponsive', () => {
const mapping = errorMappings['0x6f00'];
const mapping = getLedgerMapping('0x6f00');
expect(mapping.code).toBe(ErrorCode.DeviceUnresponsive);
expect(mapping.severity).toBe(Severity.Err);
expect(mapping.category).toBe(Category.DeviceState);
Expand All @@ -86,7 +94,7 @@ describe('HARDWARE_ERROR_MAPPINGS', () => {
});

it('has valid structure for all mappings', () => {
Object.entries(errorMappings).forEach(([_, mapping]) => {
Object.values(errorMappings).forEach((mapping) => {
expect(mapping).toHaveProperty('code');
expect(mapping).toHaveProperty('message');
expect(mapping).toHaveProperty('severity');
Expand Down Expand Up @@ -173,7 +181,7 @@ describe('HARDWARE_ERROR_MAPPINGS', () => {
});

it('has valid structure for all mappings', () => {
Object.entries(errorMappings).forEach(([_, mapping]) => {
Object.values(errorMappings).forEach((mapping) => {
expect(mapping).toHaveProperty('code');
expect(mapping).toHaveProperty('message');
expect(mapping).toHaveProperty('severity');
Expand Down Expand Up @@ -206,7 +214,7 @@ describe('HARDWARE_ERROR_MAPPINGS', () => {
});

it('has valid structure for all mappings', () => {
Object.entries(errorMappings).forEach(([_, mapping]) => {
Object.values(errorMappings).forEach((mapping) => {
expect(mapping).toHaveProperty('code');
expect(mapping).toHaveProperty('message');
expect(mapping).toHaveProperty('severity');
Expand Down
10 changes: 9 additions & 1 deletion packages/hw-wallet-sdk/src/hardware-error-mappings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { ErrorCode, Severity, Category } from './hardware-errors-enums';

export const LEDGER_ERROR_MAPPINGS = {
export type ErrorMapping = {
code: ErrorCode;
message: string;
severity: Severity;
category: Category;
userMessage?: string;
};

export const LEDGER_ERROR_MAPPINGS: Record<string, ErrorMapping> = {
'0x9000': {
code: ErrorCode.Success,
message: 'Operation successful',
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 @@ -16,6 +16,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Normalize signature `v` value from Ledger devices for proper recovery ([#449](https://github.com/MetaMask/accounts/pull/449))
- 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.61,
functions: 98.18,
lines: 97.74,
statements: 97.76,
branches: 93.78,
functions: 98.27,
lines: 97.83,
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();
});
});
69 changes: 69 additions & 0 deletions packages/keyring-eth-ledger-bridge/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
ErrorMapping,
ErrorCode,
Severity,
Category,
HardwareWalletError,
LEDGER_ERROR_MAPPINGS,
} from '@metamask/hw-wallet-sdk';

/**
* 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 errorMapping = getLedgerErrorMapping(ledgerErrorCode);

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: ErrorCode.Unknown,
severity: Severity.Err,
category: Category.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,
): ErrorMapping | undefined {
return LEDGER_ERROR_MAPPINGS[ledgerErrorCode];
}
2 changes: 2 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,5 @@ 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';
Loading
Loading