CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-ledgerhq--hw-transport

Ledger Hardware Wallet common interface of the communication layer

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

events-lifecycle.mddocs/

Events and Lifecycle Management

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.

Capabilities

Event Listening

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");
});

Event Removal

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");

Event Emission (Internal)

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;

Connection Cleanup

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");
}

Common Events

Standard Transport Events

These events are supported by the base Transport class:

disconnect

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
});

unresponsive

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
});

responsive

Emitted when a previously unresponsive device starts responding again.

transport.on("responsive", () => {
  // Device is responding normally again
  // Hide unresponsive warnings
});

Transport-Specific Events

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 + "%");
});

Lifecycle Management Patterns

Basic Connection Lifecycle

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();
    }
  }
}

Persistent Connection Management

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);
  }
}

Resource Management with Timeout

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);

Event-Driven Application Architecture

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();

Error Handling in Lifecycle

Graceful Error Recovery

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`);
}

docs

apdu-communication.md

configuration.md

device-management.md

error-handling.md

events-lifecycle.md

index.md

tile.json