or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

index.md
tile.json

tessl/npm-ledgerhq--hw-transport-webusb

Ledger Hardware Wallet WebUSB implementation of the communication layer for web browsers

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
npmpkg:npm/@ledgerhq/hw-transport-webusb@6.29.x

To install, run

npx @tessl/cli install tessl/npm-ledgerhq--hw-transport-webusb@6.29.0

index.mddocs/

Ledger WebUSB Transport

Ledger WebUSB Transport provides a WebUSB-based communication layer for interacting with Ledger Hardware Wallets in web browsers. It enables secure APDU (Application Protocol Data Unit) exchange between web applications and Ledger devices through the WebUSB API, handling device discovery, connection management, and protocol-level communication.

Package Information

  • Package Name: @ledgerhq/hw-transport-webusb
  • Package Type: npm
  • Language: TypeScript
  • Installation: npm install @ledgerhq/hw-transport-webusb
  • Browser Requirements: WebUSB support (Chrome/Chromium-based browsers), HTTPS required

Core Imports

import TransportWebUSB from "@ledgerhq/hw-transport-webusb";

For CommonJS:

const TransportWebUSB = require("@ledgerhq/hw-transport-webusb").default;

Named imports for error classes:

import TransportWebUSB, {
  TransportOpenUserCancelled,
  TransportInterfaceNotAvailable,
  TransportWebUSBGestureRequired,
  DisconnectedDeviceDuringOperation,
  DisconnectedDevice,
  TransportError,
  TransportStatusError,
  StatusCodes,
  getAltStatusMessage
} from "@ledgerhq/hw-transport-webusb";

Basic Usage

import TransportWebUSB from "@ledgerhq/hw-transport-webusb";

// Check if WebUSB is supported
const isSupported = await TransportWebUSB.isSupported();

if (isSupported) {
  // Create a transport connection (shows permission dialog if needed)
  const transport = await TransportWebUSB.create();
  
  // Exchange APDU with the device
  const apdu = Buffer.from("E0C4000000", "hex"); // Get app name APDU
  const response = await transport.exchange(apdu);
  
  // Close the connection
  await transport.close();
}

Architecture

The Ledger WebUSB Transport is built around several key components:

  • TransportWebUSB Class: Main transport implementation extending the base Transport class
  • Device Management: Static methods for device discovery, permission handling, and connection establishment
  • APDU Protocol: Low-level APDU exchange with HID framing for USB communication
  • Event System: Disconnect detection and transport lifecycle events
  • Error Handling: Comprehensive error types for various failure scenarios

Capabilities

Transport Creation and Management

Core functionality for establishing and managing WebUSB connections to Ledger devices.

/**
 * Main WebUSB transport class for Ledger devices
 */
export default class TransportWebUSB extends Transport {
  constructor(device: USBDevice, interfaceNumber: number);
  
  /** The connected USB device */
  device: USBDevice;
  
  /** Identified device model information */
  deviceModel: DeviceModel | null | undefined;
  
  /** Communication channel identifier */
  channel: number;
  
  /** USB packet size in bytes */
  packetSize: number;
  
  /** USB interface number */
  interfaceNumber: number;
}

Static Device Management Methods

Methods for checking support, discovering devices, and establishing connections.

/**
 * Check if WebUSB transport is supported in the current browser
 * @returns Promise resolving to boolean indicating support
 */
static isSupported(): Promise<boolean>;

/**
 * List WebUSB devices that were previously authorized by the user
 * @returns Promise resolving to array of authorized USBDevice objects
 */
static list(): Promise<USBDevice[]>;

/**
 * Actively listen to WebUSB devices and emit ONE device
 * Important: Must be called in the context of a UI click
 * @param observer - Observer for device descriptor events
 * @returns Subscription object with unsubscribe method
 */
static listen(observer: Observer<DescriptorEvent<USBDevice>>): Subscription;

/**
 * Always display device permission dialog, even if devices are already accepted
 * @returns Promise resolving to new TransportWebUSB instance
 */
