speculos 0.1.0

Rust wrapper for the Ledger Speculos emulator
Documentation

speculos

Rust wrapper for the Ledger Speculos device emulator.

Spawns the speculos Python binary as a child process, exposes its REST API for reading the screen, and drives input (buttons on Nano, touch on Stax/Flex).

Prerequisites

You must build and install Speculos yourself. Follow the instructions in the upstream repo:

After building, the speculos command must be available on your PATH. This crate does not bundle, download, or otherwise provide the emulator binary.

You also need an app .elf to run inside the emulator (typically built from a Ledger app project, or downloaded from a Ledger app release).

Usage

Launch with auto-assigned ports

use speculos::{Speculos, Model, Button};
use std::path::Path;

let sim = Speculos::launch(Path::new("/path/to/app.elf"), Model::NanoX)?;

// Send your APDU traffic to sim.apdu_addr() with whatever transport you use.
let apdu = sim.apdu_addr();

// Drive the UI.
sim.press(Button::Right)?;
sim.press(Button::Both)?;
# Ok::<(), speculos::Error>(())

Specify ports, seed, etc.

use speculos::{Speculos, Model, SpawnOptions};
use std::net::SocketAddr;
use std::path::Path;

let sim = Speculos::launch_with(
    Path::new("/path/to/app.elf"),
    Model::Stax,
    SpawnOptions {
        seed: Some("abandon abandon abandon abandon abandon abandon abandon abandon
abandon abandon abandon about".into()),
        apdu: Some("127.0.0.1:9999".parse::<SocketAddr>().unwrap()),
        api: Some("127.0.0.1:5000".parse::<SocketAddr>().unwrap()),
    },
)?;
# Ok::<(), speculos::Error>(())

For a raw hex seed, prefix with hex: (Speculos's own convention):

# use speculos::SpawnOptions;
let opts = SpawnOptions {
    seed: Some("hex:0123456789abcdef...".into()),
    ..Default::default()
};

Read screen text

# use speculos::{Speculos, Model};
# use std::path::Path;
# use std::time::Duration;
# let sim = Speculos::launch(Path::new("/x"), Model::NanoX)?;
use regex::Regex;

let ev = sim.wait_for(&Regex::new("Address").unwrap(), Duration::from_secs(5))?;
println!("found: {}", ev.text);
# Ok::<(), speculos::Error>(())

Touchscreen input (Stax / Flex)

# use speculos::{Speculos, Model};
# use std::path::Path;
# let sim = Speculos::launch(Path::new("/x"), Model::Stax)?;
sim.tap(200, 600)?;
sim.drag((200, 500), (200, 100))?;
# Ok::<(), speculos::Error>(())

Screenshots

# use speculos::{Speculos, Model};
# use std::path::Path;
# let sim = Speculos::launch(Path::new("/x"), Model::NanoX)?;
sim.screenshot_to_file(Path::new("screen.png"))?;
# Ok::<(), speculos::Error>(())

Attach to an already-running speculos

use speculos::{Speculos, Model};

let sim = Speculos::attach(
    Model::NanoX,
    "127.0.0.1:9999".parse().unwrap(),
    "127.0.0.1:5000".parse().unwrap(),
)?;
// Drop will NOT kill the external process.
# Ok::<(), speculos::Error>(())

Configure from environment

Speculos::from_env() reads:

Variable Required Notes
SPECULOS_ELF yes Path to the app .elf.
SPECULOS_MODEL yes nanox/nanosp/stax/flex (case-insensitive).
SPECULOS_BIN no Use a non default speculos executable
SPECULOS_APDU no host:port for APDU socket. Free if unset.
SPECULOS_API no host:port for REST API. Free if unset.
SPECULOS_SEED no Forwarded to --seed

SPECULOS_APDU and SPECULOS_API must both be set or both unset.

Override the speculos binary

By default the crate spawns speculos from PATH. To use a different executable (absolute path, alternate name, wrapper script), set SPECULOS_BIN. This applies to every launch entry point.

Models

Variant Input Screen
Model::NanoX buttons 128x64
Model::NanoSP buttons 128x64
Model::Stax touchscreen 400x672
Model::Flex touchscreen 480x600

press() is buttons-only; tap(), drag(), press_at(), release_at(), move_to() are touchscreen-only. Calling the wrong family returns Error::TouchscreenOnly or Error::ButtonsOnly.

Lifetime

Speculos::launch* owns the child process. Dropping the handle kills it. Speculos::attach does not, so dropping leaves the process alive.

If port selection races (another process grabs the port between pick_free_port and Speculos's bind), launch_with retries up to three times with fresh ports, as long as at least one port was auto-picked.