Ledger Hardware Wallet WebUSB implementation of the communication layer for web browsers
npx @tessl/cli install tessl/npm-ledgerhq--hw-transport-webusb@6.29.0Ledger 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.
npm install @ledgerhq/hw-transport-webusbimport 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";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();
}The Ledger WebUSB Transport is built around several key components:
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;
}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>;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;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 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;/**
* 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 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;
}>;
}/**
* 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 {}/**
* 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;
}
}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");
}
}
}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();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();
}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");
}
}