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:
- 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.
- 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.
- 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.
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.mdat commit8fb07b8of #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 ownhidapihandle and speaks Solana-appAPDUs directly. None of that crosses: there is no
hidapiin a browser, andnavigator.hidis a different API with a different permission model fromNode'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,exportsmap withtypes+import):@solana/keychain-ledger-core@solana/keychain-ledger-nodecreateLedgerNodeSigner()— constructs the DMK Node HID transport.@solana/keychain-ledger-webcreateLedgerWebSigner()— constructs the DMK WebHID transport.The off-chain envelope is the trap, and it is worth reading before writing
code.
signMessageon a Ledger signs a structured envelope, not raw bytes,and the envelope the device parses is not what
solana-offchain-messageserializes — 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 thesame bytes, and anything verifying those signatures server-side has to build
them too. See
rust/src/ledger/README.mdandrust/src/tests/test_ledger_envelope_conformance.rs.Three things to confirm first, rather than design around:
off-chain message signing or only transaction signing. If DMK does the
envelope, most of the trap above disappears. If not, we own it.
launch. That is the event the Rust backend retries for five seconds; in a
browser it may drop the permission grant entirely.
signMessagein the browser at all, or onlysignTransaction. If only the latter, the envelope work is out of scope forv1 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.