Rust library to talk to IOTA application on Ledger
  • Rust 99.6%
  • Scheme 0.4%
Find a file
2026-06-09 21:43:48 +01:00
.github/workflows fix: initial commit 2026-02-13 19:32:31 +00:00
examples feat: add raw EdDSA signing for verifiable credentials 2026-06-09 21:43:48 +01:00
src feat: add raw EdDSA signing for verifiable credentials 2026-06-09 21:43:48 +01:00
tests feat: add raw EdDSA signing for verifiable credentials 2026-06-09 21:43:48 +01:00
.envrc fix: initial commit 2026-02-13 19:32:31 +00:00
.gitignore fix: initial commit 2026-02-13 19:32:31 +00:00
Cargo.toml feat: add raw EdDSA signing for verifiable credentials 2026-06-09 21:43:48 +01:00
CHANGELOG.md feat: add raw EdDSA signing for verifiable credentials 2026-06-09 21:43:48 +01:00
docker-compose.yml chore: expand test coverage 2026-02-14 07:34:35 +00:00
LICENSE fix: initial commit 2026-02-13 19:32:31 +00:00
manifest.scm fix: initial commit 2026-02-13 19:32:31 +00:00
README.md fix: improve handling of different device states and reconnect 2026-02-14 18:07:09 +00:00

Ledger Library for IOTA Application

crates.io

Rust client library for talking to the IOTA Rebased Ledger app (app-iota v1.0.x). Standalone — no IOTA SDK dependency. It handles USB HID for real devices and TCP for the Speculos simulator.

The official Ledger app lives at iotaledger/ledger-app-iota. See the IOTA Ledger guide for device setup.

Usage

Connect and derive address

use ledger_iota::{LedgerIota, Bip32Path, TransportType};

let ledger = LedgerIota::new(&TransportType::NativeHID)?;

let version = ledger.get_version()?;
println!("{version}");

let path = Bip32Path::iota(0, 0, 0);
let (pubkey, address) = ledger.get_pubkey(&path)?;
println!("address: {address}");

Verify address on device

Prompts the user to confirm the address on the Ledger display:

let (pubkey, address) = ledger.verify_address(&path)?;

Sign a message

let message = b"Hi, this is my wallet";
let signature = ledger.sign_message(message, &path)?;

The device displays the message and asks for confirmation. Max message size is 2 KB on Nano X, 4 KB on other devices.

Build and sign a transfer

use ledger_iota::{build_transfer_tx, GasCoinRef};

let gas = GasCoinRef { object_id, version, digest }; // from RPC
let tx_bytes = build_transfer_tx(&sender, &recipient, amount, &gas, gas_budget, gas_price);

let signature = ledger.sign_tx(&tx_bytes, &path, None)?;

Without object data the device will show a blind signing prompt (or reject if blind signing is disabled). For clear signing, pass coin objects so the device can display transfer details:

use ledger_iota::{ObjectData, Owner};

let objects = vec![ObjectData::gas_coin(
    version,
    contents,
    Owner::AddressOwner(sender),
    previous_transaction,
    storage_rebate,
)];
let signature = ledger.sign_tx(&tx_bytes, &path, Some(&objects))?;

Features

Feature Default Description
hid yes USB HID transport for real Ledger devices
tcp no TCP transport for Speculos simulator
iota-sdk-types no SDK object conversion and SDK return types for get_pubkey/sign_tx
[dependencies]
ledger-iota = "0.1"

# for Speculos testing
ledger-iota = { version = "0.1", features = ["tcp"] }

# with iota-sdk-types integration
ledger-iota = { version = "0.1", features = ["iota-sdk-types"] }

Converting SDK objects for clear signing

With the iota-sdk-types feature enabled, you can convert SDK objects directly:

use ledger_iota::ObjectData;

let sdk_objects: Vec<iota_sdk_types::Object> = /* from RPC */;
let objects: Vec<ObjectData> = sdk_objects
    .into_iter()
    .map(ObjectData::try_from)
    .collect::<Result<_, _>>()?;

let signature = ledger.sign_tx(&tx_bytes, &path, Some(&objects))?;

Supported object types: GasCoin, custom coins (0x2::coin::Coin<T>), and StakedIota.

Device status and reconnection

check_status() probes the device and returns a DeviceStatus:

DeviceStatus Device state What the user should do
Connected IOTA app open, ready to sign Nothing — good to go
AppClosed On the dashboard, no app open Open the IOTA app
WrongApp(_) A different app is open Close it and open the IOTA app
Locked Locked after PIN timeout Enter PIN, then reopen the IOTA app
Disconnected Not on USB (unplugged or at boot PIN screen) Plug in and unlock
use ledger_iota::DeviceStatus;

match ledger.check_status() {
    DeviceStatus::Connected => { /* ready */ }
    DeviceStatus::Locked => println!("please unlock your Ledger"),
    DeviceStatus::AppClosed => println!("please open the IOTA app"),
    DeviceStatus::WrongApp(_) => println!("wrong app — open the IOTA app"),
    DeviceStatus::Disconnected => println!("Ledger not found — plug it in and unlock"),
}

When the device locks or disconnects, the USB handle goes stale. Call reconnect() after the user restores the device:

ledger.reconnect()?; // drops old handle, re-enumerates USB, verifies IOTA app

The Ledger is invisible on USB while showing the PIN screen (at boot or after a power cycle). It only appears after the user enters the PIN.

Examples

cargo run --example version       # print app version
cargo run --example address       # derive address for default path
cargo run --example verify        # verify address on device
cargo run --example pubkeys       # generate range of pubkeys
cargo run --example sign --features tcp  # sign with Speculos
cargo run --example sign_message         # sign a personal message
cargo run --example send_iota -- 0x<ADDR> 1000000000  # build & sign IOTA transfer
cargo run --example status               # probe device state
cargo run --example status -- --reconnect # test reconnection (--wait <secs> to set delay)

Testing

Unit tests:

cargo test --all-features

Integration tests (Speculos emulator)

Integration tests talk to the IOTA app running in the Speculos emulator via TCP. A pre-built app ELF is included in tests/elf/.

# start the emulator
podman compose up -d

# run integration tests (must be single-threaded)
cargo test --features tcp -- --ignored --test-threads=1

# stop
podman compose down

To use a custom ELF or device model:

APP_ELF=/path/to/app.elf SPECULOS_MODEL=nanox podman compose up -d

Supported devices

Nano S, Nano S+, Nano X, Flex, Stax — detected automatically from USB product ID.

Tested with Ledger Nano X on GNU/Linux 6.17.13 (Guix) and IOTA app v1.0.1.

License

MIT