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-webusbThen 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 # BitcoinYou 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
| 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
| 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:
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):
| 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
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 events —
subscribe()is a no-op. - Multiple accounts via paths — there's no
switchAccount; expose more addresses withaccountCountand pass a specificaccounttosignMessage/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.