Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 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
6 changes: 5 additions & 1 deletion packages/hw-device-sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

[Unreleased]: https://github.com/MetaMask/accounts/
### Added

- Add hardwared related error mappings and custom hardware error.

[Unreleased]: https://github.com/MetaMask/accounts/@0.0.0...HEAD
3 changes: 3 additions & 0 deletions packages/hw-device-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
"test:types": "../../scripts/tsd-test.sh ./src",
"test:watch": "jest --watch"
},
"dependencies": {
"@metamask/keyring-utils": "workspace:^"
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused dependency added but never imported

Low Severity

The @metamask/keyring-utils package is added as a dependency in package.json and as a TypeScript reference in tsconfig.build.json, but there are no imports or usages of this package anywhere in the source files. All source files (hardware-error.ts, hardware-error-mappings.ts, hardware-errors-enums.ts) only import from the local ./hardware-errors-enums module. This appears to be leftover from refactoring or was intended for code that wasn't included.

Additional Locations (1)

Fix in Cursor Fix in Web

"devDependencies": {
"@lavamoat/allow-scripts": "^3.2.1",
"@lavamoat/preinstall-always-fail": "^2.1.0",
Expand Down
117 changes: 117 additions & 0 deletions packages/hw-device-sdk/src/hardware-error-mappings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { LEDGER_ERROR_MAPPINGS } from './hardware-error-mappings';
import { ErrorCode, Severity, Category } from './hardware-errors-enums';

describe('HARDWARE_ERROR_MAPPINGS', () => {
describe('Ledger mappings', () => {
const errorMappings = LEDGER_ERROR_MAPPINGS;

it('have errorMappings object', () => {
expect(errorMappings).toBeDefined();
expect(typeof errorMappings).toBe('object');
});

describe('success codes', () => {
it('map 0x9000 to success', () => {
const mapping = errorMappings['0x9000'];
expect(mapping).toBeDefined();
expect(mapping.customCode).toBe(ErrorCode.Success);
expect(mapping.severity).toBe(Severity.Info);
expect(mapping.category).toBe(Category.Success);
});
});

describe('authentication errors', () => {
it('map 0x6300 to authentication failed', () => {
const mapping = errorMappings['0x6300'];
expect(mapping.customCode).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'];
expect(mapping.customCode).toBe(
ErrorCode.AuthenticationPinAttemptsRemaining,
);
expect(mapping.severity).toBe(Severity.Warning);
});

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

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

describe('user action errors', () => {
it('map 0x6985 to user rejected', () => {
const mapping = errorMappings['0x6985'];
expect(mapping.customCode).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'];
expect(mapping.customCode).toBe(ErrorCode.UserRejected);
expect(mapping.severity).toBe(Severity.Warning);
});
});
describe('connection errors', () => {
it('map 0x650f to connection issue', () => {
const mapping = errorMappings['0x650f'];
expect(mapping.customCode).toBe(ErrorCode.ConnClosed);
expect(mapping.category).toBe(Category.Connection);
});
});

it('have valid structure for all mappings', () => {
Object.entries(errorMappings).forEach(([_, mapping]) => {
expect(mapping).toHaveProperty('customCode');
expect(mapping).toHaveProperty('message');
expect(mapping).toHaveProperty('severity');
expect(mapping).toHaveProperty('category');

const numericErrorCodes = Object.values(ErrorCode).filter(
(value): value is number => typeof value === 'number',
);
expect(numericErrorCodes).toContain(mapping.customCode);
expect(Object.values(Severity)).toContain(mapping.severity);
expect(Object.values(Category)).toContain(mapping.category);
expect(typeof mapping.message).toBe('string');
});
});

it('have valid userMessage when present', () => {
const mappingsWithUserMessage = Object.values(errorMappings).filter(
(mapping): mapping is typeof mapping & { userMessage: string } =>
'userMessage' in mapping &&
typeof mapping.userMessage === 'string' &&
mapping.userMessage.length > 0,
);
expect(mappingsWithUserMessage.length).toBeGreaterThan(0);
mappingsWithUserMessage.forEach((mapping) => {
expect(typeof mapping.userMessage).toBe('string');
expect(mapping.userMessage.length).toBeGreaterThan(0);
});
});
});

describe('consistency checks', () => {
it('have unique error codes', () => {
const ledgerCodes = Object.values(LEDGER_ERROR_MAPPINGS);
const ledgerCustomCodes = ledgerCodes.map(
(mapping) => mapping.customCode,
);
expect(ledgerCustomCodes.length).toBeGreaterThan(0);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test claims uniqueness validation but doesn't verify it

Low Severity

The test named "should have unique error codes within each vendor" only asserts that ledgerCustomCodes.length is greater than zero. It doesn't actually verify uniqueness - that would require comparing the array length against a Set of the same values. Looking at the actual error mappings, multiple vendor codes intentionally map to the same customCode (e.g., both 0x6985 and 0x5501 map to UserRejected). The test name is misleading and could give false confidence that uniqueness is being validated when it's not.

Fix in Cursor Fix in Web

});
});
117 changes: 117 additions & 0 deletions packages/hw-device-sdk/src/hardware-error-mappings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { ErrorCode, Severity, Category } from './hardware-errors-enums';

export const LEDGER_ERROR_MAPPINGS = {
'0x9000': {
customCode: ErrorCode.Success,
message: 'Operation successful',
severity: Severity.Info,
category: Category.Success,
},
'0x6300': {
customCode: ErrorCode.AuthenticationFailed,
message: 'Authentication failed',
severity: Severity.Err,
category: Category.Authentication,
userMessage: 'Authentication failed. Please verify your credentials.',
},
'0x63c0': {
customCode: ErrorCode.AuthenticationPinAttemptsRemaining,
message: 'PIN attempts remaining',
severity: Severity.Warning,
category: Category.Authentication,
userMessage: 'Incorrect PIN. Please try again.',
},
'0x6982': {
customCode: ErrorCode.AuthenticationSecurityCondition,
message: 'Security conditions not satisfied',
severity: Severity.Err,
category: Category.Authentication,

userMessage:
'Device is locked or access rights are insufficient. Please unlock your device.',
},
'0x6985': {
customCode: ErrorCode.UserRejected,
message: 'User rejected action on device',
severity: Severity.Warning,
category: Category.UserAction,

userMessage:
'Transaction was rejected. Please approve on your device to continue.',
},
'0x9804': {
customCode: ErrorCode.AuthenticationSecurityCondition,
message: 'App update required',
severity: Severity.Err,
category: Category.Authentication,

userMessage: 'Please update your Ledger app to continue.',
},
'0x9808': {
customCode: ErrorCode.AuthenticationFailed,
message: 'Contradiction in secret code status',
severity: Severity.Err,
category: Category.Authentication,
},
'0x9840': {
customCode: ErrorCode.AuthenticationDeviceBlocked,
message: 'Code blocked',
severity: Severity.Critical,
category: Category.Authentication,

userMessage:
'Your device is blocked due to too many failed attempts. Please follow device recovery procedures.',
},
'0x650f': {
customCode: ErrorCode.ConnClosed,
message: 'App closed or connection issue',
severity: Severity.Err,
category: Category.Connection,
userMessage:
'Connection lost or app closed. Please open the corresponding app on your Ledger device.',
},
'0x5515': {
customCode: ErrorCode.AuthenticationDeviceLocked,
message: 'Device is locked',
severity: Severity.Err,
category: Category.Authentication,
userMessage: 'Please unlock your Ledger device to continue.',
},
'0x5501': {
customCode: ErrorCode.UserRejected,
message: 'User refused on device',
severity: Severity.Warning,
category: Category.UserAction,
userMessage:
'Operation was rejected. Please approve on your device to continue.',
},
'0x6a80': {
customCode: ErrorCode.DeviceStateBlindSignNotSupported,
message: 'Blind signing not supported',
severity: Severity.Err,
category: Category.DeviceState,
userMessage: 'Blind signing is not supported on this device.',
},
'0x6d00': {
customCode: ErrorCode.DeviceStateOnlyV4Supported,
message: 'Ledger Only V4 supported',
severity: Severity.Err,
category: Category.DeviceState,
userMessage: 'Only V4 is supported on this device.',
},
'0x6e00': {
customCode: ErrorCode.DeviceStateEthAppClosed,
message: 'Ethereum app closed',
severity: Severity.Err,
category: Category.DeviceState,
userMessage: 'Ethereum app is closed. Please open it to continue.',
},
'0x6501': {
customCode: ErrorCode.DeviceStateEthAppOutOfDate,
message: 'Ethereum app out of date',
severity: Severity.Err,
category: Category.DeviceState,
userMessage: 'Ethereum app is out of date. Please update it to continue.',
},
};
Loading
Loading