CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-ledgerhq--hw-transport-node-hid

Node.js HID transport implementation for Ledger Hardware Wallets with device event listening capabilities

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

index.mddocs/

Ledger Hardware Wallet Node HID Transport

@ledgerhq/hw-transport-node-hid provides a Node.js HID transport implementation for Ledger Hardware Wallets with device event listening capabilities. It extends the base TransportNodeHidNoEvents class to add real-time device monitoring and event handling, enabling applications to detect when Ledger devices are plugged in or removed.

Package Information

  • Package Name: @ledgerhq/hw-transport-node-hid
  • Package Type: npm
  • Language: TypeScript
  • Installation: npm install @ledgerhq/hw-transport-node-hid

Core Imports

import TransportNodeHid from "@ledgerhq/hw-transport-node-hid";

For CommonJS:

const TransportNodeHid = require("@ledgerhq/hw-transport-node-hid").default;

Basic Usage

import TransportNodeHid from "@ledgerhq/hw-transport-node-hid";

// Create a transport instance to the first available device
const transport = await TransportNodeHid.create();

// Send an APDU command
const response = await transport.exchange(apduBuffer);

// Close the transport when done
await transport.close();

Architecture

The package is built around these key components:

  • TransportNodeHid Class: Main transport class extending TransportNodeHidNoEvents with event listening
  • Device Discovery: Real-time USB device monitoring with debounced polling
  • HID Communication: Low-level HID protocol handling through node-hid library
  • Event System: Observer pattern for device add/remove notifications
  • APDU Protocol: High-level and low-level APIs for Ledger device communication

Capabilities

Transport Creation and Management

Core functionality for creating and managing transport connections to Ledger devices.

class TransportNodeHid {
  /**
   * Check if HID transport is supported on current platform
   * @returns Promise resolving to boolean indicating support
   */
  static isSupported(): Promise<boolean>;

  /**
   * List all available Ledger device paths
   * @returns Promise resolving to array of device paths
   */
  static list(): Promise<string[]>;

  /**
   * Create transport to first available device with timeouts
   * @param openTimeout - Optional timeout in ms for opening device (default: 3000)
   * @param listenTimeout - Optional timeout in ms for device discovery
   * @returns Promise resolving to TransportNodeHid instance
   */
  static create(openTimeout?: number, listenTimeout?: number): Promise<TransportNodeHid>;

  /**
   * Open connection to specific device or first available device
   * @param path - Device path string, null, or undefined (auto-selects first device if falsy)
   * @returns Promise resolving to TransportNodeHid instance
   */
  static open(path: string | null | undefined): Promise<TransportNodeHid>;

  /**
   * Close connection to device and release resources
   * @returns Promise resolving when closed
   */
  close(): Promise<void>;
}

Device Event Listening

Real-time monitoring of Ledger device connections with automatic discovery and removal detection.

/**
 * Listen for device add/remove events with real-time monitoring
 * @param observer - Observer object with next, error, complete methods
 * @returns Subscription object with unsubscribe method
 */
static listen(observer: Observer<DescriptorEvent<string | null | undefined>>): Subscription;

/**
 * Configure debounce delay for device polling
 * @param delay - Debounce delay in milliseconds
 */
static setListenDevicesDebounce(delay: number): void;

/**
 * Set condition function to skip device polling
 * @param conditionToSkip - Function returning boolean to determine when to skip polling
 */
static setListenDevicesPollingSkip(conditionToSkip: () => boolean): void;

/**
 * Deprecated debug method (logs deprecation warning)
 * @deprecated Use @ledgerhq/logs instead
 */
static setListenDevicesDebug(): void;

Usage Example:

import TransportNodeHid from "@ledgerhq/hw-transport-node-hid";

// Listen for device events
const subscription = TransportNodeHid.listen({
  next: (event) => {
    if (event.type === "add") {
      console.log("Device connected:", event.descriptor);
      console.log("Device model:", event.deviceModel);
    } else if (event.type === "remove") {
      console.log("Device disconnected:", event.descriptor);
    }
  },
  error: (err) => console.error("Device listening error:", err),
  complete: () => console.log("Device listening completed")
});

// Stop listening
subscription.unsubscribe();

// Configure polling behavior
TransportNodeHid.setListenDevicesDebounce(1000); // 1 second debounce
TransportNodeHid.setListenDevicesPollingSkip(() => someCondition);

APDU Communication

Low-level and high-level APIs for communicating with Ledger devices using the APDU protocol.

/**
 * Send APDU command to device and receive response
 * @param apdu - Buffer containing APDU command
 * @param options - Optional object with abortTimeoutMs property
 * @returns Promise resolving to response Buffer
 */
exchange(apdu: Buffer, options?: { abortTimeoutMs?: number }): Promise<Buffer>;

/**
 * High-level API to send structured commands to device
 * @param cla - Instruction class
 * @param ins - Instruction code
 * @param p1 - First parameter
 * @param p2 - Second parameter
 * @param data - Optional data buffer (default: empty buffer)
 * @param statusList - Optional acceptable status codes (default: [StatusCodes.OK])
 * @param options - Optional object with abortTimeoutMs property
 * @returns Promise resolving to response Buffer
 */
send(
  cla: number,
  ins: number,
  p1: number,
  p2: number,
  data?: Buffer,
  statusList?: number[],
  options?: { abortTimeoutMs?: number }
): Promise<Buffer>;

/**
 * Send multiple APDUs in sequence
 * @param apdus - Array of APDU buffers
 * @param observer - Observer to receive individual responses
 * @returns Subscription object with unsubscribe method
 */
