Ledger Wallet Integration
Ledger is the most common hardware wallet among DeFi users and professional traders. Integration with it opens access to an audience that fundamentally doesn't store keys in browser extensions or mobile apps. It's not just "add a connect button" — the device communication protocol is specific, and without understanding its details, you'll get unstable integration with poor UX.
How Device Communication Works
Ledger uses several transport layers depending on the environment:
- WebUSB — direct USB connection in the browser (Chrome, Edge). Requires HTTPS or localhost. Doesn't work in Firefox out of the box.
- WebHID — a newer API, recommended as the primary one. Supports Ledger Nano S Plus and Nano X.
-
Bluetooth — only Nano X, via
@ledgerhq/hw-transport-web-ble. Unstable on mobile browsers. -
Node.js HID — for desktop apps via
@ledgerhq/hw-transport-node-hid.
The @ledgerhq/hw-app-eth library encapsulates the APDU protocol (Application Protocol Data Unit) — low-level commands with which the host communicates with the device. You don't need to know APDU directly, but it's important to understand: each operation is a synchronous command/response, the device processes them sequentially.
Getting Address and Signing Transactions
Basic address retrieval flow:
import TransportWebHID from "@ledgerhq/hw-transport-webhid";
import Eth from "@ledgerhq/hw-app-eth";
async function getLedgerAddress(derivationPath: string): Promise<string> {
const transport = await TransportWebHID.create();
const eth = new Eth(transport);
try {
const result = await eth.getAddress(derivationPath, true); // true = display on device
return result.address;
} finally {
await transport.close();
}
}
Derivation path is critical. The BIP44 standard for Ethereum: m/44'/60'/0'/0/0. Ledger Live uses this path. Old Ledger Live used m/44'/60'/0' (without the last two segments) — some users have addresses there. When integrating, it's worth supporting multiple paths with user selection.
Signing a transaction requires RLP serialization and correct chain ID for EIP-155:
async function signTransaction(tx: TransactionRequest): Promise<string> {
const transport = await TransportWebHID.create();
const eth = new Eth(transport);
// Serialize transaction without signature
const unsignedTx = ethers.utils.serializeTransaction(tx);
const rlpEncoded = unsignedTx.slice(2); // remove 0x
const result = await eth.signTransaction(
"m/44'/60'/0'/0/0",
rlpEncoded,
null // resolution for ERC-20 tokens
);
// Assemble signature back
const signature = {
v: parseInt(result.v, 16),
r: '0x' + result.r,
s: '0x' + result.s,
};
return ethers.utils.serializeTransaction(tx, signature);
}
EIP-712 and Typed Data
For signing EIP-712 messages (permit, typed orders) — use eth.signEIP712Message. Important: older Ledger firmware doesn't support eth.signEIP712HashedMessage with full domain separator. Need to check firmware version or use fallback to eth.signPersonalMessage.
Common Issues and Solutions
Device is busy with another app. Ledger could be connected to Ledger Live or another tab. Transport will return error TransportError: Invalid channel. Solution: handle this error explicitly and show user a message "Close Ledger Live before using".
Blind signing is disabled. By default, Ledger requires enabling "blind signing" in Ethereum app settings on the device to sign contract transactions. Without it — error 0x6a80. Solution: warn the user in UI before initiating a transaction.
Confirmation timeout. The user didn't confirm on the device within the allocated time. @ledgerhq/hw-transport-webhid has no default timeout — the transaction hangs indefinitely. Add Promise.race with timeout and cancel button in UI.
Incompatibility with wagmi/viem. If using wagmi v2, the standard Ledger connector is via @ledgerhq/connect-kit-loader or custom connector based on createConnector. Direct integration via hw-app-eth works but requires manual provider management.
Ledger Connect Kit Integration
For web apps, Ledger offers Connect Kit — a universal way to connect via WalletConnect v2, iframe, or direct WebHID:
import { loadConnectKit, SupportedProviders } from "@ledgerhq/connect-kit-loader";
const connectKit = await loadConnectKit();
connectKit.checkSupport({
providerType: SupportedProviders.Ethereum,
walletConnectVersion: 2,
projectId: "YOUR_WC_PROJECT_ID",
});
const provider = await connectKit.getProvider();
This simplifies support for mobile users (Nano X via BLE + mobile browser), but adds a dependency on Ledger's infrastructure.
Stack and Timeline
| Component | Library |
|---|---|
| WebHID transport | @ledgerhq/hw-transport-webhid |
| Ethereum app | @ledgerhq/hw-app-eth |
| Bluetooth | @ledgerhq/hw-transport-web-ble |
| wagmi connector | custom or Connect Kit |
Basic integration (get address + sign ETH/ERC-20 transactions + EIP-712) — 1–2 weeks. Includes handling all error scenarios and testing on real devices (Nano S, Nano S Plus, Nano X).







