ledger_sim/
lib.rs

1//! Rust wrapper for executing Speculos via local install or docker image,
2//! provided to simplify CI/CD with ledger applications.
3//!
4//! Drivers are provided for [Docker](DockerDriver) and [Local](LocalDriver)
5//! execution, with a [Generic](GenericDriver) abstraction to support
6//! runtime driver selection.
7//!
8//! ### Examples:
9//!
10//! ``` no_run
11//! # use tracing::{debug};
12//! use ledger_sim::{GenericDriver, DriverMode, Driver, Model, Options};
13//! use ledger_lib::{Device, transport::{Transport, TcpTransport, TcpInfo}, DEFAULT_TIMEOUT};
14//! use ledger_proto::apdus::{AppInfoReq, AppInfoResp};
15//!
16//! #[tokio::main]
17//! async fn main() -> anyhow::Result<()> {
18//!     // Setup driver for speculos connection
19//!     let driver = GenericDriver::new(DriverMode::Docker)?;
20//!
21//!     // Launch speculos with the provided app
22//!     let opts = Options {
23//!         model: Model::NanoX,
24//!         apdu_port: Some(1237),
25//!         ..Default::default()
26//!     };
27//!     let mut handle = driver.run("ledger-app", opts).await?;
28//!
29//!     // Setup TCP APDU transport to speculos
30//!     let mut transport = TcpTransport::new()?;
31//!     let mut device = transport.connect(TcpInfo::default()).await?;
32//!
33//!     // Fetch app info via transport
34//!     let mut buff = [0u8; 256];
35//!     let info = device.request::<AppInfoResp>(AppInfoReq{}, &mut buff, DEFAULT_TIMEOUT).await?;
36//!
37//!     // Await simulator exit or exit signal
38//!     tokio::select!(
39//!         // Await simulator task completion
40//!         _ = driver.wait(&mut handle) => {
41//!             debug!("Complete!");
42//!         }
43//!         // Exit on ctrl + c
44//!         _ = tokio::signal::ctrl_c() => {
45//!             debug!("Exit!");
46//!             driver.exit(handle).await?;
47//!         },
48//!     );
49//!
50//!     Ok(())
51//! }
52//! ```
53
54use std::collections::HashMap;
55
56use clap::Parser;
57
58use strum::{Display, EnumString, EnumVariantNames};
59
60mod drivers;
61pub use drivers::*;
62
63mod handle;
64pub use handle::*;
65
66/// Device model
67#[derive(Copy, Clone, PartialEq, Debug, EnumVariantNames, Display, EnumString)]
68#[strum(serialize_all = "lowercase")]
69pub enum Model {
70    /// Nano S
71    NanoS,
72    /// Nano S Plus
73    #[strum(serialize = "nanosplus", to_string = "nanosp")]
74    NanoSP,
75    /// Nano X
76    NanoX,
77}
78
79impl Model {
80    /// Fetch target name for a given ledger model
81    pub fn target(&self) -> &'static str {
82        match self {
83            Model::NanoS => "nanos",
84            Model::NanoSP => "nanosplus",
85            Model::NanoX => "nanox",
86        }
87    }
88}
89
90/// Simulator display mode
91#[derive(Copy, Clone, PartialEq, Debug, EnumVariantNames, Display, EnumString, clap::ValueEnum)]
92#[strum(serialize_all = "lowercase")]
93pub enum Display {
94    /// Headless mode
95    Headless,
96    /// QT based rendering
97    Qt,
98    /// Text based (command line) rendering
99    Text,
100}
101
102/// Simulator options
103#[derive(Clone, PartialEq, Debug, Parser)]
104pub struct Options {
105    /// Model to simulate
106    #[clap(long, default_value_t = Options::default().model)]
107    pub model: Model,
108
109    /// Display mode
110    #[clap(long, value_enum, default_value_t = Options::default().display)]
111    pub display: Display,
112
113    /// SDK version override (defaults based on --model)
114    #[clap(long)]
115    pub sdk: Option<String>,
116
117    /// API level override
118    #[clap(long)]
119    pub api_level: Option<String>,
120
121    /// BIP39 seed for initialisation
122    #[clap(long, env)]
123    pub seed: Option<String>,
124
125    /// Enable HTTP API port
126    #[clap(long, default_value_t = Options::default().http_port)]
127    pub http_port: u16,
128
129    /// Enable APDU TCP port (usually 1237)
130    #[clap(long, env)]
131    pub apdu_port: Option<u16>,
132
133    /// Enable debugging and wait for GDB connection (port 1234)
134    #[clap(long)]
135    pub debug: bool,
136
137    /// Speculos root (used to configure python paths if set)
138    #[clap(long, env = "SPECULOS_ROOT")]
139    pub root: Option<String>,
140
141    /// Trace syscalls
142    #[clap(long)]
143    pub trace: bool,
144}
145
146impl Default for Options {
147    fn default() -> Self {
148        Self {
149            model: Model::NanoSP,
150            display: Display::Headless,
151            sdk: None,
152            api_level: None,
153            seed: None,
154            http_port: 5000,
155            apdu_port: None,
156            debug: false,
157            root: None,
158            trace: false,
159        }
160    }
161}
162
163impl Options {
164    /// Build an argument list from [Options]
165    pub fn args(&self) -> Vec<String> {
166        // Basic args
167        let mut args = vec![
168            format!("--model={}", self.model),
169            format!("--display={}", self.display),
170            format!("--api-port={}", self.http_port),
171        ];
172
173        if let Some(seed) = &self.seed {
174            args.push(format!("--seed={seed}"));
175        }
176
177        if let Some(apdu_port) = &self.apdu_port {
178            args.push(format!("--apdu-port={apdu_port}"));
179        }
180
181        if let Some(sdk) = &self.sdk {
182            args.push(format!("--sdk={sdk}"));
183        }
184
185        if let Some(api_level) = &self.api_level {
186            args.push(format!("--apiLevel={api_level}"));
187        }
188
189        if self.debug {
190            args.push("--debug".to_string());
191        }
192
193        if self.trace {
194            args.push("-t".to_string());
195        }
196
197        args
198    }
199
200    /// Build environmental variable list from [Options]
201    pub fn env(&self) -> HashMap<String, String> {
202        let mut env = HashMap::new();
203
204        if let Some(seed) = &self.seed {
205            env.insert("SPECULOS_SEED".to_string(), seed.clone());
206        }
207
208        env
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use std::str::FromStr;
215
216    use crate::Model;
217
218    #[test]
219    fn model_name_encoding() {
220        let t = &[
221            (Model::NanoS, "nanos", "nanos"),
222            (Model::NanoSP, "nanosp", "nanosp"),
223            (Model::NanoSP, "nanosp", "nanosplus"),
224            (Model::NanoX, "nanox", "nanox"),
225        ];
226
227        for (model, enc, dec) in t {
228            assert_eq!(&model.to_string(), enc);
229            assert_eq!(Ok(*model), Model::from_str(dec));
230        }
231    }
232}