How Ledger Hardware Wallet Support is Integrated into Nautilus Wallet

Nautilus Wallet integrates Ledger hardware wallet support through the ledger-ergo-js library and official Ledger Web transports, implementing a layered architecture that abstracts device communication via the createTransport helper while isolating signing logic in the Prover class and UI interactions in dedicated Vue components.

Nautilus Wallet is an open-source browser extension wallet designed for the Ergo blockchain. To provide users with secure key storage and transaction signing capabilities, the codebase implements comprehensive Ledger hardware wallet support that spans transport configuration, device detection, wallet onboarding, and cryptographic signing workflows.

Transport Layer Abstraction and Configuration

The integration begins with a minimal abstraction over Ledger's official Web transport libraries. In src/common/ledger.ts, the createTransport(type) function returns either a WebHID or WebUSB transport instance based on the user's preference.

// src/common/ledger.ts
export function createTransport(type: TransportType): Promise<Transport> {
  return type === "webhid"
    ? WebHIDTransport.create()          // <‑‑ WebHID path
    : WebUSBTransport.create();         // <‑‑ WebUSB path
}

The default transport preference is stored in src/constants/settings.ts, where the configuration object specifies "webusb" as the initial value. The appStore (src/stores/appStore.ts) persists this choice and propagates it to both the UI components and the transaction signing logic, allowing runtime switching between transport protocols without code changes.

Device Detection and Application State Management

Device interaction is handled by the LedgerDevice.vue component located in src/components/LedgerDevice.vue. On component mount, it registers navigator.usb event listeners for connect and disconnect events to detect Ledger devices (identified by vendorId === 0x2c97).

When a device is detected, the component invokes createTransport(app.settings.ledger.transport) to instantiate a communication channel. It then constructs an ErgoLedgerApp instance and verifies that the Ergo application is open on the device. If the current application differs from "Ergo", the code calls device.openApp("Ergo") and waits for the application switch to complete.

// src/components/LedgerDevice.vue – openErgoApp()
ledger = new ErgoLedgerApp(await createTransport(app.settings.ledger.transport));
const currentApp = await ledger.device.getCurrentAppInfo();
if (currentApp.name !== "Ergo") {
  await ledger.device.openApp("Ergo");
}

The component maintains UI state—ready, loading, error, or locked—through a setState() callback, ensuring users receive real-time feedback during the connection process.

Wallet Onboarding and Extended Public Key Extraction

New Ledger wallets are added through the LedgerConnectView.vue component, accessible via the dedicated route /add/hw/ledger defined in src/extension/popup/router.ts. When the user initiates the connection, the view invokes ledgerDevice.openErgoApp() to ensure the Ergo application is active, then proceeds to extract the wallet's extended public key.

// src/views/add/LedgerConnectView.vue – add()
const ledgerApp = new ErgoLedgerApp(
  await createTransport(app.settings.ledger.transport)
).useAuthToken();

const ledgerXpk = await ledgerApp.getExtendedPublicKey("m/44'/429'/0'");
const extendedPublicKey = hex.encode(
  HdKey.fromPublicKey(ledgerXpk, "m/0").extendedPublicKey
);

const walletId = await app.putWallet({
  type: WalletType.Ledger,
  name: walletName.value,
  extendedPublicKey,
});

The derivation path m/44'/429'/0' follows Ergo's BIP44 standard. The returned public key is transformed into a read-only wallet entry with type WalletType.Ledger and persisted via app.putWallet() in the appStore. This design keeps private keys confined to the hardware device while allowing the wallet software to generate addresses and monitor balances.

Transaction Signing with the Prover Class

When signing transactions, the Prover class in src/chains/ergo/transaction/prover.ts checks the this.#useLedger flag. For Ledger-backed wallets, it instantiates the transport using the stored preference (this.#ledgerTransportType), creates an authenticated ErgoLedgerApp session via useAuthToken(), and calls signTx() with mapped inputs and outputs.

// src/chains/ergo/transaction/prover.ts – #signTx()
if (this.#useLedger) {
  const ledgerApp = new ErgoLedgerApp(
    await createTransport(this.#ledgerTransportType)
  ).useAuthToken();

  const proofs = await ledgerApp.signTx({
    inputs: mapLedgerInputs(unsigned, unspentBoxes, this.#from),
    dataInputs: mapLedgerDataInputs(dataInputs),
    outputs: mapLedgerOutputs(unsigned),
    distinctTokenIds: unsigned.distinct_token_ids(),
    changeMap: { … }
  }, MAINNET ? Network.Mainnet : Network.Testnet);

  return Transaction.from_unsigned_tx(unsigned, proofs);
}

The signTx method receives Ergo-specific transaction parameters including distinct token IDs and network identification. Progress updates flow back to the UI through the StateCallback mechanism established in LedgerDevice.vue, providing visual feedback during the physical device confirmation process.

Routing and State Persistence

The hardware wallet flow is integrated into the application's routing system through src/extension/popup/router.ts, which registers the /add/hw/ledger path to render the connection wizard. State management is centralized in src/stores/appStore.ts, where the settings.ledger.transport property maintains the user's transport preference across sessions.

This architecture ensures that all Ledger-related code remains isolated behind the createTransport abstraction, simplifying testing and future transport protocol additions while maintaining a consistent interface for device communication throughout the wallet lifecycle.

Summary

Frequently Asked Questions

Which transport protocols does Nautilus support for Ledger connectivity?

Nautilus supports both WebHID and WebUSB transports through the official @ledgerhq/hw-transport-webhid and @ledgerhq/hw-transport-webusb libraries. The active transport is configurable via user settings and defaults to WebUSB as defined in src/constants/settings.ts.

How does Nautilus verify that the correct application is open on the Ledger device?

During the connection flow, src/components/LedgerDevice.vue queries the device’s current application information using ledger.device.getCurrentAppInfo(). If the returned name does not match "Ergo", the component invokes device.openApp("Ergo") to switch applications automatically before proceeding with key extraction or signing.

What derivation path does Nautilus use when importing Ledger wallets?

Nautilus uses the standard Ergo BIP44 derivation path m/44'/429'/0' when calling ledgerApp.getExtendedPublicKey(). This path corresponds to Ergo's coin type (429) and extracts the account-level extended public key for generating addresses while keeping private keys secured on the hardware device.

Where is the Ledger signing logic implemented for Ergo transactions?

The signing logic resides in src/chains/ergo/transaction/prover.ts within the Prover class. When this.#useLedger is enabled, this class creates an authenticated ErgoLedgerApp instance, maps transaction inputs and outputs to Ledger-compatible formats using helper functions like mapLedgerInputs(), and executes signTx() to generate cryptographic proofs without exposing private keys to the host environment.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →