Skip to content

Commit 0b3f823

Browse files
authored
feat: use hw wallet error for ledger keyring and add getAppNameAndVersion and getAppConfiguration (#446)
<!-- Thanks for your contribution! Take a moment to answer these questions so that reviewers have the information they need to properly understand your changes: * What is the current state of things and why does it need to change? * What is the solution your changes offer and how does it work? Are there any issues or other links reviewers should consult to understand this pull request better? For instance: * Fixes #12345 * See: #67890 --> This PR add a new method `getAppNameAndVersion` and `getAppConfiguration` to the ledger keyring and uses the hardware wallet errors. ## Examples <!-- Are there any examples of this change being used in another repository? When considering changes to the MetaMask module template, it's strongly preferred that the change be experimented with in another repository first. This gives reviewers a better sense of how the change works, making it less likely the change will need to be reverted or adjusted later. --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches Ledger signing error handling and public error shapes/messages, which can affect downstream consumers’ error parsing and UX; adds new bridge commands that depend on correct iframe/mobile transport support. > > **Overview** > Switches `@metamask/eth-ledger-bridge-keyring` from its custom `LedgerStatusError` flow to **standardized** `@metamask/hw-wallet-sdk` error mapping, introducing `createLedgerError`/`handleLedgerTransportError` that consistently throws `HardwareWalletError` (with selective `Ledger:` message prefixing for compatibility) and updating tests/messages accordingly. > > Extends the Ledger bridge/keyring surface area with `getAppNameAndVersion()` and `getAppConfiguration()` across iframe and mobile bridges (new iframe message actions included), and wires workspace dependency/build references so the keyring consumes the shared SDK types/mappings (including exporting the new `ErrorMapping` type from the SDK). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 61d895d. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 42541ee commit 0b3f823

25 files changed

Lines changed: 723 additions & 104 deletions

.syncpackrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
],
4242
"dependencies": [
4343
"@metamask/account-**",
44+
"@metamask/hw-wallet-sdk",
4445
"@metamask/keyring-**",
4546
"@metamask/eth-**-keyring"
4647
],

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ linkStyle default opacity:0.5
5757
eth_hd_keyring --> keyring_api;
5858
eth_hd_keyring --> keyring_utils;
5959
eth_hd_keyring --> account_api;
60+
eth_ledger_bridge_keyring --> hw_wallet_sdk;
6061
eth_ledger_bridge_keyring --> keyring_api;
6162
eth_ledger_bridge_keyring --> keyring_utils;
6263
eth_ledger_bridge_keyring --> account_api;

packages/hw-wallet-sdk/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Added
1313

