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.00
# Ledger Hardware Wallet Node HID Transport
1
2
@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.
3
4
## Package Information
5
6
- **Package Name**: @ledgerhq/hw-transport-node-hid
7
- **Package Type**: npm
8
- **Language**: TypeScript
9
- **Installation**: `npm install @ledgerhq/hw-transport-node-hid`
10
11
## Core Imports
12
13
```typescript
14
import TransportNodeHid from "@ledgerhq/hw-transport-node-hid";
15
```
16
17
For CommonJS:
18
19
```javascript
20
const TransportNodeHid = require("@ledgerhq/hw-transport-node-hid").default;
21
```
22
23
## Basic Usage
24
25
```typescript
26
import TransportNodeHid from "@ledgerhq/hw-transport-node-hid";
27
28
// Create a transport instance to the first available device
29
const transport = await TransportNodeHid.create();
30
31
// Send an APDU command
32
const response = await transport.exchange(apduBuffer);
33
34
// Close the transport when done
35
await transport.close();
36
```
37
38
## Architecture
39
40
The package is built around these key components:
41
42
- **TransportNodeHid Class**: Main transport class extending TransportNodeHidNoEvents with event listening
43
- **Device Discovery**: Real-time USB device monitoring with debounced polling
44
- **HID Communication**: Low-level HID protocol handling through node-hid library
45
- **Event System**: Observer pattern for device add/remove notifications
46
- **APDU Protocol**: High-level and low-level APIs for Ledger device communication
47
48
## Capabilities
49
50
### Transport Creation and Management
51
52
Core functionality for creating and managing transport connections to Ledger devices.
53
54
```typescript { .api }
55
class TransportNodeHid {
56
/**
57
* Check if HID transport is supported on current platform
58
* @returns Promise resolving to boolean indicating support
59
*/
60
static isSupported(): Promise<boolean>;
61
62
/**
63
* List all available Ledger device paths
64
* @returns Promise resolving to array of device paths
65
*/
66
static list(): Promise<string[]>;
67
68
/**
69
* Create transport to first available device with timeouts
70
* @param openTimeout - Optional timeout in ms for opening device (default: 3000)
71
* @param listenTimeout - Optional timeout in ms for device discovery
72
* @returns Promise resolving to TransportNodeHid instance
73
*/
74
static create(openTimeout?: number, listenTimeout?: number): Promise<TransportNodeHid>;
75
76
/**
77
* Open connection to specific device or first available device
78
* @param path - Device path string, null, or undefined (auto-selects first device if falsy)
79
* @returns Promise resolving to TransportNodeHid instance
80
*/
81
static open(path: string | null | undefined): Promise<TransportNodeHid>;
82
83
/**
84
* Close connection to device and release resources
85
* @returns Promise resolving when closed
86
*/
87
close(): Promise<void>;
88
}
89
```
90
91
### Device Event Listening
92
93
Real-time monitoring of Ledger device connections with automatic discovery and removal detection.
94
95
```typescript { .api }
96
/**
97
* Listen for device add/remove events with real-time monitoring
98
* @param observer - Observer object with next, error, complete methods
99
* @returns Subscription object with unsubscribe method
100
*/
101
static listen(observer: Observer<DescriptorEvent<string | null | undefined>>): Subscription;
102
103
/**
104
* Configure debounce delay for device polling
105
* @param delay - Debounce delay in milliseconds
106
*/
107
static setListenDevicesDebounce(delay: number): void;
108
109
/**
110
* Set condition function to skip device polling
111
* @param conditionToSkip - Function returning boolean to determine when to skip polling
112
*/
113
static setListenDevicesPollingSkip(conditionToSkip: () => boolean): void;
114
115
/**
116
* Deprecated debug method (logs deprecation warning)
117
* @deprecated Use @ledgerhq/logs instead
118
*/
119
static setListenDevicesDebug(): void;
120
```
121
122
**Usage Example:**
123
124
```typescript
125
import TransportNodeHid from "@ledgerhq/hw-transport-node-hid";
126
127
// Listen for device events
128
const subscription = TransportNodeHid.listen({
129
next: (event) => {
130
if (event.type === "add") {
131
console.log("Device connected:", event.descriptor);
132
console.log("Device model:", event.deviceModel);
133
} else if (event.type === "remove") {
134
console.log("Device disconnected:", event.descriptor);
135
}
136
},
137
error: (err) => console.error("Device listening error:", err),
138
complete: () => console.log("Device listening completed")
139
});
140
141
// Stop listening
142
subscription.unsubscribe();
143
144
// Configure polling behavior
145
TransportNodeHid.setListenDevicesDebounce(1000); // 1 second debounce
146
TransportNodeHid.setListenDevicesPollingSkip(() => someCondition);
147
```
148
149
### APDU Communication
150
151
Low-level and high-level APIs for communicating with Ledger devices using the APDU protocol.
152
153
```typescript { .api }
154
/**
155
* Send APDU command to device and receive response
156
* @param apdu - Buffer containing APDU command
157
* @param options - Optional object with abortTimeoutMs property
158
* @returns Promise resolving to response Buffer
159
*/
160
exchange(apdu: Buffer, options?: { abortTimeoutMs?: number }): Promise<Buffer>;
161
162
/**
163
* High-level API to send structured commands to device
164
* @param cla - Instruction class
165
* @param ins - Instruction code
166
* @param p1 - First parameter
167
* @param p2 - Second parameter
168
* @param data - Optional data buffer (default: empty buffer)
169
* @param statusList - Optional acceptable status codes (default: [StatusCodes.OK])
170
* @param options - Optional object with abortTimeoutMs property
171
* @returns Promise resolving to response Buffer
172
*/
173
send(
174
cla: number,
175
ins: number,
176
p1: number,
177
p2: number,
178
data?: Buffer,
179
statusList?: number[],
180
options?: { abortTimeoutMs?: number }
181
): Promise<Buffer>;
182
183
/**
184
* Send multiple APDUs in sequence
185
* @param apdus - Array of APDU buffers
186
* @param observer - Observer to receive individual responses
187
* @returns Subscription object with unsubscribe method
188
*/
189
exchangeBulk(apdus: Buffer[], observer: Observer<Buffer>): Subscription;
190
```
191
192
**Usage Example:**
193
194
```typescript
195
import TransportNodeHid from "@ledgerhq/hw-transport-node-hid";
196
import { StatusCodes } from "@ledgerhq/errors";
197
198
const transport = await TransportNodeHid.create();
199
200
// Low-level APDU exchange
201
const apduBuffer = Buffer.from([0xe0, 0x01, 0x00, 0x00]);
202
const response = await transport.exchange(apduBuffer);
203
204
// High-level structured command
205
const response2 = await transport.send(
206
0xe0, // CLA
207
0x01, // INS
208
0x00, // P1
209
0x00, // P2
210
Buffer.from("data"), // Data
211
[StatusCodes.OK, 0x6985] // Acceptable status codes
212
);
213
214
// Bulk APDU operations
215
const apdus = [
216
Buffer.from([0xe0, 0x01, 0x00, 0x00]),
217
Buffer.from([0xe0, 0x02, 0x00, 0x00])
218
];
219
220
const subscription = transport.exchangeBulk(apdus, {
221
next: (response) => console.log("Response:", response),
222
error: (err) => console.error("Error:", err),
223
complete: () => console.log("All APDUs completed")
224
});
225
```
226
227
### Transport Configuration and Events
228
229
Configuration options and event handling for transport instances.
230
231
```typescript { .api }
232
/**
233
* Set timeout for exchange operations
234
* @param exchangeTimeout - Timeout in milliseconds
235
*/
236
setExchangeTimeout(exchangeTimeout: number): void;
237
238
/**
239
* Set timeout before emitting unresponsive event
240
* @param unresponsiveTimeout - Timeout in milliseconds
241
*/
242
setExchangeUnresponsiveTimeout(unresponsiveTimeout: number): void;
243
244
/**
245
* Add event listener for transport events
246
* @param eventName - Event name (e.g., "disconnect", "unresponsive", "responsive")
247
* @param cb - Callback function
248
*/
249
on(eventName: string, cb: (...args: any[]) => any): void;
250
251
/**
252
* Remove event listener
253
* @param eventName - Event name
254
* @param cb - Callback function to remove
255
*/
256
off(eventName: string, cb: (...args: any[]) => any): void;
257
258
/**
259
* Set scramble key for data exchanges (deprecated)
260
* @param key - Optional scramble key
261
* @deprecated This method is no longer needed for modern transports
262
*/
263
setScrambleKey(key?: string): void;
264
265
/**
266
* Deprecated debug method (logs deprecation warning)
267
* @deprecated Use @ledgerhq/logs instead
268
*/
269
setDebugMode(): void;
270
```
271
272
**Usage Example:**
273
274
```typescript
275
const transport = await TransportNodeHid.create();
276
277
// Configure timeouts
278
transport.setExchangeTimeout(60000); // 60 seconds
279
transport.setExchangeUnresponsiveTimeout(30000); // 30 seconds
280
281
// Listen for events
282
transport.on("disconnect", () => {
283
console.log("Device disconnected");
284
});
285
286
transport.on("unresponsive", () => {
287
console.log("Device is not responding");
288
});
289
290
transport.on("responsive", () => {
291
console.log("Device is responsive again");
292
});
293
```
294
295
### Tracing and Debugging
296
297
Logging and tracing functionality for debugging transport operations.
298
299
```typescript { .api }
300
/**
301
* Set tracing context for logging
302
* @param context - Optional TraceContext object
303
*/
304
setTraceContext(context?: TraceContext): void;
305
306
/**
307
* Update existing tracing context
308
* @param contextToAdd - TraceContext to merge with current context
309
*/
310
updateTraceContext(contextToAdd: TraceContext): void;
311
312
/**
313
* Get current tracing context
314
* @returns Current TraceContext or undefined
315
*/
316
getTraceContext(): TraceContext | undefined;
317
```
318
319
## Types
320
321
```typescript { .api }
322
/**
323
* Observer pattern interface for handling events
324
*/
325
interface Observer<EventType, EventError = unknown> {
326
next: (event: EventType) => unknown;
327
error: (e: EventError) => unknown;
328
complete: () => unknown;
329
}
330
331
/**
332
* Subscription interface for cancelling event listeners
333
*/
334
interface Subscription {
335
unsubscribe: () => void;
336
}
337
338
/**
339
* Device event descriptor for add/remove notifications
340
*/
341
interface DescriptorEvent<Descriptor> {
342
type: "add" | "remove";
343
descriptor: Descriptor;
344
deviceModel?: DeviceModel | null | undefined;
345
device?: Device;
346
}
347
348
/**
349
* Device model information
350
*/
351
interface DeviceModel {
352
id: string;
353
productName: string;
354
productIdMM: number;
355
legacyUsbProductId: number;
356
usbOnly: boolean;
357
memorySize: number;
358
masks: number[];
359
getBlockSize: (firmwareVersion: string) => number;
360
bluetoothSpec?: {
361
serviceUuid: string;
362
writeUuid: string;
363
writeCmdUuid: string;
364
notifyUuid: string;
365
}[];
366
}
367
368
/**
369
* Generic device object type
370
*/
371
type Device = any;
372
373
/**
374
* Tracing context for logging operations
375
*/
376
type TraceContext = Record<string, any>;
377
378
/**
379
* Log type for tracing operations
380
*/
381
type LogType = string;
382
```
383
384
## Error Handling
385
386
The package uses error types from @ledgerhq/errors:
387
388
```typescript { .api }
389
/**
390
* General transport errors
391
*/
392
class TransportError extends Error {
393
constructor(message: string, id: string);
394
}
395
396
/**
397
* APDU status code errors
398
*/
399
class TransportStatusError extends TransportError {
400
constructor(statusCode: number);
401
statusCode: number;
402
}
403
404
/**
405
* Concurrent operation errors
406
*/
407
class TransportRaceCondition extends TransportError {
408
constructor(message: string);
409
}
410
411
/**
412
* Device disconnection errors
413
*/
414
class DisconnectedDevice extends TransportError {
415
constructor(message?: string);
416
}
417
418
/**
419
* Errors during active operations
420
*/
421
class DisconnectedDeviceDuringOperation extends TransportError {
422
constructor(message: string);
423
}
424
425
/**
426
* Status code constants
427
*/
428
const StatusCodes: {
429
OK: 0x9000;
430
// Additional status codes...
431
};
432
```
433
434
**Common Error Scenarios:**
435
436
```typescript
437
import TransportNodeHid from "@ledgerhq/hw-transport-node-hid";
438
import { TransportError, TransportStatusError } from "@ledgerhq/errors";
439
440
try {
441
const transport = await TransportNodeHid.create();
442
const response = await transport.exchange(apduBuffer);
443
} catch (error) {
444
if (error instanceof TransportError) {
445
if (error.id === "NoDevice") {
446
console.error("No Ledger device found");
447
} else if (error.id === "ListenTimeout") {
448
console.error("Device discovery timed out");
449
}
450
} else if (error instanceof TransportStatusError) {
451
console.error("Device returned error status:", error.statusCode.toString(16));
452
}
453
}
454
```
455
456
## Platform Support
457
458
- **Supported Platforms**: Node.js environments (Windows, macOS, Linux)
459
- **Requirements**: Node.js with node-hid and usb native modules
460
- **Device Compatibility**: Ledger Nano S, Nano X, Nano S Plus, Blue, and other Ledger hardware wallets
461
- **Limitations**: Desktop/server environments only (not browser-compatible)
462
463
## Installation Requirements
464
465
The package requires native dependencies:
466
467
```bash
468
npm install @ledgerhq/hw-transport-node-hid
469
470
# On Linux, you may need additional system dependencies:
471
# sudo apt-get install libudev-dev libusb-1.0-0-dev
472
473
# On Windows, you may need build tools:
474
# npm install --global windows-build-tools
475
```