exchangeBulk(apdus: Buffer[], observer: Observer<Buffer>): Subscription;

Usage Example:

import TransportNodeHid from "@ledgerhq/hw-transport-node-hid";
import { StatusCodes } from "@ledgerhq/errors";

const transport = await TransportNodeHid.create();

// Low-level APDU exchange
const apduBuffer = Buffer.from([0xe0, 0x01, 0x00, 0x00]);
const response = await transport.exchange(apduBuffer);

// High-level structured command
const response2 = await transport.send(
  0xe0, // CLA
  0x01, // INS
  0x00, // P1
  0x00, // P2
  Buffer.from("data"), // Data
  [StatusCodes.OK, 0x6985] // Acceptable status codes
);

// Bulk APDU operations
const apdus = [
  Buffer.from([0xe0, 0x01, 0x00, 0x00]),
  Buffer.from([0xe0, 0x02, 0x00, 0x00])
];

const subscription = transport.exchangeBulk(apdus, {
  next: (response) => console.log("Response:", response),
  error: (err) => console.error("Error:", err),
  complete: () => console.log("All APDUs completed")
});

Transport Configuration and Events

Configuration options and event handling for transport instances.

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

/**
 * Set timeout before emitting unresponsive event
 * @param unresponsiveTimeout - Timeout in milliseconds
 */
setExchangeUnresponsiveTimeout(unresponsiveTimeout: number): void;

/**
 * Add event listener for transport events
 * @param eventName - Event name (e.g., "disconnect", "unresponsive", "responsive")
 * @param cb - Callback function
 */
on(eventName: string, cb: (...args: any[]) => any): void;

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

/**
 * Set scramble key for data exchanges (deprecated)
 * @param key - Optional scramble key
 * @deprecated This method is no longer needed for modern transports
 */
setScrambleKey(key?: string): void;

/**
 * Deprecated debug method (logs deprecation warning)
 * @deprecated Use @ledgerhq/logs instead
 */
setDebugMode(): void;

Usage Example:

const transport = await TransportNodeHid.create();

// Configure timeouts
transport.setExchangeTimeout(60000); // 60 seconds
transport.setExchangeUnresponsiveTimeout(30000); // 30 seconds

// Listen for events
transport.on("disconnect", () => {
  console.log("Device disconnected");
});

transport.on("unresponsive", () => {
  console.log("Device is not responding");
});

transport.on("responsive", () => {
  console.log("Device is responsive again");
});

Tracing and Debugging

Logging and tracing functionality for debugging transport operations.

/**
 * Set tracing context for logging
 * @param context - Optional TraceContext object
 */
setTraceContext(context?: TraceContext): void;

/**
 * Update existing tracing context
 * @param contextToAdd - TraceContext to merge with current context
 */
updateTraceContext(contextToAdd: TraceContext): void;

/**
 * Get current tracing context
 * @returns Current TraceContext or undefined
 */
getTraceContext(): TraceContext | undefined;

Types

/**
 * Observer pattern interface for handling events
 */
interface Observer<EventType, EventError = unknown> {
  next: (event: EventType) => unknown;
  error: (e: EventError) => unknown;
  complete: () => unknown;
}

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

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

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

/**
 * Generic device object type
 */
type Device = any;

/**
 * Tracing context for logging operations
 */
type TraceContext = Record<string, any>;

/**
 * Log type for tracing operations
 */
type LogType = string;

Error Handling

The package uses error types from @ledgerhq/errors:

/**
 * General transport errors
 */
class TransportError extends Error {
  constructor(message: string, id: string);
}

/**
 * APDU status code errors
 */
class TransportStatusError extends TransportError {
  constructor(statusCode: number);
  statusCode: number;
}

/**
 * Concurrent operation errors
 */
class TransportRaceCondition extends TransportError {
  constructor(message: string);
}

/**
 * Device disconnection errors
 */
class DisconnectedDevice extends TransportError {
  constructor(message?: string);
}

/**
 * Errors during active operations
 */
class DisconnectedDeviceDuringOperation extends TransportError {
  constructor(message: string);
}

/**
 * Status code constants
 */
const StatusCodes: {
  OK: 0x9000;
  // Additional status codes...
};

Common Error Scenarios:

import TransportNodeHid from "@ledgerhq/hw-transport-node-hid";
import { TransportError, TransportStatusError } from "@ledgerhq/errors";

try {
  const transport = await TransportNodeHid.create();
  const response = await transport.exchange(apduBuffer);
} catch (error) {
  if (error instanceof TransportError) {
    if (error.id === "NoDevice") {
      console.error("No Ledger device found");
    } else if (error.id === "ListenTimeout") {
      console.error("Device discovery timed out");
    }
  } else if (error instanceof TransportStatusError) {
    console.error("Device returned error status:", error.statusCode.toString(16));
  }
}

Platform Support

  • Supported Platforms: Node.js environments (Windows, macOS, Linux)
  • Requirements: Node.js with node-hid and usb native modules
  • Device Compatibility: Ledger Nano S, Nano X, Nano S Plus, Blue, and other Ledger hardware wallets
  • Limitations: Desktop/server environments only (not browser-compatible)

Installation Requirements

The package requires native dependencies:

npm install @ledgerhq/hw-transport-node-hid

# On Linux, you may need additional system dependencies:
# sudo apt-get install libudev-dev libusb-1.0-0-dev

# On Windows, you may need build tools:
# npm install --global windows-build-tools

docs

index.md

tile.json