butr
Connectors

Ledger

Ledger hardware wallets over WebUSB — EVM, Solana, Sui, and Bitcoin. Chromium-only, signing-only, no RPC or events.

@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

npm install @usebutr/ledger @ledgerhq/hw-transport-webusb

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

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

You only pay for the apps you actually use.

Build the adapter

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

EVM

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

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

Solana

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

Sui

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

Bitcoin

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

OptionDefaultNotes
accountCount1How many paths to enumerate via getAccounts(). Each hits the device (~1–2s) — keep small.
derivationPathPrefixper-platform (see below)getAccounts(n) appends the index.
id / name / icon"ledger" / "Ledger" / LEDGER_*_DEFAULT_ICONPer-platform icon constants are exported.

Per-platform defaults

PlatformchainId / clusterDerivation prefixNotes
EVMchainId: 144'/60'/0'/0Local state — Ledger has no on-device "current chain".
SVMcluster: "mainnet"44'/501'/0'Each account appends /<n>' (hardened).
Suicluster: "mainnet"44'/784'/0'/0'Each account appends /<n>' (canonical 5-segment).
Bitcoinmainnet bip122:000…84'/0'/0'/0BIP-84 native SegWit. Each account appends /<n>.

Register it through the unified provider exactly like WalletConnect:

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

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

EVMSVMSuiBitcoinWhy
signMessage@ledgerhq/hw-app-sui doesn't expose signPersonalMessage at the current version.
signTransactionBitcoin uses Bitcoin app v2.1+'s signPsbt (PSBT in, signed PSBT out).
sendTransactionLedger signs but doesn't broadcast. Wrap getSigner() with your own RPC.
subscribeNo events — devices don't push.
switchChainLocal state only — nothing is written to the device.
switchAccountUse accountCount to enumerate; pass account per call.
requestAccountsWalking to more paths is sequential and slow.
getBalanceNo RPC.
getTransactionReceiptSame.

Caveats

WebUSB only. Works in Chromium browsers (Chrome, Edge, Brave, Arc). Not Firefox, not Safari. Gate the Ledger affordance accordingly.

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.

Sui has no signMessage at the current @ledgerhq/hw-app-sui version. The capability flag reflects this — gate the UI accordingly.

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.

  • No persistence — the device must be reconnected on reload (no session survives).
  • No eventssubscribe() 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.

Source: packages/ledger/src (adapter.ts, capabilities.ts, apps/{evm,svm,sui,bitcoin}.ts) in the butr repository.

On this page