Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ linkStyle default opacity:0.5
eth_qr_keyring --> account_api;
eth_simple_keyring --> keyring_api;
eth_simple_keyring --> keyring_utils;
eth_trezor_keyring --> hw_wallet_sdk;
eth_trezor_keyring --> keyring_api;
eth_trezor_keyring --> keyring_utils;
eth_trezor_keyring --> account_api;
Expand Down
7 changes: 7 additions & 0 deletions packages/keyring-eth-trezor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Wraps legacy `TrezorKeyring` and `OneKeyKeyring` to expose accounts via the unified `KeyringV2` API and the `KeyringAccount` type.
- Extends `EthKeyringWrapper` for common Ethereum logic.

### Changed

- Integrate `@metamask/hw-wallet-sdk` for standardized Trezor error handling ([#471](https://github.com/MetaMask/accounts/pull/471))
- Replace custom transport and user-action error handling with typed `HardwareWalletError` instances.
- Add Trezor-specific error mappings for consistent `ErrorCode`, `Severity`, and `Category` classification.
- Export Trezor error helpers for creating and normalizing typed hardware wallet errors.

## [9.0.0]

### Changed
Expand Down
8 changes: 4 additions & 4 deletions packages/keyring-eth-trezor/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ module.exports = merge(baseConfig, {
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 62.65,
functions: 93.15,
lines: 93.57,
statements: 93.66,
branches: 83.05,
functions: 95.89,
lines: 96.43,
statements: 96.48,
},
},
});
1 change: 1 addition & 0 deletions packages/keyring-eth-trezor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@ethereumjs/tx": "^5.4.0",
"@ethereumjs/util": "^9.1.0",
"@metamask/eth-sig-util": "^8.2.0",
"@metamask/hw-wallet-sdk": "workspace:^",
"@metamask/keyring-api": "workspace:^",
"@metamask/keyring-utils": "workspace:^",
"@metamask/utils": "^11.1.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/keyring-eth-trezor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ export * from './trezor-keyring';
export * from './trezor-keyring-v2';
export * from './onekey-keyring';
export * from './onekey-keyring-v2';
export * from './trezor-error-handler';
export * from './trezor-errors';
export type * from './trezor-bridge';
export * from './trezor-connect-bridge';
158 changes: 158 additions & 0 deletions packages/keyring-eth-trezor/src/trezor-error-handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import {
HardwareWalletError,
ErrorCode,
Severity,
Category,
} from '@metamask/hw-wallet-sdk';

import { handleTrezorTransportError } from './trezor-error-handler';

