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}