Repository navigation
feat: add the iota-sdk-ledger crate - #1702
Open
DaughterOfMars wants to merge 7 commits into
Open
DaughterOfMars wants to merge 7 commits into
DaughterOfMars wants to merge 7 commits into
Conversation
This was referenced Oct 12, 2026
Thoralf-M
reviewed
Oct 12, 2026
Thoralf-M
reviewed
Oct 12, 2026
DaughterOfMars
force-pushed
the
feat/ledger-crate
branch
from
October 14, 2026 07:57
6ea2815 to
bb31eb7
Compare
Thoralf-M
reviewed
Oct 21, 2026
Thoralf-M
reviewed
Oct 21, 2026
Thoralf-M
reviewed
Oct 21, 2026
Thoralf-M
approved these changes
Oct 21, 2026
Alex6323
reviewed
Oct 26, 2026
Alex6323
reviewed
Oct 26, 2026
DaughterOfMars
force-pushed
the
feat/ledger-crate
branch
from
October 26, 2026 12:09
3603b92 to
dd0135e
Compare
Alex6323
approved these changes
Oct 26, 2026
thibault-martinez
approved these changes
Oct 26, 2026
thibault-martinez
approved these changes
Oct 27, 2026
DaughterOfMars
added a commit
that referenced
this pull request
Oct 27, 2026
## Why Feature branch for #1675. Its first commit adds the `iota-sdk-ledger` crate (#1676); the PRs for the epic's remaining issues target this branch instead of `develop`, and it merges into `develop` once they have landed. Issues only close when a PR merges into `develop`, so the list below grows as each issue's PR merges here. Closes #1676. ## What changes The crate ports the monorepo's `iota-ledger`. No third-party types (`bip32`, `ledger-transport`, `hidapi`) reach the public API: `Transport` is a public trait over SDK-owned APDU types so hosts can bring their own link (#1682), and derivation paths are a new `DerivationPath` in `iota-sdk-crypto`, which the mnemonic constructors now parse with too. Signing takes a transaction or a personal message instead of any serializable intent. Passing input objects to an app older than 1.0 fails instead of silently blind-signing. After an app switch the device re-enumerates, so the crate polls for the expected app instead of sleeping 3s, and HID closes the old handle before reopening, since two open handles can lock the device. With several devices attached, `Ledger::hid_devices` and `Ledger::native_hid_device` select one. Speculos cannot switch apps and reports a placeholder app name, so its transport skips app switching, as the monorepo did. The app reports a disallowed derivation path and refused blind signing as bare syscall numbers (`0x0002`, `0x0008`), which map to their own errors. HID is a default feature and Speculos an opt-in one. CI installs `libudev`, which `hidapi` links on Linux, wherever the crate is built with HID, and `semver-checks` skips the crate until it is published. ## Test plan Ran against Speculos with IOTA app 1.0.1 (Nano S+): - version, public keys and addresses match the app's ragger fixtures - address verification, approved and rejected - personal message and clear-signed transfer, using the ragger transaction and gas coin (both BCS round-trip byte-for-byte); signatures verified with `iota-sdk-crypto` - refused blind signing and a disallowed path return their dedicated errors The app exits on the command after it rejects a derivation path, which looks like an app bug. Protocol tests against a scripted transport follow in #1677. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DaughterOfMars
force-pushed
the
feat/ledger-crate
branch
from
October 27, 2026 08:56
729af82 to
fa54301
Compare
DaughterOfMars
added a commit
that referenced
this pull request
Oct 27, 2026
Feature branch for #1675. Its first commit adds the `iota-sdk-ledger` crate (#1676); the PRs for the epic's remaining issues target this branch instead of `develop`, and it merges into `develop` once they have landed. Issues only close when a PR merges into `develop`, so the list below grows as each issue's PR merges here. Closes #1676. The crate ports the monorepo's `iota-ledger`. No third-party types (`bip32`, `ledger-transport`, `hidapi`) reach the public API: `Transport` is a public trait over SDK-owned APDU types so hosts can bring their own link (#1682), and derivation paths are a new `DerivationPath` in `iota-sdk-crypto`, which the mnemonic constructors now parse with too. Signing takes a transaction or a personal message instead of any serializable intent. Passing input objects to an app older than 1.0 fails instead of silently blind-signing. After an app switch the device re-enumerates, so the crate polls for the expected app instead of sleeping 3s, and HID closes the old handle before reopening, since two open handles can lock the device. With several devices attached, `Ledger::hid_devices` and `Ledger::native_hid_device` select one. Speculos cannot switch apps and reports a placeholder app name, so its transport skips app switching, as the monorepo did. The app reports a disallowed derivation path and refused blind signing as bare syscall numbers (`0x0002`, `0x0008`), which map to their own errors. HID is a default feature and Speculos an opt-in one. CI installs `libudev`, which `hidapi` links on Linux, wherever the crate is built with HID, and `semver-checks` skips the crate until it is published. Ran against Speculos with IOTA app 1.0.1 (Nano S+): - version, public keys and addresses match the app's ragger fixtures - address verification, approved and rejected - personal message and clear-signed transfer, using the ragger transaction and gas coin (both BCS round-trip byte-for-byte); signatures verified with `iota-sdk-crypto` - refused blind signing and a disallowed path return their dedicated errors The app exits on the command after it rejects a derivation path, which looks like an app bug. Protocol tests against a scripted transport follow in #1677. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DaughterOfMars
force-pushed
the
feat/ledger-crate
branch
from
October 27, 2026 13:01
fa54301 to
2e8289e
Compare
4 of 12 tasks
Feature branch for #1675. Its first commit adds the `iota-sdk-ledger` crate (#1676); the PRs for the epic's remaining issues target this branch instead of `develop`, and it merges into `develop` once they have landed. Issues only close when a PR merges into `develop`, so the list below grows as each issue's PR merges here. Closes #1676. The crate ports the monorepo's `iota-ledger`. No third-party types (`bip32`, `ledger-transport`, `hidapi`) reach the public API: `Transport` is a public trait over SDK-owned APDU types so hosts can bring their own link (#1682), and derivation paths are a new `DerivationPath` in `iota-sdk-crypto`, which the mnemonic constructors now parse with too. Signing takes a transaction or a personal message instead of any serializable intent. Passing input objects to an app older than 1.0 fails instead of silently blind-signing. After an app switch the device re-enumerates, so the crate polls for the expected app instead of sleeping 3s, and HID closes the old handle before reopening, since two open handles can lock the device. With several devices attached, `Ledger::hid_devices` and `Ledger::native_hid_device` select one. Speculos cannot switch apps and reports a placeholder app name, so its transport skips app switching, as the monorepo did. The app reports a disallowed derivation path and refused blind signing as bare syscall numbers (`0x0002`, `0x0008`), which map to their own errors. HID is a default feature and Speculos an opt-in one. CI installs `libudev`, which `hidapi` links on Linux, wherever the crate is built with HID, and `semver-checks` skips the crate until it is published. Ran against Speculos with IOTA app 1.0.1 (Nano S+): - version, public keys and addresses match the app's ragger fixtures - address verification, approved and rejected - personal message and clear-signed transfer, using the ragger transaction and gas coin (both BCS round-trip byte-for-byte); signatures verified with `iota-sdk-crypto` - refused blind signing and a disallowed path return their dedicated errors The app exits on the command after it rejects a derivation path, which looks like an app bug. Protocol tests against a scripted transport follow in #1677. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(ledger): make the transport async Host-provided transports such as WebHID or Bluetooth are asynchronous, and blocking on them is not possible on wasm32. `Transport` now returns boxed futures, `Send` except on wasm32, and every `Ledger` call is async. HID and Speculos still do blocking I/O inside their futures. The wait for an app switch no longer sleeps or reads the clock itself, neither of which works on wasm32: transports pause in `reconnect`, and `Ledger` retries a fixed number of times. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ledger): run calls on the same Ledger one at a time A command can take several exchanges with the device, and `Ledger` calls take `&self`, so two concurrent calls interleaved their exchanges and broke the device's command state. Each call now holds a lock on the transport for the whole command. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ledger): pause between attempts to find the app after a switch The wait relied on each transport pausing in `reconnect`, so a transport that keeps the default would use up every attempt at once. `Ledger` now waits between attempts itself, with a timer that needs no runtime and works on wasm32. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ledger): stop the USB HID transport from blocking the polling thread The HID connection now lives on a thread of its own, which also handles reconnects, so a dropped call cannot leave an exchange running against a closed handle while a new one opens. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore(ledger): leave the waiting out of the reconnect docs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore(ledger): note the Speculos blocking on its constructor Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(ledger): read the version before locking the transport to sign Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore(ledger): check that every Ledger future is Send Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * rename nit --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(ledger): add LedgerSigner Signs transactions and personal messages with the key at one derivation path and returns `UserSignature`s. The device hashes the intent message itself, so the digest-based signing traits do not apply. Before each signature the signer reads the public key again and fails with `AddressMismatch` if it changed, as the monorepo's address check did; `with_expected_address` catches a wrong device or path up front. Closes #1678. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(ledger): leave the expected-address check to callers Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(ledger): name the changed-key error after its cause Without the expected-address check, the error only reports that the device's key changed since the signer was created. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(ledger): construct a signer from a Ledger Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor(ledger): name the key error without assuming its cause A different key for the signer's path also comes from the same device unlocked with another seed or passphrase, where reconnecting does not help. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * reword * chore(ledger): open the app in the README signing example Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * sort errors * reword * chore(ledger): check that a changed key also stops personal message signing Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(ledger): reject clear-signing objects that miss a referenced input or gas coin Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(ledger): reconnect in open_app when the device does not answer and add LedgerSigner::open_app Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(ledger): add sign_intent to Ledger and LedgerSigner Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * remove open_app fn * add ledger_mut --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat: sign with a Ledger through the transaction builder The transaction builder's `ledger` feature implements `TransactionSigner` for `LedgerSigner`, which blind-signs, and for a signer paired with a client through `LedgerSigner::with_client`, which fetches the gas coins and immutable or owned inputs at their versions to clear-sign and falls back to blind signing, with a warning, when it cannot. The umbrella crate gets opt-in `ledger`, `ledger-hid` and `ledger-speculos` features. HID is separate so that targets without hidapi, such as mobile and wasm through the FFI, can still use the crate with their own transport. Closes #1679. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor: pass the builder's objects to signers instead of pairing a client The signer paired with its own client fetched objects the builder had already fetched while resolving, through a second copy of the client. The builder now keeps those objects and, for signers that ask for them through `TransactionSigner::uses_objects`, passes them to `sign_with_objects`, fetching only the ones it lacks. Both methods have defaults, so existing signers are unchanged and never cause a fetch. `LedgerSigner` clear-signs through `sign_with_objects`, so `LedgerSigner::with_client` and its pairing type are gone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor: drop the version check on cached signing objects Cached objects are the ones the transaction's references were built from, so their versions always match. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor: check whether a signer wants objects at the call sites Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore: install libudev for the examples job The examples build `iota-sdk` with all features, which now include the Ledger HID transport. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat: pass signers the shared and receiving objects a transaction reads LedgerSigner still sends the device only the gas coins and immutable or owned inputs, the objects the IOTA app can match. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: fail execution when the objects for a signer cannot be fetched The Ledger signer blind-signed instead, which hid the fetch error behind `BlindSigningDisabled` by default, and blind-signed transfers the app can clear-sign when blind signing was enabled. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore: sort iota-ledger as an internal dependency Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore: document the ledger feature on docs.rs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * check device_objects * slightly improve clones * fix(transaction-builder): warn when the offline sponsored path blind-signs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(ledger): leave out unreferenced objects instead of filtering them in the builder The Ledger signer now checks the app version before signing, so falling back to blind signing on an app older than 1.0 reads the public key once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(ledger): fall back to blind signing in LedgerSigner when the app cannot clear-sign Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat: add the Ledger sign-and-send example and crate docs The example connects to the Speculos emulator when `LEDGER_SIMULATOR` is set and to a USB device otherwise, funds the device's address from the faucet and executes a transfer that the device clear-signs. It is left out of `make examples`, since it needs a device or the emulator. The ledger crate's README covers using a device, running Speculos, the Linux udev rules and when the app needs blind signing. Closes #1680. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: use the current Ledger constructor and open_app names in the example and docs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore: set the app name Speculos reports in the Ledger README Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: describe the SPECULOS_APPNAME format in the Ledger README Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore: add a script, action and make targets to start Speculos with the IOTA app Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: run the Ledger example on Speculos in CI Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: point the Ledger README to the Speculos make targets Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: move the Speculos scripts into scripts/speculos and drop the action Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: name the approval script's arguments and time out the Ledger CI step Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
DaughterOfMars
force-pushed
the
feat/ledger-crate
branch
from
November 6, 2026 09:43
cc49ab4 to
710c5b4
Compare
* feat(ledger)!: make Ledger cloneable Clones share the connection and its lock, so commands still run one at a time across them, and one device can back several signers while the `Ledger` stays usable. `open_app` and `exit_app` lock instead of taking `&mut self`, which makes `LedgerSigner::ledger_mut` unnecessary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ledger): check the key and sign under one lock Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(ledger): restore the Ledger doc comment Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(ledger): test that clones share the cached app version Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pcd8NHtD9DLWiw29MMfBct * chore(ledger): test that the signer keeps the device locked until it signs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pcd8NHtD9DLWiw29MMfBct --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Chloe Martin <chloedaughterofmars@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Feature branch for #1675. Its first commit adds the
iota-sdk-ledgercrate (#1676); the PRs for the epic's remaining issues target this branch instead ofdevelop, and it merges intodeveloponce they have landed.Issues only close when a PR merges into
develop, so the list below grows as each issue's PR merges here.Closes #1676.
What changes
The crate ports the monorepo's
iota-ledger. No third-party types (bip32,ledger-transport,hidapi) reach the public API:Transportis a public trait over SDK-owned APDU types so hosts can bring their own link (#1682), and derivation paths are a newDerivationPathiniota-sdk-crypto, which the mnemonic constructors now parse with too.Signing takes a transaction or a personal message instead of any serializable intent. Passing input objects to an app older than 1.0 fails instead of silently blind-signing.
After an app switch the device re-enumerates, so the crate polls for the expected app instead of sleeping 3s, and HID closes the old handle before reopening, since two open handles can lock the device. With several devices attached,
Ledger::hid_devicesandLedger::native_hid_deviceselect one. Speculos cannot switch apps and reports a placeholder app name, so its transport skips app switching, as the monorepo did. The app reports a disallowed derivation path and refused blind signing as bare syscall numbers (0x0002,0x0008), which map to their own errors.HID is a default feature and Speculos an opt-in one. CI installs
libudev, whichhidapilinks on Linux, wherever the crate is built with HID, andsemver-checksskips the crate until it is published.Test plan
Ran against Speculos with IOTA app 1.0.1 (Nano S+):
iota-sdk-cryptoThe app exits on the command after it rejects a derivation path, which looks like an app bug. Protocol tests against a scripted transport follow in #1677.
🤖 Generated with Claude Code