static request(): Promise<TransportWebUSB>;

/**
 * Never display device permission dialog, returns null if no device found
 * @returns Promise resolving to TransportWebUSB instance or null
 */
static openConnected(): Promise<TransportWebUSB | null>;

/**
 * Create a Ledger transport with a specific USBDevice
 * @param device - The USBDevice to create transport for
 * @returns Promise resolving to new TransportWebUSB instance
 */
static open(device: USBDevice): Promise<TransportWebUSB>;

/**
 * Create a transport (inherited from base Transport class)
 * @param openTimeout - Timeout for opening connection
 * @param listenTimeout - Timeout for listening to devices
 * @returns Promise resolving to new TransportWebUSB instance
 */
static create(openTimeout?: number, listenTimeout?: number): Promise<TransportWebUSB>;

APDU Communication

Low-level APDU exchange functionality for communicating with Ledger device applications.

/**
 * Exchange APDU with the device using the WebUSB protocol
 * @param apdu - The APDU buffer to send to the device
 * @returns Promise resolving to response APDU buffer
 */
exchange(apdu: Buffer): Promise<Buffer>;

/**
 * Release the transport device and close the connection
 * @returns Promise that resolves when connection is closed
 */
close(): Promise<void>;

/**
 * Legacy method for scramble key (no-op implementation)
 */
setScrambleKey(): void;

High-Level Transport Methods

Inherited methods from the base Transport class for higher-level APDU operations.

/**
 * Send APDU command with automatic status code handling
 * @param cla - Class byte
 * @param ins - Instruction byte  
 * @param p1 - Parameter 1
 * @param p2 - Parameter 2
 * @param data - Optional data buffer
 * @param statusList - List of acceptable status codes
 * @param options - Send options
 * @returns Promise resolving to response data
 */
send(
  cla: number,
  ins: number, 
  p1: number,
  p2: number,
  data?: Buffer,
  statusList?: number[],
  options?: SendOptions
): Promise<Buffer>;

/**
 * Set timeout for APDU exchanges
 * @param exchangeTimeout - Timeout in milliseconds
 */
setExchangeTimeout(exchangeTimeout: number): void;

/**
 * Set timeout for detecting unresponsive devices
 * @param exchangeUnresponsiveTimeout - Timeout in milliseconds
 */
setExchangeUnresponsiveTimeout(exchangeUnresponsiveTimeout: number): void;

Event Handling

Event system for monitoring transport state and device connectivity.

/**
 * Register event listener
 * @param eventName - Event name ("disconnect", "unresponsive", "responsive")
 * @param callback - Event callback function
 */
on(eventName: string, callback: (...args: any[]) => void): void;

/**
 * Remove event listener
 * @param eventName - Event name
 * @param callback - Event callback function to remove
 */
off(eventName: string, callback: (...args: any[]) => void): void;

/**
 * Emit event to all registered listeners
 * @param eventName - Event name
 * @param args - Event arguments
 */
emit(eventName: string, ...args: any[]): void;

Types

Core Interfaces

/**
 * Observer pattern interface for device events
 */
interface Observer<T> {
  next: (event: T) => unknown;
  error: (e: unknown) => unknown;
  complete: () => unknown;
}

/**
 * Subscription interface for managing event listeners
 */
interface Subscription {
  unsubscribe: () => void;
}

/**
 * Device descriptor event for device add/remove notifications
 */
interface DescriptorEvent<T> {
  type: "add" | "remove";
  descriptor: T;
  deviceModel?: DeviceModel | null | undefined;
}

/**
 * Options for send method
 */
interface SendOptions {
  /** Timeout for the operation in milliseconds */
  abortTimeoutMs?: number;
}

Device Model Information

/**
 * Device model enumeration
 */
enum DeviceModelId {
  blue = "blue",
  nanoS = "nanoS",
  nanoSP = "nanoSP", 
  nanoX = "nanoX",
  stax = "stax",
  europa = "europa",
  apex = "apex"
}

/**
 * Device model information interface
 */
