# Ledger (/connectors/ledger)



`@usebutr/ledger` is a Ledger hardware-wallet adapter using WebUSB. It ships
per-platform factories for EVM, SVM (Solana), Sui, and Bitcoin. It signs
messages and transactions; it does **not** broadcast or read chain state.

## Install [#install]

<CodeBlockTabs defaultValue="npm" groupId="package-manager">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install @usebutr/ledger @ledgerhq/hw-transport-webusb
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @usebutr/ledger @ledgerhq/hw-transport-webusb
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @usebutr/ledger @ledgerhq/hw-transport-webusb
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @usebutr/ledger @ledgerhq/hw-transport-webusb
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Then add the Ledger app modules you need — each is an optional peer dep loaded
on demand:

<CodeBlockTabs defaultValue="npm" groupId="package-manager">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install @ledgerhq/hw-app-eth      # EVM
    npm install @ledgerhq/hw-app-solana   # SVM
    npm install @ledgerhq/hw-app-sui      # Sui
    npm install @ledgerhq/hw-app-btc      # Bitcoin
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @ledgerhq/hw-app-eth # EVM
    pnpm add @ledgerhq/hw-app-solana # SVM
    pnpm add @ledgerhq/hw-app-sui # Sui
    pnpm add @ledgerhq/hw-app-btc # Bitcoin
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @ledgerhq/hw-app-eth # EVM
    yarn add @ledgerhq/hw-app-solana # SVM
    yarn add @ledgerhq/hw-app-sui # Sui
    yarn add @ledgerhq/hw-app-btc # Bitcoin
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @ledgerhq/hw-app-eth # EVM
    bun add @ledgerhq/hw-app-solana # SVM
    bun add @ledgerhq/hw-app-sui # Sui
    bun add @ledgerhq/hw-app-btc # Bitcoin
    ```
  </CodeBlockTab>
</CodeBlockTabs>

You only pay for the apps you actually use.

## Build the adapter [#build-the-adapter]

The unified factory dispatches on `options.platform`. The discriminant is
required — every caller passes it explicitly.

### EVM [#evm]

```ts
import { createLedgerAdapter } from "@usebutr/ledger";

const ledger = await createLedgerAdapter({
  platform: "evm",
  chainId: 1, // EIP-155 number, not CAIP-2
  accountCount: 3,
});
```

### Solana [#solana]

```ts
const ledger = await createLedgerAdapter({
  platform: "svm",
  cluster: "mainnet", // "mainnet" | "testnet" | "devnet"
  accountCount: 3,
});
```

### Sui [#sui]

```ts
const ledger = await createLedgerAdapter({
  platform: "sui",
  cluster: "mainnet",
  accountCount: 3,
});
```

### Bitcoin [#bitcoin]

```ts
const ledger = await createLedgerAdapter({
  platform: "bitcoin",
  addressFormat: "bech32", // "legacy" | "p2sh" | "bech32" | "bech32m"
  accountCount: 3,
});
```

You can also import each per-platform factory directly:
`createEvmLedgerAdapter`, `createSvmLedgerAdapter`, `createSuiLedgerAdapter`,
`createBitcoinLedgerAdapter`.

### Common options [#common-options]

| Option                 | Default                                           | Notes                                                                                        |
| ---------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `accountCount`         | `1`                                               | How many paths to enumerate via `getAccounts()`. Each hits the device (\~1–2s) — keep small. |
| `derivationPathPrefix` | per-platform (see below)                          | `getAccounts(n)` appends the index.                                                          |
| `id` / `name` / `icon` | `"ledger"` / `"Ledger"` / `LEDGER_*_DEFAULT_ICON` | Per-platform icon constants are exported.                                                    |

### Per-platform defaults [#per-platform-defaults]

| Platform | `chainId` / `cluster` | Derivation prefix | Notes                                                  |
| -------- | --------------------- | ----------------- | ------------------------------------------------------ |
| EVM      | `chainId: 1`          | `44'/60'/0'/0`    | Local state — Ledger has no on-device "current chain". |
| SVM      | `cluster: "mainnet"`  | `44'/501'/0'`     | Each account appends `/<n>'` (hardened).               |
| Sui      | `cluster: "mainnet"`  | `44'/784'/0'/0'`  | Each account appends `/<n>'` (canonical 5-segment).    |
| Bitcoin  | mainnet `bip122:000…` | `84'/0'/0'/0`     | BIP-84 native SegWit. Each account appends `/<n>`.     |

