Skip to content

Commit a3a1919

Browse files
authored
test: ledger integration test (xJonathanLEI#734)
1 parent 13bc57a commit a3a1919

File tree

6 files changed

+168
-4
lines changed

6 files changed

+168
-4
lines changed

.github/workflows/test.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,21 @@ jobs:
151151
cargo build --package starknet-core \
152152
--target thumbv6m-none-eabi \
153153
--no-default-features
154+
155+
ledger:
156+
name: Ledger tests
157+
runs-on: ubuntu-latest
158+
container: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools
159+
160+
steps:
161+
- name: Checkout source code
162+
uses: actions/checkout@v4
163+
164+
- name: Install stable Rust toolchain
165+
run: |
166+
rustup toolchain install stable
167+
168+
- name: Run tests
169+
run: |
170+
cargo test --package starknet-signers \
171+
-- --ignored

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

starknet-signers/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ eth-keystore = { version = "0.5.0", default-features = false }
3030
[target.'cfg(target_arch = "wasm32")'.dependencies]
3131
getrandom = { version = "0.2.9", features = ["js"] }
3232

33+
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
34+
starknet-signers = { path = ".", features = ["ledger"] }
35+
hex = { version = "0.4.3", default-features = false, features = ["alloc", "serde"] }
36+
serde = { version = "1.0.160", features = ["derive"] }
37+
reqwest = { version = "0.12.15", default-features = false, features = ["json"] }
38+
tokio = { version = "1.27.0", features = ["full"] }
39+
3340
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
3441
wasm-bindgen-test = "0.3.50"
3542

starknet-signers/src/ledger.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ pub struct LedgerSigner {
3333

3434
/// A handle for communicating with the Ledger Starknet app.
3535
#[derive(Debug)]
36-
pub struct LedgerStarknetApp {
37-
transport: Ledger,
36+
pub struct LedgerStarknetApp<T = Ledger> {
37+
transport: T,
3838
}
3939

4040
/// Errors using the Ledger hardware wallet.
@@ -133,11 +133,21 @@ impl Signer for LedgerSigner {
133133
}
134134
}
135135

136-
impl LedgerStarknetApp {
136+
impl<T> LedgerStarknetApp<T> {
137+
/// Creates Starknet Ledger app handle using an already-initialized transport.
138+
pub fn from_transport(transport: T) -> Self {
139+
Self { transport }
140+
}
141+
}
142+
143+
impl<T> LedgerStarknetApp<T>
144+
where
145+
T: LedgerAsync,
146+
{
137147
/// Initializes the Starknet Ledger app. Attempts to find and connect to a Ledger device. The
138148
/// device must be unlocked and have the Starknet app open.
139149
pub async fn new() -> Result<Self, LedgerError> {
140-
let transport = Ledger::init().await?;
150+
let transport = T::init().await?;
141151

142152
Ok(Self { transport })
143153
}
Binary file not shown.

starknet-signers/tests/ledger.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#![cfg(not(target_arch = "wasm32"))]
2+
3+
use std::{
4+
process::{Child, Command},
5+
time::Duration,
6+
};
7+
8+
use async_trait::async_trait;
9+
use coins_ledger::{transports::LedgerAsync, APDUAnswer, APDUCommand, LedgerError};
10+
use reqwest::{Client, ClientBuilder};
11+
use semver::Version;
12+
use serde::{Deserialize, Serialize};
13+
use starknet_core::types::Felt;
14+
use starknet_signers::ledger::LedgerStarknetApp;
15+
16+
const TEST_PATH: &str = "m/2645'/1195502025'/1470455285'/0'/0'/0";
17+
18+
#[derive(Debug)]
19+
struct SpeculosTransport {
20+
process: Child,
21+
port: u16,
22+
client: Client,
23+
}
24+
25+
#[derive(Serialize, Deserialize)]
26+
struct ApduData {
27+
#[serde(with = "hex")]
28+
data: Vec<u8>,
29+
}
30+
31+
impl SpeculosTransport {
32+
async fn new(port: u16) -> Self {
33+
let mut cmd = Command::new("speculos");
34+
let process = cmd
35+
.args([
36+
"--api-port",
37+
&port.to_string(),
38+
"--apdu-port",
39+
"0",
40+
"-m",
41+
"nanox",
42+
"--display",
43+
"headless",
44+
"./test-data/ledger-app/nanox_2.4.2_2.3.1_sdk_v22.10.0",
45+
])
46+
.spawn()
47+
.expect("Unable to spawn speculos process");
48+
49+
// Wait for process to be ready (flaky)
50+
tokio::time::sleep(Duration::from_secs(1)).await;
51+
52+
Self {
53+
process,
54+
port,
55+
client: ClientBuilder::new()
56+
.timeout(Duration::from_secs(10))
57+
.build()
58+
.unwrap(),
59+
}
60+
}
61+
62+
async fn send(&self, packet: &APDUCommand) -> Result<APDUAnswer, LedgerError> {
63+
let response = self
64+
.client
65+
.post(format!("http://localhost:{}/apdu", self.port))
66+
.json(&ApduData {
67+
data: packet.serialize(),
68+
})
69+
.send()
70+
.await
71+
.unwrap();
72+
73+
let body = response.json::<ApduData>().await.unwrap();
74+
APDUAnswer::from_answer(body.data)
75+
}
76+
}
77+
78+
#[async_trait]
79+
impl LedgerAsync for SpeculosTransport {
80+
async fn init() -> Result<Self, LedgerError> {
81+
Ok(Self::new(5001).await)
82+
}
83+
84+
async fn exchange(&self, packet: &APDUCommand) -> Result<APDUAnswer, LedgerError> {
85+
self.send(packet).await
86+
}
87+
88+
fn close(self) {}
89+
}
90+
91+
impl Drop for SpeculosTransport {
92+
fn drop(&mut self) {
93+
let _ = self.process.kill();
94+
}
95+
}
96+
97+
#[tokio::test]
98+
#[ignore = "requires Speculos installation"]
99+
async fn test_get_app_version() {
100+
let app = LedgerStarknetApp::from_transport(SpeculosTransport::new(5001).await);
101+
let version = app.get_version().await.unwrap();
102+
103+
assert_eq!(version, Version::new(2, 3, 1));
104+
}
105+
106+
#[tokio::test]
107+
#[ignore = "requires Speculos installation"]
108+
async fn test_get_public_key_headless() {
109+
let app = LedgerStarknetApp::from_transport(SpeculosTransport::new(5002).await);
110+
let public_key = app
111+
.get_public_key(TEST_PATH.parse().unwrap(), false)
112+
.await
113+
.unwrap();
114+
115+
assert_eq!(
116+
public_key.scalar(),
117+
Felt::from_hex_unchecked(
118+
"0x07427aa749c4fc98a5bf76f037eb3c61e7b4793b576a72d45a4b52c5ded997f2"
119+
)
120+
);
121+
}

0 commit comments

Comments
 (0)