Ledger Hardware Wallet common interface of the communication layer
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Event-driven architecture for handling device state changes, connection management, and cleanup operations. The Transport class extends EventEmitter to provide real-time notifications about device status and connection changes.
Add event listeners to monitor transport and device state changes.
/**
* Listen to an event on an instance of transport
* Transport implementation can have specific events. Common events:
* - "disconnect": triggered if Transport is disconnected
* - "unresponsive": triggered when device becomes unresponsive
* - "responsive": triggered when device becomes responsive again
* @param eventName Name of the event to listen for
* @param cb Callback function to handle the event
*/
on(eventName: string, cb: Function): void;Usage Example:
import Transport from "@ledgerhq/hw-transport";
const transport = await MyTransport.create();
// Listen for disconnect events
transport.on("disconnect", () => {
console.log("Device disconnected unexpectedly");
// Clean up resources, notify user, attempt reconnection
handleDisconnection();
});
// Listen for device responsiveness
transport.on("unresponsive", () => {
console.warn("Device is not responding - please check your device");
showUserWarning("Device unresponsive");
});
transport.on("responsive", () => {
console.log("Device is responding again");
hideUserWarning();
});
// Custom events from specific transport implementations
transport.on("device-locked", () => {
console.log("Device is locked - user needs to unlock");
});Remove specific event listeners to prevent memory leaks and unwanted callbacks.
/**
* Stop listening to an event on an instance of transport
* @param eventName Name of the event to stop listening for
* @param cb The same callback function that was passed to on()
*/
off(eventName: string, cb: Function): void;Usage Example:
const transport = await MyTransport.create();
// Define event handler
const disconnectHandler = () => {
console.log("Device disconnected");
// Handle disconnection
};
// Add listener
transport.on("disconnect", disconnectHandler);
// Later, remove the specific listener
transport.off("disconnect", disconnectHandler);
// Remove all listeners for an event (not part of public API, but possible)
transport._events.removeAllListeners("disconnect");Emit events from transport implementations. This method is used internally by transport implementations and should not be called directly by applications.
/**
* Emit an event to all registered listeners (internal use)
* @param event Name of the event to emit
* @param args Arguments to pass to event listeners
*/
emit(event: string, ...args: any): void;Properly close the transport connection and clean up resources.
/**
* Close the exchange with the device
* @returns Promise that resolves when the transport is closed
*/
close(): Promise<void>;Usage Example:
const transport = await MyTransport.create();
try {
// Use the transport
transport.setScrambleKey("BTC");
const response = await transport.send(0xB0, 0x01, 0x00, 0x00);
} finally {
// Always close the transport
await transport.close();
console.log("Transport closed successfully");
}These events are supported by the base Transport class:
Emitted when the device is unexpectedly disconnected or becomes unavailable.
transport.on("disconnect", () => {
// Device was unplugged or connection lost
// Stop ongoing operations and clean up
});Emitted when a device stops responding during an operation (after unresponsiveTimeout).
transport.on("unresponsive", () => {
// Device is not responding to commands
// Show user feedback, but don't disconnect yet
});Emitted when a previously unresponsive device starts responding again.
transport.on("responsive", () => {
// Device is responding normally again
// Hide unresponsive warnings
});Different transport implementations may emit additional events:
// WebUSB transport might emit:
transport.on("device-selected", (device) => {
console.log("User selected device:", device.productName);
});
// Bluetooth transport might emit:
transport.on("pairing-request", () => {
console.log("Device is requesting pairing");
});
transport.on("battery-low", (level) => {
console.log("Device battery low:", level + "%");
});async function performDeviceOperation() {
let transport = null;
try {
// 1. Create connection
transport = await MyTransport.create();
// 2. Set up event handlers
transport.on("disconnect", () => {
console.log("Connection lost during operation");
});
// 3. Configure transport
transport.setScrambleKey("BTC");
transport.setExchangeTimeout(30000);
// 4. Perform operations
const result = await transport.send(0xE0, 0x40, 0x00, 0x00);
return result;
} finally {
// 5. Always clean up
if (transport) {
await transport.close();
}
}
}class LedgerDeviceManager {
constructor() {
this.transport = null;
this.isConnected = false;
}
async connect() {
if (this.transport) {
await this.disconnect();
}
this.transport = await MyTransport.create();
this.setupEventHandlers();
this.isConnected = true;
console.log("Connected to device");
}
setupEventHandlers() {
this.transport.on("disconnect", () => {
this.isConnected = false;
this.transport = null;
console.log("Device disconnected");
// Attempt reconnection after delay
setTimeout(() => this.attemptReconnection(), 2000);
});
this.transport.on("unresponsive", () => {
console.warn("Device unresponsive");
// Don't reconnect immediately, wait for responsive event
});
this.transport.on("responsive", () => {
console.log("Device responsive again");
});
}
async attemptReconnection() {
if (this.isConnected) return;
try {
await this.connect();
console.log("Reconnection successful");
} catch (error) {
console.error("Reconnection failed:", error);
// Try again after longer delay
setTimeout(() => this.attemptReconnection(), 5000);
}
}
async disconnect() {
if (this.transport) {
await this.transport.close();
this.transport = null;
}
this.isConnected = false;
}
async executeCommand(cla, ins, p1, p2, data) {
if (!this.isConnected || !this.transport) {
throw new Error("Device not connected");
}
return await this.transport.send(cla, ins, p1, p2, data);
}
}async function withTransportTimeout(operation, timeoutMs = 30000) {
let transport = null;
const operationPromise = async () => {
transport = await MyTransport.create();
return await operation(transport);
};
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Operation timeout")), timeoutMs);
});
try {
return await Promise.race([operationPromise(), timeoutPromise]);
} finally {
if (transport) {
await transport.close();
}
}
}
// Usage
const result = await withTransportTimeout(async (transport) => {
transport.setScrambleKey("ETH");
return await transport.send(0xE0, 0x02, 0x00, 0x00);
}, 15000);class LedgerEventManager extends EventEmitter {
constructor() {
super();
this.transport = null;
}
async init() {
this.transport = await MyTransport.create();
// Forward transport events to application
this.transport.on("disconnect", () => {
this.emit("device-disconnected");
});
this.transport.on("unresponsive", () => {
this.emit("device-unresponsive");
});
this.transport.on("responsive", () => {
this.emit("device-responsive");
});
this.emit("device-connected", this.transport.deviceModel);
}
}
// Application usage
const ledger = new LedgerEventManager();
ledger.on("device-connected", (deviceModel) => {
console.log("Ledger connected:", deviceModel.productName);
updateUI({ connected: true, device: deviceModel });
});
ledger.on("device-disconnected", () => {
console.log("Ledger disconnected");
updateUI({ connected: false });
});
ledger.on("device-unresponsive", () => {
showWarning("Device is not responding. Please check your device.");
});
ledger.on("device-responsive", () => {
hideWarning();
});
await ledger.init();async function robustDeviceOperation(operation) {
const maxRetries = 3;
let attempt = 0;
while (attempt < maxRetries) {
let transport = null;
try {
transport = await MyTransport.create();
return await operation(transport);
} catch (error) {
attempt++;
if (error.name === "DisconnectedDevice") {
console.log(`Attempt ${attempt}: Device disconnected, retrying...`);
} else if (error.name === "TransportRaceCondition") {
console.log(`Attempt ${attempt}: Race condition, retrying...`);
} else {
// Non-recoverable error
throw error;
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
} finally {
if (transport) {
try {
await transport.close();
} catch (closeError) {
console.warn("Error closing transport:", closeError);
}
}
}
}
throw new Error(`Operation failed after ${maxRetries} attempts`);
}