Ledger signing
End-to-end: install the optional deps, add a profile, plug in the
device, sign an approve and a deposit from a hardware wallet.
pnpm add @ledgerhq/hw-transport-node-hid @ledgerhq/hw-app-eth
apyx repl --profile cold
- Why hardware
- Step 1 — install the optional deps
- Step 2 — add a Ledger profile
- Step 3 — physical setup
- Step 4 — open the REPL
- Step 5 — sign an approve
- Step 6 — sign a deposit
- Cleanup
- Linux: udev rules (one-time)
- Hardware-less testing
- Common pitfalls
- Related
Why hardware
Local-key signing is fine for a hot wallet with bounded funds — a testing account, an automated worker with daily-limited approvals. For anything at protocol scale, hardware-wallet signing means:
- The private key never leaves the device.
- Every transaction is reviewed on the device’s screen — destination contract, function, decoded args, chainId, gas cap.
- An attacker on your laptop can submit a malicious tx to your CLI but cannot sign it.
The CLI integrates with Ledger via @ledgerhq/hw-app-eth over USB-HID.
On every write, the device prompts and you confirm physically.
Step 1 — install the optional deps
The SDK lists Ledger packages under optionalDependencies, so a stock
pnpm add @koed_jang/apyx-sdk does not pull them in. Add them when
you want hardware support:
pnpm add @ledgerhq/hw-transport-node-hid @ledgerhq/hw-app-eth
If you forget, the CLI fails fast with the install hint:
apyx: Ledger support requires optional deps. Install them with:
pnpm add @ledgerhq/hw-transport-node-hid @ledgerhq/hw-app-eth
Step 2 — add a Ledger profile
Edit ./apyx.config.json (or ~/.apyx/config.json):
{
"defaultProfile": "default",
"profiles": {
"default": {
"chain": "ethereum",
"rpcUrl": "https://eth.llamarpc.com",
"signer": { "type": "key", "keyPath": "~/.apyx/keys/default" }
},
"cold": {
"chain": "ethereum",
"rpcUrl": "https://eth-mainnet.g.alchemy.com/v2/SECRET",
"signer": {
"type": "ledger",
"derivationPath": "m/44'/60'/0'/0/0"
}
}
}
}
The derivation path matches whatever Ledger Live shows for the account you want to use.
Step 3 — physical setup
- Plug the device into USB.
- Unlock with your PIN.
- Open the Ethereum app on the device.
If the Ethereum app isn’t installed, install it from Ledger Live’s manager. The CLI talks only to the Ethereum app; other apps don’t expose the right methods.
Step 4 — open the REPL
$ apyx repl --profile cold
@apyx-labs/sdk
profile: cold (from /path/to/cwd/apyx.config.json)
chain: ethereum (1)
rpc: https://eth-mainnet.g.alchemy.com/v2/SECRET
account: 0x1234…abcd
The account: line is the address read from the device — no key
material crossed the wire.
Step 5 — sign an approve
apyx> await apyx.apxUSD.approve({
spender: apyx.addresses.apyUSD,
amount: viem.parseUnits('1', 6),
})
[ledger]: review tx on device…
On the device’s screen:
Review transaction
─────────────────
Approve
1.00 apxUSD
to apyUSD vault
0x38EE…E8a6A
Network: Ethereum
Max fees: 0.00210 ETH
─────────────────
Approve and send
[Cancel] [Approve]
Press the right button to approve. The CLI returns once the device has produced a signature:
[ledger]: signed
{ hash: '0x3c6a…a9e1', wait: [Function: wait] }
apyx> await tx.wait()
{ status: 'success', blockNumber: 19884112n, ... }
Step 6 — sign a deposit
Same flow:
apyx> const dep = await apyx.apyUSD.deposit({
assets: viem.parseUnits('1', 6),
receiver: account.address,
})
[ledger]: review tx on device…
[ledger]: signed
apyx> await dep.wait()
{ status: 'success', ... }
Cleanup
.exit closes the HID transport cleanly. The device returns to its
idle screen.
Linux: udev rules (one-time)
Linux requires udev rules so non-root processes can talk to the Ledger HID interface:
wget -q -O - https://raw.githubusercontent.com/LedgerHQ/udev-rules/master/add_udev_rules.sh \
| sudo bash
Re-plug after the script runs. macOS and Windows do not need this.
Hardware-less testing
The Ledger code path can be tested without a physical device — the SDK
exposes a ledgerFactory hook on resolveSigner that lets a test
inject a mock backed by a real private key. See the canonical e2e
spec:
test/e2e/cli/ledger.e2e.spec.ts— drives the full production dispatch with a crypto-backed mock against an anvil mainnet fork; lands anapproveon chain.
This is the same pattern the SDK uses internally — your own test harness can do the same.
Common pitfalls
Cannot open device(Linux only) — udev rules missing. Run the Ledger script above.UNKNOWN_ERROR (0x6804)— the Ethereum app isn’t open. Switch to it on the device and retry.DisconnectedDeviceDuringOperation— the device went to sleep (long PIN timeout) or the USB handle got recycled by another process (browser USB pop-up, Ledger Live). Unplug + re-plug, retry.- Address mismatch — your config’s derivation path doesn’t match
the account you think you’re using.
apyx replalways prints the address it read from the device; cross-check that against Ledger Live before signing anything substantial.
Related
- Ledger Setup — full reference (installation, udev, HID lifecycle).
- Approve and deposit — the two-step flow you just signed.
- Configuration —
signer.type = "ledger"schema.