Ledger Hardware Wallet common interface of the communication layer
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Comprehensive error classes and patterns for handling transport-specific and device status errors. The @ledgerhq/hw-transport package provides detailed error information with specific error codes, status messages, and recovery strategies.
Generic transport errors that occur during communication setup, data exchange, or connection management.
/**
* TransportError is used for any generic transport errors
* e.g. Error thrown when data received by exchanges are incorrect or if exchange failed to communicate with the device for various reasons
*/
class TransportError extends Error {
constructor(message: string, id: string);
name: "TransportError";
message: string;
id: string;
stack: string;
}Usage Example:
import { TransportError } from "@ledgerhq/hw-transport";
try {
const transport = await MyTransport.create();
const largeData = Buffer.alloc(300); // Too large for APDU
await transport.send(0xB0, 0x01, 0x00, 0x00, largeData);
} catch (error) {
if (error instanceof TransportError) {
console.log("Transport error ID:", error.id); // "DataLengthTooBig"
console.log("Error message:", error.message); // "data.length exceed 256 bytes limit. Got: 300"
// Handle specific transport errors
switch (error.id) {
case "DataLengthTooBig":
console.log("Split data into smaller chunks");
break;
case "NoDeviceFound":
console.log("Please connect your Ledger device");
break;
case "ListenTimeout":
console.log("Timeout waiting for device - please retry");
break;
default:
console.log("Unknown transport error:", error.id);
}
}
}Errors returned by the Ledger device as status codes, indicating specific device states or command failures.
/**
* Error thrown when a device returned a non-success status
* The error.statusCode is one of the StatusCodes exported by this library
*/
class TransportStatusError extends Error {
constructor(statusCode: number);
name: "TransportStatusError";
message: string;
statusCode: number;
statusText: string;
stack: string;
}Errors that occur when multiple operations attempt to use the transport simultaneously, violating the atomic operation requirement.
/**
* Error thrown when an operation is attempted while another operation is in progress
* Prevents race conditions by enforcing atomic operations
*/
class TransportRaceCondition extends Error {
constructor(message: string);
name: "TransportRaceCondition";
message: string;
stack: string;
}Usage Example:
import { TransportRaceCondition } from "@ledgerhq/hw-transport";
// Example of race condition prevention
async function performOperation(transport) {
try {
// Start first operation
const operation1 = transport.send(0xB0, 0x01, 0x00, 0x00);
// Attempt second operation while first is running - this will throw
const operation2 = transport.send(0xB0, 0x02, 0x00, 0x00);
} catch (error) {
if (error instanceof TransportRaceCondition) {
console.log("Race condition detected:", error.message);
// "An action was already pending on the Ledger device. Please deny or reconnect."
// Wait for first operation to complete, then retry
await new Promise(resolve => setTimeout(resolve, 1000));
return performOperation(transport);
}
}
}Transport Status Error Usage:
import { TransportStatusError, StatusCodes } from "@ledgerhq/hw-transport";
try {
// Attempt to sign a transaction
const signature = await transport.send(0xE0, 0x04, 0x01, 0x00, transactionData);
} catch (error) {
if (error instanceof TransportStatusError) {
console.log("Status code:", "0x" + error.statusCode.toString(16));
console.log("Status text:", error.statusText);
console.log("Message:", error.message);
// Handle specific device status codes
switch (error.statusCode) {
case StatusCodes.CONDITIONS_OF_USE_NOT_SATISFIED:
console.log("User rejected the transaction");
showUserMessage("Transaction cancelled by user");
break;
case StatusCodes.SECURITY_STATUS_NOT_SATISFIED:
console.log("Device is locked or access denied");
showUserMessage("Please unlock your device and try again");
break;
case StatusCodes.INCORRECT_DATA:
console.log("Invalid transaction data");
showUserMessage("Transaction data is invalid");
break;
case StatusCodes.INS_NOT_SUPPORTED:
console.log("Command not supported by this app");
showUserMessage("Please open the correct app on your device");
break;
default:
console.log("Unknown device error:", error.statusText);
showUserMessage("Device error: " + error.message);
}
}
}Comprehensive set of status codes returned by Ledger devices, following ISO 7816-4 standards.
interface StatusCodes {
// Success
OK: 0x9000;
// Parameter errors
INCORRECT_LENGTH: 0x6700;
MISSING_CRITICAL_PARAMETER: 0x6800;
INCORRECT_DATA: 0x6A80;
INCORRECT_P1_P2: 0x6B00;
// Security errors
SECURITY_STATUS_NOT_SATISFIED: 0x6982;
CONDITIONS_OF_USE_NOT_SATISFIED: 0x6985;
// Command errors
INS_NOT_SUPPORTED: 0x6D00;
CLA_NOT_SUPPORTED: 0x6E00;
// System errors
TECHNICAL_PROBLEM: 0x6F00;
NOT_ENOUGH_MEMORY_SPACE: 0x6A84;
// Authentication errors
PIN_REMAINING_ATTEMPTS: 0x63C0;
CODE_BLOCKED: 0x9840;
// And many more...
}Utility function to get human-readable messages for status codes.
/**
* Get alternative status message for common error codes
* @param code Status code number
* @returns Human-readable error message or undefined
*/
function getAltStatusMessage(code: number): string | undefined | null;Usage Example:
import { getAltStatusMessage, StatusCodes } from "@ledgerhq/hw-transport";
// Get user-friendly messages
console.log(getAltStatusMessage(0x6700)); // "Incorrect length"
console.log(getAltStatusMessage(0x6982)); // "Security not satisfied (dongle locked or have invalid access rights)"
console.log(getAltStatusMessage(0x6985)); // "Condition of use not satisfied (denied by the user?)"
// Use in error handling
function handleStatusError(statusCode) {
const friendlyMessage = getAltStatusMessage(statusCode);
if (friendlyMessage) {
console.log("User-friendly error:", friendlyMessage);
} else {
console.log("Status code:", "0x" + statusCode.toString(16));
}
}// Device not found
try {
const transport = await MyTransport.create();
} catch (error) {
if (error instanceof TransportError && error.id === "NoDeviceFound") {
console.log("No Ledger device found. Please:");
console.log("1. Connect your device");
console.log("2. Make sure it's unlocked");
console.log("3. Open the correct app");
}
}
// Platform not supported
try {
const isSupported = await MyTransport.isSupported();
if (!isSupported) {
throw new Error("Transport not supported");
}
} catch (error) {
console.log("This browser/platform doesn't support Ledger connectivity");
console.log("Please try Chrome, Firefox, or use the desktop app");
}async function handleUserRejection(operation) {
try {
return await operation();
} catch (error) {
if (error instanceof TransportStatusError) {
switch (error.statusCode) {
case StatusCodes.CONDITIONS_OF_USE_NOT_SATISFIED:
return { rejected: true, reason: "user_cancelled" };
case StatusCodes.SECURITY_STATUS_NOT_SATISFIED:
return { rejected: true, reason: "device_locked" };
default:
throw error; // Re-throw other status errors
}
}
throw error; // Re-throw non-status errors
}
}
// Usage
const result = await handleUserRejection(async () => {
return await transport.send(0xE0, 0x04, 0x01, 0x00, txData);
});
if (result.rejected) {
if (result.reason === "user_cancelled") {
showMessage("Transaction cancelled by user");
} else if (result.reason === "device_locked") {
showMessage("Please unlock your device and try again");
}
} else {
showMessage("Transaction signed successfully");
}function validateAndSend(transport, data) {
// Pre-validate data size
if (data.length >= 256) {
throw new TransportError(
`Data too large: ${data.length} bytes (max 255)`,
"DataLengthTooBig"
);
}
// Pre-validate data format
if (!Buffer.isBuffer(data)) {
throw new TransportError(
"Data must be a Buffer",
"InvalidDataType"
);
}
return transport.send(0xB0, 0x01, 0x00, 0x00, data);
}import { TransportRaceCondition } from "@ledgerhq/errors";
async function safeExchange(transport, operation) {
try {
return await operation();
} catch (error) {
if (error instanceof TransportRaceCondition) {
console.log("Another operation is in progress");
// Wait and retry
await new Promise(resolve => setTimeout(resolve, 1000));
return await operation();
}
throw error;
}
}async function withRetry(operation, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
// Determine if error is retryable
const retryableErrors = [
"TransportRaceCondition",
"DisconnectedDevice",
"TransportInterfaceNotAvailable"
];
if (error instanceof TransportError &&
retryableErrors.includes(error.id)) {
console.log(`Attempt ${attempt} failed: ${error.id}, retrying...`);
// Exponential backoff
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
// Non-retryable error
throw error;
}
}
}
throw lastError;
}
// Usage
const result = await withRetry(async () => {
const transport = await MyTransport.create();
try {
return await transport.send(0xB0, 0x01, 0x00, 0x00);
} finally {
await transport.close();
}
});class EnhancedTransportError extends Error {
constructor(originalError, context) {
super(originalError.message);
this.name = "EnhancedTransportError";
this.originalError = originalError;
this.context = context;
this.timestamp = new Date().toISOString();
}
}
async function enhancedOperation(transport, operationName, operation) {
try {
return await operation();
} catch (error) {
const context = {
operation: operationName,
deviceModel: transport.deviceModel?.id,
exchangeTimeout: transport.exchangeTimeout,
timestamp: new Date().toISOString()
};
throw new EnhancedTransportError(error, context);
}
}function getUserFriendlyErrorMessage(error) {
if (error instanceof TransportStatusError) {
const messages = {
[StatusCodes.CONDITIONS_OF_USE_NOT_SATISFIED]:
"Please approve the action on your Ledger device",
[StatusCodes.SECURITY_STATUS_NOT_SATISFIED]:
"Please unlock your Ledger device",
[StatusCodes.INS_NOT_SUPPORTED]:
"Please open the correct app on your Ledger device",
[StatusCodes.INCORRECT_DATA]:
"Invalid data format - please check your input",
[StatusCodes.NOT_ENOUGH_MEMORY_SPACE]:
"Not enough space on your Ledger device"
};
return messages[error.statusCode] ||
`Device error: ${error.statusText}`;
}
if (error instanceof TransportError) {
const messages = {
"NoDeviceFound": "Please connect and unlock your Ledger device",
"ListenTimeout": "Device not found - please check connection",
"DataLengthTooBig": "Data too large - please contact support",
"TransportLocked": "Please wait for current operation to complete"
};
return messages[error.id] || `Connection error: ${error.message}`;
}
return "An unexpected error occurred. Please try again.";
}