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
8 changes: 4 additions & 4 deletions packages/keyring-eth-ledger-bridge/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: 91.89,
functions: 97.93,
lines: 97.28,
statements: 97.31,
branches: 92.35,
functions: 98.09,
lines: 97.47,
statements: 97.49,
},
},
});
1 change: 1 addition & 0 deletions packages/keyring-eth-ledger-bridge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@ethereumjs/util": "^9.1.0",
"@ledgerhq/hw-app-eth": "^6.42.0",
"@metamask/eth-sig-util": "^8.2.0",
"@metamask/keyring-utils": "workspace:^",
"hdkey": "^2.1.0"
},
"devDependencies": {
Expand Down
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();
});
});
99 changes: 99 additions & 0 deletions packages/keyring-eth-ledger-bridge/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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 LedgerHardwareWalletError 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
*
* @example
* ```typescript
* const error = createLedgerError('0x6985'); // User rejected action
* const errorWithContext = createLedgerError('0x6985', 'during transaction signing');
* ```
*/
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];
}
2 changes: 2 additions & 0 deletions packages/keyring-eth-ledger-bridge/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export type * from './ledger-bridge';
export * from './ledger-transport-middleware';
export type * from './type';
export * from './ledger-hw-app';
export * from './errors';
export * from './ledger-error-handler';
12 changes: 12 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 @@ -63,4 +68,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