Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

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

  1. Plug the device into USB.
  2. Unlock with your PIN.
  3. 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 an approve on 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 repl always prints the address it read from the device; cross-check that against Ledger Live before signing anything substantial.