interface DeviceModel {
  id: DeviceModelId;
  productName: string;
  productIdMM: number;
  legacyUsbProductId: number;
  usbOnly: boolean;
  memorySize: number;
  masks: number[];
  getBlockSize: (firmwareVersion: string) => number;
  bluetoothSpec?: Array<{
    serviceUuid: string;
    writeUuid: string;
    writeCmdUuid: string;
    notifyUuid: string;
  }>;
}

Error Types

/**
 * Base transport error class
 */
class TransportError extends Error {
  id: string;
}

/**
 * APDU status code error
 */
class TransportStatusError extends Error {
  statusCode: number;
  statusText: string;
}

/**
 * User cancelled device selection
 */
class TransportOpenUserCancelled extends Error {}

/**
 * WebUSB interface not available or not supported
 */
class TransportInterfaceNotAvailable extends Error {}

/**
 * WebUSB operation requires user gesture (click)
 */
class TransportWebUSBGestureRequired extends Error {}

/**
 * Device disconnected during operation
 */
class DisconnectedDeviceDuringOperation extends Error {}

/**
 * Device disconnected
 */
class DisconnectedDevice extends Error {}

/**
 * Transport race condition detected
 */
class TransportRaceCondition extends Error {}

Constants

/**
 * Ledger USB vendor ID
 */
const ledgerUSBVendorId = 0x2c97;

/**
 * APDU status codes
 */
const StatusCodes = {
  ACCESS_CONDITION_NOT_FULFILLED: 0x9804,
  ALGORITHM_NOT_SUPPORTED: 0x9484,
  CLA_NOT_SUPPORTED: 0x6e00,
  CODE_BLOCKED: 0x9840,
  CODE_NOT_INITIALIZED: 0x9802,
  COMMAND_INCOMPATIBLE_FILE_STRUCTURE: 0x6981,
  CONDITIONS_OF_USE_NOT_SATISFIED: 0x6985,
  CONTRADICTION_INVALIDATION: 0x9810,
  CONTRADICTION_SECRET_CODE_STATUS: 0x9808,
  DEVICE_IN_RECOVERY_MODE: 0x662f,
  CUSTOM_IMAGE_EMPTY: 0x662e,
  FILE_ALREADY_EXISTS: 0x6a89,
  FILE_NOT_FOUND: 0x9404,
  GP_AUTH_FAILED: 0x6300,
  HALTED: 0x6faa,
  INCONSISTENT_FILE: 0x9408,
  INCORRECT_DATA: 0x6a80,
  INCORRECT_LENGTH: 0x6700,
  INCORRECT_P1_P2: 0x6b00,
  INS_NOT_SUPPORTED: 0x6d00,
  DEVICE_NOT_ONBOARDED: 0x6d07,
  DEVICE_NOT_ONBOARDED_2: 0x6611,
  INVALID_KCV: 0x9485,
  INVALID_OFFSET: 0x9402,
  LICENSING: 0x6f42,
  LOCKED_DEVICE: 0x5515,
  MAX_VALUE_REACHED: 0x9850,
  MEMORY_PROBLEM: 0x9240,
  MISSING_CRITICAL_PARAMETER: 0x6800,
  NO_EF_SELECTED: 0x9400,
  NOT_ENOUGH_MEMORY_SPACE: 0x6a84,
  OK: 0x9000,
  PIN_REMAINING_ATTEMPTS: 0x63c0,
  REFERENCED_DATA_NOT_FOUND: 0x6a88,
  SECURITY_STATUS_NOT_SATISFIED: 0x6982,
  TECHNICAL_PROBLEM: 0x6f00,
  UNKNOWN_APDU: 0x6d02,
  USER_REFUSED_ON_DEVICE: 0x5501,
  NOT_ENOUGH_SPACE: 0x5102,
  APP_NOT_FOUND_OR_INVALID_CONTEXT: 0x5123,
  INVALID_APP_NAME_LENGTH: 0x670a,
  GEN_AES_KEY_FAILED: 0x5419,
  INTERNAL_CRYPTO_OPERATION_FAILED: 0x541a,
  INTERNAL_COMPUTE_AES_CMAC_FAILED: 0x541b,
  ENCRYPT_APP_STORAGE_FAILED: 0x541c,
  INVALID_BACKUP_STATE: 0x6642,
  PIN_NOT_SET: 0x5502,
  INVALID_BACKUP_LENGTH: 0x6733,
  INVALID_RESTORE_STATE: 0x6643,
  INVALID_CHUNK_LENGTH: 0x6734,
  INVALID_BACKUP_HEADER: 0x684a,
  TRUSTCHAIN_WRONG_SEED: 0xb007
};