describe('handleTrezorTransportError', () => {
const fallbackMessage = 'Default Trezor error';

it.each([
{
tc: 'transport missing',
input: Object.assign(new Error('error'), {
code: 'Transport_Missing',
}),
code: ErrorCode.ConnectionTransportMissing,
},
{
tc: 'disconnected device',
input: Object.assign(new Error('error'), {
code: 'Device_Disconnected',
}),
code: ErrorCode.DeviceDisconnected,
},
{
tc: 'closed popup/session',
input: Object.assign(new Error('error'), {
code: 'Method_Interrupted',
}),
code: ErrorCode.ConnectionClosed,
},
{
tc: 'cancelled action',
input: Object.assign(new Error('error'), { code: 'Method_Cancel' }),
code: ErrorCode.UserCancelled,
},
{
tc: 'rejected action',
input: Object.assign(new Error('error'), {
code: 'Method_PermissionsNotGranted',
}),
code: ErrorCode.UserRejected,
},
{
tc: 'timeout',
input: Object.assign(new Error('error'), {
code: 'Init_IframeTimeout',
}),
code: ErrorCode.ConnectionTimeout,
},
])('maps $tc to HardwareWalletError', ({ input, code }) => {
let thrownError: unknown;
try {
handleTrezorTransportError(input, fallbackMessage);
} catch (error) {
thrownError = error;
}

expect(thrownError).toBeInstanceOf(HardwareWalletError);
expect((thrownError as HardwareWalletError).code).toBe(code);
expect((thrownError as HardwareWalletError).cause).toBe(input);
});

it('prioritizes machine-readable code when present', () => {
const error = new Error('error') as Error & { code: string };
error.code = 'Method_PermissionsNotGranted';

let thrownError: unknown;
try {
handleTrezorTransportError(error, fallbackMessage);
} catch (error_) {
thrownError = error_;
}

expect(thrownError).toBeInstanceOf(HardwareWalletError);
expect((thrownError as HardwareWalletError).code).toBe(
ErrorCode.UserRejected,
);
});

it('uses error name as fallback identifier when code is absent', () => {
const error = new Error('error');
error.name = 'Device_Disconnected';

let thrownError: unknown;
try {
handleTrezorTransportError(error, fallbackMessage);
} catch (error_) {
thrownError = error_;
}

expect(thrownError).toBeInstanceOf(HardwareWalletError);
expect((thrownError as HardwareWalletError).code).toBe(
ErrorCode.DeviceDisconnected,
);
});

it('passes through HardwareWalletError instances unchanged', () => {
const originalError = new HardwareWalletError('original', {
code: ErrorCode.UserRejected,
severity: Severity.Warning,
category: Category.UserAction,
userMessage: 'original',
});

let thrownError: unknown;
try {
handleTrezorTransportError(originalError, fallbackMessage);
} catch (error) {
thrownError = error;
}

expect(thrownError).toBe(originalError);
});

it('wraps unknown Error instances as ErrorCode.Unknown', () => {
const originalError = new Error('Unexpected Trezor failure');

let thrownError: unknown;
try {
handleTrezorTransportError(originalError, fallbackMessage);
} catch (error) {
thrownError = error;
}

expect(thrownError).toBeInstanceOf(HardwareWalletError);
expect((thrownError as HardwareWalletError).code).toBe(ErrorCode.Unknown);
expect((thrownError as HardwareWalletError).cause).toBe(originalError);
expect((thrownError as HardwareWalletError).message).toBe(
'Unexpected Trezor failure',
);
});

it.each([null, undefined, 'string error', { message: 'not an error' }])(
'uses fallback for non-Error input: %p',
(value) => {
const throwingFunction = (): never =>
handleTrezorTransportError(value, fallbackMessage);

expect(throwingFunction).toThrow(HardwareWalletError);
expect(throwingFunction).toThrow(fallbackMessage);
},
);

it('has never return type', () => {
type ReturnTypeIsNever = ReturnType<
typeof handleTrezorTransportError
> extends never
? true
: false;

const isNever: ReturnTypeIsNever = true;
expect(isNever).toBe(true);
});
});
73 changes: 73 additions & 0 deletions packages/keyring-eth-trezor/src/trezor-error-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
ErrorCode,
Severity,
Category,
HardwareWalletError,
} from '@metamask/hw-wallet-sdk';

import { createTrezorError, getTrezorErrorIdentifier } from './trezor-errors';

type ErrorDetails = {
message?: string;
code?: string;
name?: string;
};

function getErrorDetails(error: Error): ErrorDetails {
const details: ErrorDetails = {
message: error.message,
name: error.name,
};

if ('code' in error) {
const { code } = error as Error & { code?: unknown };
if (typeof code === 'string') {
details.code = code;
}
}

return details;
}

/**
* Converts unknown Trezor errors into typed HardwareWalletError instances.
*
* @param error - Error thrown from Trezor bridge or keyring flow.
* @param fallbackMessage - Default message for unknown non-Error inputs.
* @throws HardwareWalletError Always throws typed errors.
*/
export function handleTrezorTransportError(
error: unknown,
fallbackMessage: string,
): never {
if (error instanceof HardwareWalletError) {
throw error;
}

if (error instanceof Error) {
const details = getErrorDetails(error);
const identifier =
getTrezorErrorIdentifier(details.code) ??
getTrezorErrorIdentifier(details.name) ??
getTrezorErrorIdentifier(details.message);

if (identifier) {
throw createTrezorError(identifier, details.message, error);
}

throw new HardwareWalletError(details.message ?? fallbackMessage, {
code: ErrorCode.Unknown,
severity: Severity.Err,
category: Category.Unknown,
userMessage: details.message ?? fallbackMessage,
cause: error,
});
}

throw new HardwareWalletError(fallbackMessage, {
code: ErrorCode.Unknown,
severity: Severity.Err,
category: Category.Unknown,
userMessage: fallbackMessage,
});
}
113 changes: 113 additions & 0 deletions packages/keyring-eth-trezor/src/trezor-errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {
Category,
ErrorCode,
HardwareWalletError,
Severity,
} from '@metamask/hw-wallet-sdk';
import { ERRORS } from '@trezor/connect-web';