Register it through the unified provider exactly like
[WalletConnect](/connectors/walletconnect):

```tsx
import { WalletManagerProvider } from "@usebutr/react";
import { autoDiscovery } from "@usebutr/wallets";
import { createLedgerAdapter } from "@usebutr/ledger";

const discovery = autoDiscovery();
const ledger = await createLedgerAdapter({ platform: "sui", accountCount: 3 });
const extra = new Map([[ledger.id, ledger]]);

<WalletManagerProvider
  discovery={discovery}
  connectors={[{ id: ledger.id, name: ledger.name, chainPlatform: ledger.chainPlatform }]}
  createConnector={(id) => extra.get(id) ?? null}
>
  {children}
</WalletManagerProvider>;
```

On `connect()` the browser shows the WebUSB permission prompt and the user
unlocks the device on the platform's Ledger app (Ethereum, Solana, Sui, or
Bitcoin).

## Capabilities [#capabilities]

`LEDGER_CAPABILITIES` is hardware-only and identical across EVM, SVM, and
Bitcoin. Sui diverges in one spot (no `signMessage`):

|                         | EVM | SVM | Sui | Bitcoin | Why                                                                                 |
| ----------------------- | :-: | :-: | :-: | :-----: | ----------------------------------------------------------------------------------- |
| `signMessage`           |  ✓  |  ✓  |     |    ✓    | `@ledgerhq/hw-app-sui` doesn't expose `signPersonalMessage` at the current version. |
| `signTransaction`       |  ✓  |  ✓  |  ✓  |    ✓    | Bitcoin uses Bitcoin app v2.1+'s `signPsbt` (PSBT in, signed PSBT out).             |
| `sendTransaction`       |     |     |     |         | Ledger signs but doesn't broadcast. Wrap `getSigner()` with your own RPC.           |
| `subscribe`             |     |     |     |         | No events — devices don't push.                                                     |
| `switchChain`           |  ✓  |  ✓  |  ✓  |    ✓    | Local state only — nothing is written to the device.                                |
| `switchAccount`         |     |     |     |         | Use `accountCount` to enumerate; pass `account` per call.                           |
| `requestAccounts`       |     |     |     |         | Walking to more paths is sequential and slow.                                       |
| `getBalance`            |     |     |     |         | No RPC.                                                                             |
| `getTransactionReceipt` |     |     |     |         | Same.                                                                               |

## Caveats [#caveats]

<Callout type="warn">
  **WebUSB only.** Works in Chromium browsers (Chrome, Edge, Brave, Arc). Not Firefox, not Safari.
  Gate the Ledger affordance accordingly.
</Callout>

<Callout type="warn">
  **Signing only.** `sendTx`, `sendTxToChain`, `getBalance`, and `getTransactionReceipt` reject with
  descriptive errors. To submit a transaction, wrap `getSigner()` with viem / @solana/kit /
  @mysten/sui / bitcoinjs-lib and your own RPC, then broadcast there.
</Callout>

<Callout type="warn">
  **Sui has no `signMessage`** at the current `@ledgerhq/hw-app-sui` version. The capability flag
  reflects this — gate the UI accordingly.
</Callout>

<Callout type="warn">
  **Bitcoin PSBTs need full BIP-32 derivation paths.** The adapter doesn't backfill missing
  `PSBT_IN_BIP32_DERIVATION` entries — your PSBT builder must populate them, or the device will
  reject the request.
</Callout>

* **No persistence** — the device must be reconnected on reload (no session
  survives).
* **No events** — `subscribe()` is a no-op.
* **Multiple accounts via paths** — there's no `switchAccount`; expose more
  addresses with `accountCount` and pass a specific `account` to
  `signMessage` / `signTransaction`. Walking to a non-active path hits the
  device sequentially.
* **Manual chain** — call `switchChain()` to update the adapter's local
  state; it isn't written to the device.

<Callout type="info">
  **Source:** `packages/ledger/src` (`adapter.ts`, `capabilities.ts`,
  `apps/{evm,svm,sui,bitcoin}.ts`) in the [butr
  repository](https://github.com/pedroapfilho/usebutr/tree/main/packages/ledger).
</Callout>
