CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-ledgerhq--hw-transport

Ledger Hardware Wallet common interface of the communication layer

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

error-handling.mddocs/

Error Handling

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.

Capabilities

Transport Errors

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);
    }
  }
}

Device Status Errors

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;
}

Race Condition Errors

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);
    }
  }
}

Status Codes

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...
}

Status Message Helper

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));
  }
}

Common Error Patterns

Connection and Discovery Errors

// 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");
}

User Interaction Errors

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");
}

Data Validation Errors

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);
}

Race Condition Handling

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;
  }
}

Error Recovery Strategies

Automatic Retry with Backoff

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();
  }
});

Error Context Enhancement

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);
  }
}

User-Friendly Error Messages

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.";
}

docs

apdu-communication.md

configuration.md

device-management.md

error-handling.md

events-lifecycle.md

index.md

tile.json