14+
- export `ErrorMapping` type ([#446](https://github.com/MetaMask/accounts/pull/446))
1415
- Add hardware wallet connection types and improved error handling ([#456](https://github.com/MetaMask/accounts/pull/456))
1516
- Add `HardwareWalletType`, `ConnectionStatus`, `DeviceEvent` enums and `HardwareWalletConnectionState`, `DeviceEventPayload` types.
1617
- Add Ledger error mappings for device locked and Ethereum app closed states.

packages/hw-wallet-sdk/src/hardware-error-mappings.test.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,19 @@ import {
33
BLE_ERROR_MAPPINGS,
44
MOBILE_ERROR_MAPPINGS,
55
} from './hardware-error-mappings';
6+
import type { ErrorMapping } from './hardware-error-mappings';
67
import { ErrorCode, Severity, Category } from './hardware-errors-enums';
78

89
describe('HARDWARE_ERROR_MAPPINGS', () => {
910
describe('Ledger mappings', () => {
1011
const errorMappings = LEDGER_ERROR_MAPPINGS;
12+
const getLedgerMapping = (code: string): ErrorMapping => {
13+
const mapping = errorMappings[code];
14+
if (!mapping) {
15+
throw new Error(`Missing Ledger mapping for ${code}`);
16+
}
17+
return mapping;
18+
};
1119

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

1725
describe('success codes', () => {
1826
it('map 0x9000 to success', () => {
19-
const mapping = errorMappings['0x9000'];
27+
const mapping = getLedgerMapping('0x9000');
2028
expect(mapping).toBeDefined();
2129
expect(mapping.code).toBe(ErrorCode.Success);
2230
expect(mapping.severity).toBe(Severity.Info);
@@ -26,58 +34,58 @@ describe('HARDWARE_ERROR_MAPPINGS', () => {
2634

2735
describe('authentication errors', () => {
2836
it('map 0x6300 to authentication failed', () => {
29-
const mapping = errorMappings['0x6300'];
37+
const mapping = getLedgerMapping('0x6300');
3038
expect(mapping.code).toBe(ErrorCode.AuthenticationFailed);
3139
expect(mapping.severity).toBe(Severity.Err);
3240
expect(mapping.category).toBe(Category.Authentication);
3341
expect(mapping.userMessage).toBeDefined();
3442
});
3543

3644
it('map 0x63c0 to PIN attempts remaining', () => {
37-
const mapping = errorMappings['0x63c0'];
45+
const mapping = getLedgerMapping('0x63c0');
3846
expect(mapping.code).toBe(ErrorCode.AuthenticationPinAttemptsRemaining);
3947
expect(mapping.severity).toBe(Severity.Warning);
4048
});
4149

4250
it('map 0x5515 to device locked', () => {
43-
const mapping = errorMappings['0x5515'];
51+
const mapping = getLedgerMapping('0x5515');
4452
expect(mapping.code).toBe(ErrorCode.AuthenticationDeviceLocked);
4553
expect(mapping.severity).toBe(Severity.Err);
4654
expect(mapping.userMessage).toContain('unlock');
4755
});
4856

4957
it('map 0x9840 to device blocked', () => {
50-
const mapping = errorMappings['0x9840'];
58+
const mapping = getLedgerMapping('0x9840');
5159
expect(mapping.code).toBe(ErrorCode.AuthenticationDeviceBlocked);
5260
expect(mapping.severity).toBe(Severity.Critical);
5361
});
5462
});
5563

5664
describe('user action errors', () => {
5765
it('map 0x6985 to user rejected', () => {
58-
const mapping = errorMappings['0x6985'];
66+
const mapping = getLedgerMapping('0x6985');
5967
expect(mapping.code).toBe(ErrorCode.UserRejected);
6068
expect(mapping.severity).toBe(Severity.Warning);
6169
expect(mapping.category).toBe(Category.UserAction);
6270
});
6371

6472
it('map 0x5501 to user refused', () => {
65-
const mapping = errorMappings['0x5501'];
73+
const mapping = getLedgerMapping('0x5501');
6674
expect(mapping.code).toBe(ErrorCode.UserRejected);
6775
expect(mapping.severity).toBe(Severity.Warning);
6876
});
6977
});
7078
describe('connection errors', () => {
7179
it('map 0x650f to connection issue', () => {
72-
const mapping = errorMappings['0x650f'];
80+
const mapping = getLedgerMapping('0x650f');
7381
expect(mapping.code).toBe(ErrorCode.ConnectionClosed);
7482
expect(mapping.category).toBe(Category.Connection);
7583
});
7684
});
7785

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

8896
it('has valid structure for all mappings', () => {
89-
Object.entries(errorMappings).forEach(([_, mapping]) => {
97+
Object.values(errorMappings).forEach((mapping) => {
9098
expect(mapping).toHaveProperty('code');
9199
expect(mapping).toHaveProperty('message');
92100
expect(mapping).toHaveProperty('severity');
@@ -173,7 +181,7 @@ describe('HARDWARE_ERROR_MAPPINGS', () => {
173181
});
174182

175183
it('has valid structure for all mappings', () => {
176-
Object.entries(errorMappings).forEach(([_, mapping]) => {
184+
Object.values(errorMappings).forEach((mapping) => {
177185
expect(mapping).toHaveProperty('code');
178186
expect(mapping).toHaveProperty('message');
179187
expect(mapping).toHaveProperty('severity');
@@ -206,7 +214,7 @@ describe('HARDWARE_ERROR_MAPPINGS', () => {
206214
});
207215

208216
it('has valid structure for all mappings', () => {
209-
Object.entries(errorMappings).forEach(([_, mapping]) => {
217+
Object.values(errorMappings).forEach((mapping) => {
210218
expect(mapping).toHaveProperty('code');
211219
expect(mapping).toHaveProperty('message');
212220
expect(mapping).toHaveProperty('severity');

packages/hw-wallet-sdk/src/hardware-error-mappings.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { ErrorCode, Severity, Category } from './hardware-errors-enums';
22

3-
export const LEDGER_ERROR_MAPPINGS = {
3+
export type ErrorMapping = {
4+
code: ErrorCode;
5+
message: string;
6+
severity: Severity;
7+
category: Category;
8+
userMessage?: string;
9+
};
10+
11+
export const LEDGER_ERROR_MAPPINGS: Record<string, ErrorMapping> = {
412
'0x9000': {
513
code: ErrorCode.Success,
614
message: 'Operation successful',

packages/keyring-eth-ledger-bridge/CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- Integrate `@metamask/hw-wallet-sdk` for standardized hardware wallet error handling ([#446](https://github.com/MetaMask/accounts/pull/446))
13+
- Replace custom error handling with `HardwareWalletError` from the SDK.
14+
- Use `LEDGER_ERROR_MAPPINGS` from the SDK for consistent error code mapping.
15+
- Re-export `HardwareWalletError`, `ErrorCode`, `Severity`, `Category`, and error mappings for consumer convenience.
16+
- Deprecate `LedgerStatusError` in favor of `HardwareWalletError`.
17+
- Prefix Ledger transport error messages for compatibility
18+
1019
## [11.2.0]
1120

1221
### Added
@@ -15,7 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1524
- Wraps legacy `LedgerKeyring` to expose accounts via the unified `KeyringV2` API and the `KeyringAccount` type.
1625
- Extends `EthKeyringWrapper` for common Ethereum logic.
1726

18-
### Fixed
27+
### Changed
1928

2029
- Normalize signature `v` value from Ledger devices for proper recovery ([#449](https://github.com/MetaMask/accounts/pull/449))
2130

packages/keyring-eth-ledger-bridge/jest.config.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,21 @@ module.exports = merge(baseConfig, {
1515
displayName,
1616

1717
// An array of regexp pattern strings used to skip coverage collection
18-
coveragePathIgnorePatterns: ['./src/tests'],
18+
coveragePathIgnorePatterns: [
19+
'./src/tests',
20+
'./src/type.ts', // Deprecated, kept for backwards compatibility
21+
],
1922

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

2326
// An object that configures minimum threshold enforcement for coverage results
2427
coverageThreshold: {
2528
global: {
26-
branches: 93.61,
27-
functions: 98.18,
28-
lines: 97.74,
29-
statements: 97.76,
29+
branches: 93.36,
30+
functions: 98.29,
31+
lines: 97.86,
32+
statements: 97.88,
3033
},
3134
},
3235
});

packages/keyring-eth-ledger-bridge/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"@ethereumjs/util": "^9.1.0",
5252
"@ledgerhq/hw-app-eth": "^6.42.0",
5353
"@metamask/eth-sig-util": "^8.2.0",
54+
"@metamask/hw-wallet-sdk": "workspace:^",
5455
"@metamask/keyring-api": "workspace:^",
5556
"@metamask/keyring-utils": "workspace:^",
5657
"hdkey": "^2.1.0"
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import {
2+
HardwareWalletError,
3+
ErrorCode as ErrorCodeEnum,
4+
} from '@metamask/hw-wallet-sdk';
5+
6+
import {
7+
createLedgerError,
8+
isKnownLedgerError,
9+
getLedgerErrorMapping,
10+
} from './errors';
11+
12+
describe('createLedgerError', () => {
13+
describe('known error codes', () => {
14+
it('creates HardwareWalletError for user rejection (0x6985)', () => {
15+
const error = createLedgerError('0x6985');
16+
17+
expect(error).toBeInstanceOf(HardwareWalletError);
18+
expect(error.code).toBe(ErrorCodeEnum.UserRejected);
19+
expect(error.message).toBe('User rejected action on device');
20+
});
21+
22+
it('creates HardwareWalletError for device locked (0x5515)', () => {
23+
const error = createLedgerError('0x5515');
24+
25+
expect(error).toBeInstanceOf(HardwareWalletError);
26+
expect(error.code).toBe(ErrorCodeEnum.AuthenticationDeviceLocked);
27+
expect(error.message).toBe('Device is locked');
28+
});
29+
30+
it('includes context in message when provided', () => {
31+
const error = createLedgerError('0x6985', 'during sign transaction');
32+
33+
expect(error.message).toBe(
34+
'User rejected action on device (during sign transaction)',
35+
);
36+
});
37+
38+
it('uses error message as userMessage when userMessage is not provided in mapping', () => {
39+
// 0x9000 is success code which doesn't have a userMessage in the mapping
40+
const error = createLedgerError('0x9000');
41+
42+
expect(error).toBeInstanceOf(HardwareWalletError);
43+
// userMessage should fallback to the message
44+
expect(error.userMessage).toBeDefined();
45+
});
46+
});
47+
48+
describe('unknown error codes', () => {
49+
it('creates fallback error for unknown code without context', () => {
50+
const error = createLedgerError('0x9999');
51+
52+
expect(error).toBeInstanceOf(HardwareWalletError);
53+
expect(error.code).toBe(ErrorCodeEnum.Unknown);
54+
expect(error.message).toBe('Unknown Ledger error: 0x9999');
55+
});
56+
57+
it('creates fallback error for unknown code with context', () => {
58+
const error = createLedgerError('0x9999', 'during operation');
59+
60+
expect(error).toBeInstanceOf(HardwareWalletError);
61+
expect(error.code).toBe(ErrorCodeEnum.Unknown);
62+
expect(error.message).toBe(
63+
'Unknown Ledger error: 0x9999 (during operation)',
64+
);
65+
});
66+
});
67+
});
68+
69+
describe('isKnownLedgerError', () => {
70+
it('returns true for known error codes', () => {
71+
expect(isKnownLedgerError('0x6985')).toBe(true);
72+
expect(isKnownLedgerError('0x5515')).toBe(true);
73+
expect(isKnownLedgerError('0x6a80')).toBe(true);
74+
});
75+
76+
it('returns false for unknown error codes', () => {
77+
expect(isKnownLedgerError('0x9999')).toBe(false);
78+
expect(isKnownLedgerError('invalid')).toBe(false);
79+
expect(isKnownLedgerError('')).toBe(false);
80+
});
81+
});
82+
83+
describe('getLedgerErrorMapping', () => {
84+
it('returns mapping for known error codes', () => {
85+
const mapping = getLedgerErrorMapping('0x6985');
86+
87+
expect(mapping).toBeDefined();
88+
expect(mapping?.code).toBe(ErrorCodeEnum.UserRejected);
89+
expect(mapping?.message).toBe('User rejected action on device');
90+
});
91+
92+
it('returns undefined for unknown error codes', () => {
93+
const mapping = getLedgerErrorMapping('0x9999');
94+
95+
expect(mapping).toBeUndefined();
96+
});
97+
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {
2+
ErrorMapping,
3+
ErrorCode,
4+
Severity,
5+
Category,
6+
HardwareWalletError,
7+
LEDGER_ERROR_MAPPINGS,
8+
} from '@metamask/hw-wallet-sdk';
9+
10+
/**
11+
* Factory function to create a HardwareWalletError from a Ledger error code.
12+
*
13+
* @param ledgerErrorCode - The Ledger error code (e.g., '0x6985', '0x5515')
14+
* @param context - Optional additional context to append to the error message
15+
* @returns A HardwareWalletError instance with mapped error details
16+
*/
17+
export function createLedgerError(
18+
ledgerErrorCode: string,
19+
context?: string,
20+
): HardwareWalletError {
21+
const errorMapping = getLedgerErrorMapping(ledgerErrorCode);
22+
23+
if (errorMapping) {
24+
const message = context
25+
? `${errorMapping.message} (${context})`
26+
: errorMapping.message;
27+
28+
return new HardwareWalletError(message, {
29+
code: errorMapping.code,
30+
severity: errorMapping.severity,
31+
category: errorMapping.category,
32+
userMessage: errorMapping.userMessage ?? message,
33+
});
34+
}
35+
36+
// Fallback for unknown error codes
37+
const fallbackMessage = context
38+
? `Unknown Ledger error: ${ledgerErrorCode} (${context})`
39+
: `Unknown Ledger error: ${ledgerErrorCode}`;
40+
41+
return new HardwareWalletError(fallbackMessage, {
42+
code: ErrorCode.Unknown,
43+
severity: Severity.Err,
44+
category: Category.Unknown,
45+
userMessage: fallbackMessage,
46+
});
47+
}
48+
49+
/**
50+
* Checks if a Ledger error code exists in the error mappings.
51+
*
52+
* @param ledgerErrorCode - The Ledger error code to check
53+
* @returns True if the error code is mapped, false otherwise
54+
*/
55+
export function isKnownLedgerError(ledgerErrorCode: string): boolean {
56+
return ledgerErrorCode in LEDGER_ERROR_MAPPINGS;
57+
}
58+
59+
/**
60+
* Gets the error mapping details for a Ledger error code without creating an error instance.
61+
*
62+
* @param ledgerErrorCode - The Ledger error code to look up
63+
* @returns The error mapping details or undefined if not found
64+
*/
65+
export function getLedgerErrorMapping(
66+
ledgerErrorCode: string,
67+
): ErrorMapping | undefined {
68+
return LEDGER_ERROR_MAPPINGS[ledgerErrorCode];
69+
}

0 commit comments

Comments
 (0)