Ledger Hardware Wallet WebUSB implementation of the communication layer for web browsers
npx @tessl/cli install tessl/npm-ledgerhq--hw-transport-webusb@6.29.00
# Ledger WebUSB Transport
1
2
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.
3
4
## Package Information
5
6
- **Package Name**: @ledgerhq/hw-transport-webusb
7
- **Package Type**: npm
8
- **Language**: TypeScript
9
- **Installation**: `npm install @ledgerhq/hw-transport-webusb`
10
- **Browser Requirements**: WebUSB support (Chrome/Chromium-based browsers), HTTPS required
11
12
## Core Imports
13
14
```typescript
15
import TransportWebUSB from "@ledgerhq/hw-transport-webusb";
16
```
17
18
For CommonJS:
19
20
```javascript
21
const TransportWebUSB = require("@ledgerhq/hw-transport-webusb").default;
22
```
23
24
Named imports for error classes:
25
26
```typescript
27
import TransportWebUSB, {
28
TransportOpenUserCancelled,
29
TransportInterfaceNotAvailable,
30
TransportWebUSBGestureRequired,
31
DisconnectedDeviceDuringOperation,
32
DisconnectedDevice,
33
TransportError,
34
TransportStatusError,
35
StatusCodes,
36
getAltStatusMessage
37
} from "@ledgerhq/hw-transport-webusb";
38
```
39
40
## Basic Usage
41
42
```typescript
43
import TransportWebUSB from "@ledgerhq/hw-transport-webusb";
44
45
// Check if WebUSB is supported
46
const isSupported = await TransportWebUSB.isSupported();
47
48
if (isSupported) {
49
// Create a transport connection (shows permission dialog if needed)
50
const transport = await TransportWebUSB.create();
51
52
// Exchange APDU with the device
53
const apdu = Buffer.from("E0C4000000", "hex"); // Get app name APDU
54
const response = await transport.exchange(apdu);
55
56
// Close the connection
57
await transport.close();
58
}
59
```
60
61
## Architecture
62
63
The Ledger WebUSB Transport is built around several key components:
64
65
- **TransportWebUSB Class**: Main transport implementation extending the base Transport class
66
- **Device Management**: Static methods for device discovery, permission handling, and connection establishment
67
- **APDU Protocol**: Low-level APDU exchange with HID framing for USB communication
68
- **Event System**: Disconnect detection and transport lifecycle events
69
- **Error Handling**: Comprehensive error types for various failure scenarios
70
71
## Capabilities
72
73
### Transport Creation and Management
74
75
Core functionality for establishing and managing WebUSB connections to Ledger devices.
76
77
```typescript { .api }
78
/**
79
* Main WebUSB transport class for Ledger devices
80
*/
81
export default class TransportWebUSB extends Transport {
82
constructor(device: USBDevice, interfaceNumber: number);
83
84
/** The connected USB device */
85
device: USBDevice;
86
87
/** Identified device model information */
88
deviceModel: DeviceModel | null | undefined;
89
90
/** Communication channel identifier */
91
channel: number;
92
93
/** USB packet size in bytes */
94
packetSize: number;
95
96
/** USB interface number */
97
interfaceNumber: number;
98
}
99
```
100
101
### Static Device Management Methods
102
103
Methods for checking support, discovering devices, and establishing connections.
104
105
```typescript { .api }
106
/**
107
* Check if WebUSB transport is supported in the current browser
108
* @returns Promise resolving to boolean indicating support
109
*/
110
static isSupported(): Promise<boolean>;
111
112
/**
113
* List WebUSB devices that were previously authorized by the user
114
* @returns Promise resolving to array of authorized USBDevice objects
115
*/
116
static list(): Promise<USBDevice[]>;
117
118
/**
119
* Actively listen to WebUSB devices and emit ONE device
120
* Important: Must be called in the context of a UI click
121
* @param observer - Observer for device descriptor events
122
* @returns Subscription object with unsubscribe method
123
*/
124
static listen(observer: Observer<DescriptorEvent<USBDevice>>): Subscription;
125
126
/**
127
* Always display device permission dialog, even if devices are already accepted
128
* @returns Promise resolving to new TransportWebUSB instance
129
*/
130
static request(): Promise<TransportWebUSB>;
131
132
/**
133
* Never display device permission dialog, returns null if no device found
134
* @returns Promise resolving to TransportWebUSB instance or null
135
*/
136
static openConnected(): Promise<TransportWebUSB | null>;
137
138
/**
139
* Create a Ledger transport with a specific USBDevice
140
* @param device - The USBDevice to create transport for
141
* @returns Promise resolving to new TransportWebUSB instance
142
*/
143
static open(device: USBDevice): Promise<TransportWebUSB>;
144
145
/**
146
* Create a transport (inherited from base Transport class)
147
* @param openTimeout - Timeout for opening connection
148
* @param listenTimeout - Timeout for listening to devices
149
* @returns Promise resolving to new TransportWebUSB instance
150
*/
151
static create(openTimeout?: number, listenTimeout?: number): Promise<TransportWebUSB>;
152
```
153
154
### APDU Communication
155
156
Low-level APDU exchange functionality for communicating with Ledger device applications.
157
158
```typescript { .api }
159
/**
160
* Exchange APDU with the device using the WebUSB protocol
161
* @param apdu - The APDU buffer to send to the device
162
* @returns Promise resolving to response APDU buffer
163
*/
164
exchange(apdu: Buffer): Promise<Buffer>;
165
166
/**
167
* Release the transport device and close the connection
168
* @returns Promise that resolves when connection is closed
169
*/
170
close(): Promise<void>;
171
172
/**
173
* Legacy method for scramble key (no-op implementation)
174
*/
175
setScrambleKey(): void;
176
```
177
178
### High-Level Transport Methods
179
180
Inherited methods from the base Transport class for higher-level APDU operations.
181
182
```typescript { .api }
183
/**
184
* Send APDU command with automatic status code handling
185
* @param cla - Class byte
186
* @param ins - Instruction byte
187
* @param p1 - Parameter 1
188
* @param p2 - Parameter 2
189
* @param data - Optional data buffer
190
* @param statusList - List of acceptable status codes
191
* @param options - Send options
192
* @returns Promise resolving to response data
193
*/
194
send(
195
cla: number,
196
ins: number,
197
p1: number,
198
p2: number,
199
data?: Buffer,
200
statusList?: number[],
201
options?: SendOptions
202
): Promise<Buffer>;
203
204
/**
205
* Set timeout for APDU exchanges
206
* @param exchangeTimeout - Timeout in milliseconds
207
*/
208
setExchangeTimeout(exchangeTimeout: number): void;
209
210
/**
211
* Set timeout for detecting unresponsive devices
212
* @param exchangeUnresponsiveTimeout - Timeout in milliseconds
213
*/
214
setExchangeUnresponsiveTimeout(exchangeUnresponsiveTimeout: number): void;
215
```
216
217
### Event Handling
218
219
Event system for monitoring transport state and device connectivity.
220
221
```typescript { .api }
222
/**
223
* Register event listener
224
* @param eventName - Event name ("disconnect", "unresponsive", "responsive")
225
* @param callback - Event callback function
226
*/
227
on(eventName: string, callback: (...args: any[]) => void): void;
228
229
/**
230
* Remove event listener
231
* @param eventName - Event name
232
* @param callback - Event callback function to remove
233
*/
234
off(eventName: string, callback: (...args: any[]) => void): void;
235
236
/**
237
* Emit event to all registered listeners
238
* @param eventName - Event name
239
* @param args - Event arguments
240
*/
241
emit(eventName: string, ...args: any[]): void;
242
```
243
244
## Types
245
246
### Core Interfaces
247
248
```typescript { .api }
249
/**
250
* Observer pattern interface for device events
251
*/
252
interface Observer<T> {
253
next: (event: T) => unknown;
254
error: (e: unknown) => unknown;
255
complete: () => unknown;
256
}
257
258
/**
259
* Subscription interface for managing event listeners
260
*/
261
interface Subscription {
262
unsubscribe: () => void;
263
}
264
265
/**
266
* Device descriptor event for device add/remove notifications
267
*/
268
interface DescriptorEvent<T> {
269
type: "add" | "remove";
270
descriptor: T;
271
deviceModel?: DeviceModel | null | undefined;
272
}
273
274
/**
275
* Options for send method
276
*/
277
interface SendOptions {
278
/** Timeout for the operation in milliseconds */
279
abortTimeoutMs?: number;
280
}
281
```
282
283
### Device Model Information
284
285
```typescript { .api }
286
/**
287
* Device model enumeration
288
*/
289
enum DeviceModelId {
290
blue = "blue",
291
nanoS = "nanoS",
292
nanoSP = "nanoSP",
293
nanoX = "nanoX",
294
stax = "stax",
295
europa = "europa",
296
apex = "apex"
297
}
298
299
/**
300
* Device model information interface
301
*/
302
interface DeviceModel {
303
id: DeviceModelId;
304
productName: string;
305
productIdMM: number;
306
legacyUsbProductId: number;
307
usbOnly: boolean;
308
memorySize: number;
309
masks: number[];
310
getBlockSize: (firmwareVersion: string) => number;
311
bluetoothSpec?: Array<{
312
serviceUuid: string;
313
writeUuid: string;
314
writeCmdUuid: string;
315
notifyUuid: string;
316
}>;
317
}
318
```
319
320
### Error Types
321
322
```typescript { .api }
323
/**
324
* Base transport error class
325
*/
326
class TransportError extends Error {
327
id: string;
328
}
329
330
/**
331
* APDU status code error
332
*/
333
class TransportStatusError extends Error {
334
statusCode: number;
335
statusText: string;
336
}
337
338
/**
339
* User cancelled device selection
340
*/
341
class TransportOpenUserCancelled extends Error {}
342
343
/**
344
* WebUSB interface not available or not supported
345
*/
346
class TransportInterfaceNotAvailable extends Error {}
347
348
/**
349
* WebUSB operation requires user gesture (click)
350
*/
351
class TransportWebUSBGestureRequired extends Error {}
352
353
/**
354
* Device disconnected during operation
355
*/
356
class DisconnectedDeviceDuringOperation extends Error {}
357
358
/**
359
* Device disconnected
360
*/
361
class DisconnectedDevice extends Error {}
362
363
/**
364
* Transport race condition detected
365
*/
366
class TransportRaceCondition extends Error {}
367
```
368
369
### Constants
370
371
```typescript { .api }
372
/**
373
* Ledger USB vendor ID
374
*/
375
const ledgerUSBVendorId = 0x2c97;
376
377
/**
378
* APDU status codes
379
*/
380
const StatusCodes = {
381
ACCESS_CONDITION_NOT_FULFILLED: 0x9804,
382
ALGORITHM_NOT_SUPPORTED: 0x9484,
383
CLA_NOT_SUPPORTED: 0x6e00,
384
CODE_BLOCKED: 0x9840,
385
CODE_NOT_INITIALIZED: 0x9802,
386
COMMAND_INCOMPATIBLE_FILE_STRUCTURE: 0x6981,
387
CONDITIONS_OF_USE_NOT_SATISFIED: 0x6985,
388
CONTRADICTION_INVALIDATION: 0x9810,
389
CONTRADICTION_SECRET_CODE_STATUS: 0x9808,
390
DEVICE_IN_RECOVERY_MODE: 0x662f,
391
CUSTOM_IMAGE_EMPTY: 0x662e,
392
FILE_ALREADY_EXISTS: 0x6a89,
393
FILE_NOT_FOUND: 0x9404,
394
GP_AUTH_FAILED: 0x6300,
395
HALTED: 0x6faa,
396
INCONSISTENT_FILE: 0x9408,
397
INCORRECT_DATA: 0x6a80,
398
INCORRECT_LENGTH: 0x6700,
399
INCORRECT_P1_P2: 0x6b00,
400
INS_NOT_SUPPORTED: 0x6d00,
401
DEVICE_NOT_ONBOARDED: 0x6d07,
402
DEVICE_NOT_ONBOARDED_2: 0x6611,
403
INVALID_KCV: 0x9485,
404
INVALID_OFFSET: 0x9402,
405
LICENSING: 0x6f42,
406
LOCKED_DEVICE: 0x5515,
407
MAX_VALUE_REACHED: 0x9850,
408
MEMORY_PROBLEM: 0x9240,
409
MISSING_CRITICAL_PARAMETER: 0x6800,
410
NO_EF_SELECTED: 0x9400,
411
NOT_ENOUGH_MEMORY_SPACE: 0x6a84,
412
OK: 0x9000,
413
PIN_REMAINING_ATTEMPTS: 0x63c0,
414
REFERENCED_DATA_NOT_FOUND: 0x6a88,
415
SECURITY_STATUS_NOT_SATISFIED: 0x6982,
416
TECHNICAL_PROBLEM: 0x6f00,
417
UNKNOWN_APDU: 0x6d02,
418
USER_REFUSED_ON_DEVICE: 0x5501,
419
NOT_ENOUGH_SPACE: 0x5102,
420
APP_NOT_FOUND_OR_INVALID_CONTEXT: 0x5123,
421
INVALID_APP_NAME_LENGTH: 0x670a,
422
GEN_AES_KEY_FAILED: 0x5419,
423
INTERNAL_CRYPTO_OPERATION_FAILED: 0x541a,
424
INTERNAL_COMPUTE_AES_CMAC_FAILED: 0x541b,
425
ENCRYPT_APP_STORAGE_FAILED: 0x541c,
426
INVALID_BACKUP_STATE: 0x6642,
427
PIN_NOT_SET: 0x5502,
428
INVALID_BACKUP_LENGTH: 0x6733,
429
INVALID_RESTORE_STATE: 0x6643,
430
INVALID_CHUNK_LENGTH: 0x6734,
431
INVALID_BACKUP_HEADER: 0x684a,
432
TRUSTCHAIN_WRONG_SEED: 0xb007
433
};
434
435
/**
436
* Get alternative status message for status code
437
* @param code - Status code number
438
* @returns Human-readable status message or undefined
439
*/
440
function getAltStatusMessage(code: number): string | undefined | null {
441
switch (code) {
442
case 0x6700:
443
return "Incorrect length";
444
case 0x6800:
445
return "Missing critical parameter";
446
case 0x6982:
447
return "Security not satisfied (dongle locked or have invalid access rights)";
448
case 0x6985:
449
return "Condition of use not satisfied (denied by the user?)";
450
case 0x6a80:
451
return "Invalid data received";
452
case 0x6b00:
453
return "Invalid parameter received";
454
case 0x5515:
455
return "Locked device";
456
default:
457
if (0x6f00 <= code && code <= 0x6fff) {
458
return "Internal error, please report";
459
}
460
return undefined;
461
}
462
}
463
```
464
465
## Usage Examples
466
467
### Device Permission and Connection
468
469
```typescript
470
import TransportWebUSB from "@ledgerhq/hw-transport-webusb";
471
472
// Check browser support
473
if (await TransportWebUSB.isSupported()) {
474
try {
475
// Request device permission (always shows dialog)
476
const transport = await TransportWebUSB.request();
477
478
// Or connect silently if already authorized
479
const transport2 = await TransportWebUSB.openConnected();
480
481
if (transport2) {
482
console.log("Connected to:", transport2.deviceModel?.productName);
483
}
484
} catch (error) {
485
if (error instanceof TransportOpenUserCancelled) {
486
console.log("User cancelled device selection");
487
}
488
}
489
}
490
```
491
492
### Device Listening
493
494
```typescript
495
import TransportWebUSB from "@ledgerhq/hw-transport-webusb";
496
497
// Listen for device events (must be called on user interaction)
498
const subscription = TransportWebUSB.listen({
499
next: (event) => {
500
if (event.type === "add") {
501
console.log("Device connected:", event.deviceModel?.productName);
502
// Open transport with the detected device
503
TransportWebUSB.open(event.descriptor).then(transport => {
504
// Use transport...
505
});
506
}
507
},
508
error: (error) => {
509
console.error("Device listening error:", error);
510
},
511
complete: () => {
512
console.log("Device listening completed");
513
}
514
});
515
516
// Later, stop listening
517
subscription.unsubscribe();
518
```
519
520
### APDU Exchange
521
522
```typescript
523
import TransportWebUSB from "@ledgerhq/hw-transport-webusb";
524
525
const transport = await TransportWebUSB.create();
526
527
try {
528
// Low-level APDU exchange
529
const getAppNameAPDU = Buffer.from("E0C4000000", "hex");
530
const response = await transport.exchange(getAppNameAPDU);
531
532
// High-level send method with status handling
533
const appInfo = await transport.send(0xE0, 0xC4, 0x00, 0x00);
534
535
console.log("App info:", appInfo.toString("hex"));
536
} finally {
537
await transport.close();
538
}
539
```
540
541
### Error Handling
542
543
```typescript
544
import TransportWebUSB, {
545
TransportWebUSBGestureRequired,
546
TransportInterfaceNotAvailable,
547
DisconnectedDeviceDuringOperation
548
} from "@ledgerhq/hw-transport-webusb";
549
550
try {
551
const transport = await TransportWebUSB.create();
552
553
// Listen for disconnect events
554
transport.on("disconnect", (error) => {
555
console.log("Device disconnected:", error.message);
556
});
557
558
const response = await transport.exchange(apduBuffer);
559
} catch (error) {
560
if (error instanceof TransportWebUSBGestureRequired) {
561
console.log("Please trigger this action from a user click");
562
} else if (error instanceof TransportInterfaceNotAvailable) {
563
console.log("Device interface not available. Please upgrade firmware.");
564
} else if (error instanceof DisconnectedDeviceDuringOperation) {
565
console.log("Device was disconnected during operation");
566
}
567
}
568
```