Node.js HID transport implementation for Ledger Hardware Wallets with device event listening capabilities
npx @tessl/cli install tessl/npm-ledgerhq--hw-transport-node-hid@6.29.0@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.
npm install @ledgerhq/hw-transport-node-hidimport TransportNodeHid from "@ledgerhq/hw-transport-node-hid";For CommonJS:
const TransportNodeHid = require("@ledgerhq/hw-transport-node-hid").default;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();The package is built around these key components:
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>;
}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);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")
});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");
});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;/**
* 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;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));
}
}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