Skip to content
Draft
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
81 changes: 81 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,81 @@
import {
ErrorCode as ErrorCodeEnum,
Severity as SeverityEnum,
Category as CategoryEnum,
RetryStrategy as RetryStrategyEnum,
HardwareWalletError,
} from '@metamask/keyring-utils';

import {
createLedgerError,
isKnownLedgerError,
getLedgerErrorMapping,
} from './errors';

describe('createLedgerError', () => {
it('should create a HardwareWalletError from a known error code', () => {
const error = createLedgerError('0x6985');

expect(error).toBeInstanceOf(HardwareWalletError);
expect(error.message).toContain('User rejected');
expect(error.code).toBe(ErrorCodeEnum.USER_CANCEL_001);
});

it('should create a HardwareWalletError with context', () => {
const error = createLedgerError('0x6985', 'during transaction signing');

expect(error).toBeInstanceOf(HardwareWalletError);
expect(error.message).toContain('User rejected');
expect(error.message).toContain('(during transaction signing)');
});

it('should create a fallback error for unknown error codes without context', () => {
const error = createLedgerError('0x9999');

expect(error).toBeInstanceOf(HardwareWalletError);
expect(error.message).toBe('Unknown Ledger error: 0x9999');
expect(error.code).toBe(ErrorCodeEnum.UNKNOWN_001);
expect(error.severity).toBe(SeverityEnum.ERROR);
expect(error.category).toBe(CategoryEnum.UNKNOWN);
expect(error.retryStrategy).toBe(RetryStrategyEnum.NO_RETRY);
});

it('should create a fallback error for unknown error codes with context', () => {
const error = createLedgerError('0x9999', 'while doing something');

expect(error).toBeInstanceOf(HardwareWalletError);
expect(error.message).toBe(
'Unknown Ledger error: 0x9999 (while doing something)',
);
expect(error.code).toBe(ErrorCodeEnum.UNKNOWN_001);
});
});

describe('isKnownLedgerError', () => {
it('should return true for known error codes', () => {
expect(isKnownLedgerError('0x6985')).toBe(true);
expect(isKnownLedgerError('0x5515')).toBe(true);
expect(isKnownLedgerError('0x6a80')).toBe(true);
});

it('should return false for unknown error codes', () => {
expect(isKnownLedgerError('0x9999')).toBe(false);
expect(isKnownLedgerError('0x0000')).toBe(false);
});
});

describe('getLedgerErrorMapping', () => {
it('should return error mapping for known error codes', () => {
const mapping = getLedgerErrorMapping('0x6985');

expect(mapping).toBeDefined();
expect(mapping?.customCode).toBe(ErrorCodeEnum.USER_CANCEL_001);
expect(mapping?.message).toContain('User rejected');
});

it('should return undefined for unknown error codes', () => {
const mapping = getLedgerErrorMapping('0x9999');

expect(mapping).toBeUndefined();
});
});
93 changes: 93 additions & 0 deletions packages/keyring-eth-ledger-bridge/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {
type ErrorCode,
type Severity,
type Category,
type RetryStrategy,
HardwareWalletError,
HARDWARE_MAPPINGS,
ErrorCode as ErrorCodeEnum,
Severity as SeverityEnum,
Category as CategoryEnum,
RetryStrategy as RetryStrategyEnum,
} from '@metamask/keyring-utils';

type LedgerErrorMapping = {
customCode: ErrorCode;
message: string;
severity: Severity;
category: Category;
retryStrategy: RetryStrategy;
userActionable: boolean;
userMessage?: string;
};

/**
* 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 LedgerHardwareWalletError instance with mapped error details
*/
export function createLedgerError(
ledgerErrorCode: string,
context?: string,
): HardwareWalletError {
const mappings = HARDWARE_MAPPINGS.ledger.errorMappings as {
[key: string]: LedgerErrorMapping;
};
const errorMapping = mappings[ledgerErrorCode];

if (errorMapping) {
const message = context
? `${errorMapping.message} (${context})`
: errorMapping.message;

return new HardwareWalletError(message, {
code: errorMapping.customCode,
severity: errorMapping.severity,
category: errorMapping.category,
retryStrategy: errorMapping.retryStrategy,
userActionable: errorMapping.userActionable,
userMessage: errorMapping.userMessage ?? '',
});
}

// Fallback for unknown error codes
const fallbackMessage = context
? `Unknown Ledger error: ${ledgerErrorCode} (${context})`
: `Unknown Ledger error: ${ledgerErrorCode}`;

return new HardwareWalletError(fallbackMessage, {
code: ErrorCodeEnum.UNKNOWN_001,
severity: SeverityEnum.ERROR,
category: CategoryEnum.UNKNOWN,
retryStrategy: RetryStrategyEnum.NO_RETRY,
userActionable: false,
userMessage: '',
});
}

/**
* 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 HARDWARE_MAPPINGS.ledger.errorMappings;
}

/**
* 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,
): LedgerErrorMapping | undefined {
const mappings = HARDWARE_MAPPINGS.ledger.errorMappings as {
[key: string]: LedgerErrorMapping;
};
return mappings[ledgerErrorCode];
}
3 changes: 2 additions & 1 deletion packages/keyring-eth-ledger-bridge/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
export * from './ledger-keyring';
export * from './ledger-keyring-v2';
export * from './ledger-iframe-bridge';
export * from './ledger-mobile-bridge';
export type * from './ledger-bridge';

Check failure on line 4 in packages/keyring-eth-ledger-bridge/src/index.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (22.x)

Multiple exports of name 'GetAppNameAndVersionResponse'
export * from './ledger-transport-middleware';
export type * from './type';

Check failure on line 6 in packages/keyring-eth-ledger-bridge/src/index.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (22.x)

Multiple exports of name 'GetAppNameAndVersionResponse'
export * from './ledger-hw-app';
export * from './errors';
export * from './ledger-error-handler';
14 changes: 14 additions & 0 deletions packages/keyring-eth-ledger-bridge/src/ledger-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export type LedgerSignTypedDataResponse = Awaited<
ReturnType<LedgerHwAppEth['signEIP712HashedMessage']>
>;

export type GetAppNameAndVersionResponse = {
appName: string;
version: string;
};

export type LedgerBridgeOptions = Record<string, unknown>;

export type LedgerBridge<T extends LedgerBridgeOptions> = {
Expand Down Expand Up @@ -52,6 +57,8 @@ export type LedgerBridge<T extends LedgerBridgeOptions> = {

getPublicKey(params: GetPublicKeyParams): Promise<GetPublicKeyResponse>;

getAppNameAndVersion(): Promise<GetAppNameAndVersionResponse>;

deviceSignTransaction(
params: LedgerSignTransactionParams,
): Promise<LedgerSignTransactionResponse>;
Expand All @@ -63,4 +70,11 @@ export type LedgerBridge<T extends LedgerBridgeOptions> = {
deviceSignTypedData(
params: LedgerSignTypedDataParams,
): Promise<LedgerSignTypedDataResponse>;

/**
* Method to retrieve the name and version of the running application on the Ledger device.
*
* @returns An object containing appName and version.
*/
getAppNameAndVersion(): Promise<GetAppNameAndVersionResponse>;
};
Loading
Loading