import {
createTrezorError,
getTrezorErrorIdentifier,
getTrezorErrorMapping,
isKnownTrezorError,
} from './trezor-errors';

describe('trezor-errors', () => {
describe('isKnownTrezorError', () => {
it('returns true for known identifiers', () => {
expect(isKnownTrezorError('Device_Disconnected')).toBe(true);
expect(isKnownTrezorError('Method_Cancel')).toBe(true);
});

it('returns false for unknown identifiers', () => {
expect(isKnownTrezorError('unknownIdentifier')).toBe(false);
expect(isKnownTrezorError('')).toBe(false);
});
});

describe('getTrezorErrorMapping', () => {
it('maps all current TrezorConnect error codes', () => {
for (const identifier of Object.keys(ERRORS.ERROR_CODES)) {
expect(getTrezorErrorMapping(identifier)).toBeDefined();
}
});

it('returns mapping for known identifiers', () => {
expect(getTrezorErrorMapping('Init_IframeTimeout')).toMatchObject({
code: ErrorCode.ConnectionTimeout,
severity: Severity.Err,
category: Category.Connection,
});
});

it('returns undefined for unknown identifiers', () => {
expect(getTrezorErrorMapping('not-real')).toBeUndefined();
});
});

describe('getTrezorErrorIdentifier', () => {
it('returns undefined for empty values', () => {
expect(getTrezorErrorIdentifier(undefined)).toBeUndefined();
expect(getTrezorErrorIdentifier('')).toBeUndefined();
});

it('matches known identifiers case-insensitively', () => {
expect(getTrezorErrorIdentifier('Device_Disconnected')).toBe(
'Device_Disconnected',
);
expect(getTrezorErrorIdentifier('DEVice_disconnected')).toBe(
'Device_Disconnected',
);
});

it('maps sdk messages to identifiers', () => {
expect(getTrezorErrorIdentifier('Device disconnected')).toBe(
'Device_Disconnected',
);
});

it('does not resolve removed legacy identifiers', () => {
expect(getTrezorErrorIdentifier('deviceDisconnected')).toBeUndefined();
expect(getTrezorErrorIdentifier('connectionTimeout')).toBeUndefined();
});
});

describe('createTrezorError', () => {
it('creates typed errors for known identifiers', () => {
const cause = new Error('underlying');
const error = createTrezorError('Transport_Missing', undefined, cause);

expect(error).toBeInstanceOf(HardwareWalletError);
expect(error.code).toBe(ErrorCode.ConnectionTransportMissing);
expect(error.severity).toBe(Severity.Err);
expect(error.category).toBe(Category.Connection);
expect(error.cause).toBe(cause);
});

it('appends context when it differs from mapped message', () => {
const error = createTrezorError('Method_Cancel', 'during sign operation');
expect(error.message).toContain('(during sign operation)');
});

it('does not append context when it only repeats mapped message casing/spacing', () => {
const error = createTrezorError(
'Method_Cancel',
' USER CANCELLED ACTION ON TREZOR DEVICE ',
);
expect(error.message).toBe('User cancelled action on Trezor device');
});

it('falls back to ErrorCode.Unknown for unknown identifiers', () => {
const cause = new Error('unknown cause');
const error = createTrezorError('not-real', 'while testing', cause);
expect(error).toBeInstanceOf(HardwareWalletError);
expect(error.code).toBe(ErrorCode.Unknown);
expect(error.category).toBe(Category.Unknown);
expect(error.userMessage).toBe(
'Unknown Trezor error: not-real (while testing)',
);
expect(error.cause).toBe(cause);
});
});
});
Loading
Loading