/**
 * Get alternative status message for status code
 * @param code - Status code number
 * @returns Human-readable status message or undefined
 */
function getAltStatusMessage(code: number): string | undefined | null {
  switch (code) {
    case 0x6700:
      return "Incorrect length";
    case 0x6800:
      return "Missing critical parameter";
    case 0x6982:
      return "Security not satisfied (dongle locked or have invalid access rights)";
    case 0x6985:
      return "Condition of use not satisfied (denied by the user?)";
    case 0x6a80:
      return "Invalid data received";
    case 0x6b00:
      return "Invalid parameter received";
    case 0x5515:
      return "Locked device";
    default:
      if (0x6f00 <= code && code <= 0x6fff) {
        return "Internal error, please report";
      }
      return undefined;
  }
}

Usage Examples

Device Permission and Connection

import TransportWebUSB from "@ledgerhq/hw-transport-webusb";

// Check browser support
if (await TransportWebUSB.isSupported()) {
  try {
    // Request device permission (always shows dialog)
    const transport = await TransportWebUSB.request();
    
    // Or connect silently if already authorized
    const transport2 = await TransportWebUSB.openConnected();
    
    if (transport2) {
      console.log("Connected to:", transport2.deviceModel?.productName);
    }
  } catch (error) {
    if (error instanceof TransportOpenUserCancelled) {
      console.log("User cancelled device selection");
    }
  }
}

Device Listening

import TransportWebUSB from "@ledgerhq/hw-transport-webusb";

// Listen for device events (must be called on user interaction)
const subscription = TransportWebUSB.listen({
  next: (event) => {
    if (event.type === "add") {
      console.log("Device connected:", event.deviceModel?.productName);
      // Open transport with the detected device
      TransportWebUSB.open(event.descriptor).then(transport => {
        // Use transport...
      });
    }
  },
  error: (error) => {
    console.error("Device listening error:", error);
  },
  complete: () => {
    console.log("Device listening completed");
  }
});

// Later, stop listening
subscription.unsubscribe();

APDU Exchange

import TransportWebUSB from "@ledgerhq/hw-transport-webusb";

const transport = await TransportWebUSB.create();

try {
  // Low-level APDU exchange
  const getAppNameAPDU = Buffer.from("E0C4000000", "hex");
  const response = await transport.exchange(getAppNameAPDU);
  
  // High-level send method with status handling
  const appInfo = await transport.send(0xE0, 0xC4, 0x00, 0x00);
  
  console.log("App info:", appInfo.toString("hex"));
} finally {
  await transport.close();
}

Error Handling

import TransportWebUSB, { 
  TransportWebUSBGestureRequired,
  TransportInterfaceNotAvailable,
  DisconnectedDeviceDuringOperation 
} from "@ledgerhq/hw-transport-webusb";

try {
  const transport = await TransportWebUSB.create();
  
  // Listen for disconnect events
  transport.on("disconnect", (error) => {
    console.log("Device disconnected:", error.message);
  });
  
  const response = await transport.exchange(apduBuffer);
} catch (error) {
  if (error instanceof TransportWebUSBGestureRequired) {
    console.log("Please trigger this action from a user click");
  } else if (error instanceof TransportInterfaceNotAvailable) {
    console.log("Device interface not available. Please upgrade firmware.");
  } else if (error instanceof DisconnectedDeviceDuringOperation) {
    console.log("Device was disconnected during operation");
  }
}