Skip to content

Ledger signing for the TypeScript keychain: transport-injected DMK core plus Node and browser entry points #306

Description

@ledgicr

Moving this out of docs/ and into an issue, at @dev-jodee's request on #301:
it is a proposal with no code behind it, so it does not belong in the docs tree.
Full text was docs/LEDGER_TYPESCRIPT_DESIGN.md at commit 8fb07b8 of #301,
and that file is deleted in the same PR.

Status: proposal, no code written. This is the browser path Pay.sh needs;
the Rust backend does not serve it.

Why this is not a port of the Rust backend. The Rust backend drives
solana-remote-wallet, which owns its own hidapi handle and speaks Solana-app
APDUs directly. None of that crosses: there is no hidapi in a browser, and
navigator.hid is a different API with a different permission model from
Node's. Ledger already solved this — the Device Management Kit is their
supported TypeScript stack and its central design decision is the one needed
here: the core is transport-agnostic and the transport is injected. So the right
shape is not "a Ledger signer" but a transport-injected core plus two thin entry
points.

Proposed packages, following the existing typescript/packages/*
convention (ESM-only, sideEffects: false, exports map with types +
import):

Package Contains
@solana/keychain-ledger-core The signer. All APDU and envelope logic. Takes a transport as a constructor argument, never imports one.
@solana/keychain-ledger-node createLedgerNodeSigner() — constructs the DMK Node HID transport.
@solana/keychain-ledger-web createLedgerWebSigner() — constructs the DMK WebHID transport.

The off-chain envelope is the trap, and it is worth reading before writing
code.
signMessage on a Ledger signs a structured envelope, not raw bytes,
and the envelope the device parses is not what solana-offchain-message
serializes — the app expects a longer v0 header carrying an application domain
and a signer list, which that crate writes neither of. The Rust backend builds
the app's layout by hand for exactly this reason and pins it against
LedgerHQ/app-solana's own source. A TypeScript implementation has to build the
same bytes, and anything verifying those signatures server-side has to build
them too. See rust/src/ledger/README.md and
rust/src/tests/test_ledger_envelope_conformance.rs.

Three things to confirm first, rather than design around:

  1. Which DMK package provides the Solana app's APDUs, and whether it implements
    off-chain message signing or only transaction signing. If DMK does the
    envelope, most of the trap above disappears. If not, we own it.
  2. Whether DMK's WebHID transport handles the device re-enumerating after an app
    launch. That is the event the Rust backend retries for five seconds; in a
    browser it may drop the permission grant entirely.
  3. Whether Pay.sh needs signMessage in the browser at all, or only
    signTransaction. If only the latter, the envelope work is out of scope for
    v1 and the surface shrinks a great deal.

Happy to write it, or to hand the design over — whichever suits the TS
roadmap. Flagging one thing either way: (1) and (2) are the answers that decide
how much work this is, and I have not verified them.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions