Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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