diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c0634bb16..446d3e1aba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -400,6 +400,15 @@ jobs: - uses: taiki-e/install-action@v2 with: tool: nextest@0.9.98 + - name: Build Starknet Ledger App (Nano X) + run: | + git clone --depth 1 --branch nanox_2.7.0_2.4.0_sdk_v26.0.2 \ + https://github.com/LedgerHQ/app-starknet /tmp/app-starknet + cd /tmp/app-starknet/starknet + cargo +${RUST_NIGHTLY} ledger build nanox + mkdir -p ${GITHUB_WORKSPACE}/crates/sncast/tests/data/ledger-app + cp target/nanox/release/starknet \ + ${GITHUB_WORKSPACE}/crates/sncast/tests/data/ledger-app/nanox.elf - name: Run tests (nextest) run: cargo nextest run --no-fail-fast --cargo-profile ci -p sncast --features ledger-emulator ledger --run-ignored all diff --git a/Cargo.lock b/Cargo.lock index 4fc460dad8..5fcb6a75eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4045,9 +4045,6 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -dependencies = [ - "serde", -] [[package]] name = "hidapi-rusb" @@ -6444,38 +6441,6 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "reqwest" version = "0.13.2" @@ -7504,7 +7469,7 @@ dependencies = [ "promptly", "rand 0.8.6", "regex", - "reqwest 0.13.2", + "reqwest", "rpassword", "runtime", "scarb-api", @@ -7520,7 +7485,6 @@ dependencies = [ "shared", "shellexpand", "snapbox", - "speculos-client", "starknet-rust", "starknet-rust-crypto 0.9.0", "starknet-types-core", @@ -7562,17 +7526,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "speculos-client" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aab48e66d675276b5970b3243e9638053a9ff8d693d4a0f9c2514e83dbbc8cab" -dependencies = [ - "hex", - "reqwest 0.12.28", - "serde", -] - [[package]] name = "spin" version = "0.9.8" @@ -7830,7 +7783,7 @@ dependencies = [ "flate2", "getrandom 0.2.17", "log", - "reqwest 0.13.2", + "reqwest", "serde", "serde_json", "serde_with", @@ -8919,7 +8872,7 @@ dependencies = [ "lazy_static 1.5.0", "log", "regex", - "reqwest 0.13.2", + "reqwest", "scarb-metadata", "semver", "serde", diff --git a/Cargo.toml b/Cargo.toml index 389c0d83d6..af1863d15d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,7 +122,6 @@ serde_path_to_error = "0.1.20" serde_with = "3.18.0" wiremock = "0.6.3" coins-ledger = "0.13.0" -speculos-client = "0.1.2" const-hex = "1.18.1" indicatif = "0.18.3" shell-words = "1.1.0" diff --git a/crates/sncast/Cargo.toml b/crates/sncast/Cargo.toml index 6ed5768655..0f0e001ae8 100644 --- a/crates/sncast/Cargo.toml +++ b/crates/sncast/Cargo.toml @@ -82,7 +82,6 @@ wiremock.workspace = true docs = { workspace = true, features = ["testing"] } shared = { path = "../shared", features = ["testing"] } packages_validation = { path = "../testing/packages_validation" } -speculos-client.workspace = true [features] default = [] diff --git a/crates/sncast/tests/data/ledger-app/nanox#strk#0.25.13.elf b/crates/sncast/tests/data/ledger-app/nanox#strk#0.25.13.elf deleted file mode 100755 index 25217afff5..0000000000 Binary files a/crates/sncast/tests/data/ledger-app/nanox#strk#0.25.13.elf and /dev/null differ diff --git a/crates/sncast/tests/docs_snippets/ledger.rs b/crates/sncast/tests/docs_snippets/ledger.rs index 702eb06242..4edcf8c99a 100644 --- a/crates/sncast/tests/docs_snippets/ledger.rs +++ b/crates/sncast/tests/docs_snippets/ledger.rs @@ -1,4 +1,4 @@ -use crate::e2e::ledger::{automation, setup_speculos}; +use crate::e2e::ledger::{automation, set_automation, setup_speculos}; use crate::helpers::constants::URL; use crate::helpers::runner::runner; use docs::snippet::SnippetType; @@ -12,20 +12,21 @@ use tempfile::TempDir; const DOCS_SNIPPETS_PORT_BASE: u16 = 4006; -async fn setup_speculos_automation(client: &Arc, args: &[&str]) { +async fn setup_speculos_automation( + client: &Arc, + args: &[&str], +) { if args.contains(&"get-public-key") && !args.contains(&"--no-display") { - client - .automation(&[automation::APPROVE_PUBLIC_KEY]) - .await - .unwrap(); + set_automation(client, &[automation::APPROVE_PUBLIC_KEY]).await; } else if args.contains(&"sign-hash") { - client - .automation(&[ + set_automation( + client, + &[ automation::ENABLE_BLIND_SIGN, automation::APPROVE_BLIND_SIGN_HASH, - ]) - .await - .unwrap(); + ], + ) + .await; } } diff --git a/crates/sncast/tests/docs_snippets/mod.rs b/crates/sncast/tests/docs_snippets/mod.rs index 68725cb2e7..89b1cd79ae 100644 --- a/crates/sncast/tests/docs_snippets/mod.rs +++ b/crates/sncast/tests/docs_snippets/mod.rs @@ -1,2 +1,3 @@ +#[cfg(feature = "ledger-emulator")] pub mod ledger; pub mod validation; diff --git a/crates/sncast/tests/e2e/ledger/account.rs b/crates/sncast/tests/e2e/ledger/account.rs index 951ea6a493..e0c0b43ded 100644 --- a/crates/sncast/tests/e2e/ledger/account.rs +++ b/crates/sncast/tests/e2e/ledger/account.rs @@ -1,8 +1,9 @@ use std::fs; +use super::speculos::AutomationRule; use crate::e2e::ledger::{ BRAAVOS_LEDGER_PATH, LEDGER_ACCOUNT_NAME, LEDGER_PUBLIC_KEY, OZ_LEDGER_PATH, READY_LEDGER_PATH, - TEST_LEDGER_PATH, TEST_LEDGER_PATH_STORED, automation, setup_speculos, + TEST_LEDGER_PATH, TEST_LEDGER_PATH_STORED, automation, set_automation, setup_speculos, }; use crate::helpers::constants::URL; use crate::helpers::fixtures::mint_token; @@ -16,19 +17,17 @@ use shared::test_utils::output_assert::{assert_stderr_contains, assert_stdout_co use snapbox::assert_data_eq; use sncast::helpers::account::load_accounts; use sncast::helpers::constants::{BRAAVOS_CLASS_HASH, OZ_CLASS_HASH, READY_CLASS_HASH}; -use speculos_client::AutomationRule; use tempfile::tempdir; use test_case::test_case; -#[test_case("oz", "open_zeppelin", OZ_CLASS_HASH.into_hex_string(), 6001, &[automation::APPROVE_PUBLIC_KEY]; "oz_account_type")] -#[test_case("ready", "ready", READY_CLASS_HASH.into_hex_string(), 6002, &[automation::APPROVE_PUBLIC_KEY]; "ready_account_type")] +#[test_case("oz", "open_zeppelin", OZ_CLASS_HASH.into_hex_string(), 6001, &[]; "oz_account_type")] +#[test_case("ready", "ready", READY_CLASS_HASH.into_hex_string(), 6002, &[]; "ready_account_type")] // Braavos calls sign_hash twice during fee estimation (tx_hash + aux_hash) because // is_signer_interactive() always returns false — see BraavosAccountFactory::is_signer_interactive. -// That means we need ENABLE_BLIND_SIGN + two APPROVE_BLIND_SIGN_HASH after the public key approval. +// That means we need ENABLE_BLIND_SIGN + two APPROVE_BLIND_SIGN_HASH during creation. #[test_case( "braavos", "braavos", BRAAVOS_CLASS_HASH.into_hex_string(), 6003, &[ - automation::APPROVE_PUBLIC_KEY, automation::ENABLE_BLIND_SIGN, automation::APPROVE_BLIND_SIGN_HASH, // tx_hash automation::APPROVE_BLIND_SIGN_HASH, // aux_hash @@ -42,12 +41,12 @@ async fn test_create_ledger_account( saved_type: &str, class_hash: String, port: u16, - automations: &[speculos_client::AutomationRule<'static>], + automations: &[AutomationRule<'static>], ) { let (client, url) = setup_speculos(port); let tempdir = tempdir().unwrap(); - client.automation(automations).await.unwrap(); + set_automation(&client, automations).await; let output = runner(&[ "--accounts-file", @@ -115,14 +114,9 @@ async fn test_create_ledger_account( #[tokio::test] #[ignore = "requires Speculos installation"] async fn test_create_ledger_account_add_profile() { - let (client, url) = setup_speculos(6004); + let (_client, url) = setup_speculos(6004); let tempdir = copy_config_to_tempdir("tests/data/files/snfoundry_correct.toml", None); - client - .automation(&[automation::APPROVE_PUBLIC_KEY]) - .await - .unwrap(); - let output = runner(&[ "--accounts-file", "accounts.json", @@ -159,10 +153,9 @@ async fn test_create_ledger_account_add_profile() { #[test_case( "oz", OZ_LEDGER_PATH, 6005, - // create: public key only (OZ skips signing during fee estimation) + // create: no confirmation needed // deploy: enable blind sign + 1 sign_hash &[ - automation::APPROVE_PUBLIC_KEY, automation::ENABLE_BLIND_SIGN, automation::APPROVE_BLIND_SIGN_HASH, ]; @@ -170,10 +163,9 @@ async fn test_create_ledger_account_add_profile() { )] #[test_case( "ready", READY_LEDGER_PATH, 6006, - // create: public key only (Ready skips signing during fee estimation) + // create: no confirmation needed // deploy: enable blind sign + 1 sign_hash &[ - automation::APPROVE_PUBLIC_KEY, automation::ENABLE_BLIND_SIGN, automation::APPROVE_BLIND_SIGN_HASH, ]; @@ -181,10 +173,9 @@ async fn test_create_ledger_account_add_profile() { )] #[test_case( "braavos", BRAAVOS_LEDGER_PATH, 6007, - // create: public key + enable blind sign + 2x sign_hash (tx_hash + aux_hash) + // create: enable blind sign + 2x sign_hash (tx_hash + aux_hash) // deploy: 2x sign_hash again (tx_hash + aux_hash), blind sign already enabled &[ - automation::APPROVE_PUBLIC_KEY, automation::ENABLE_BLIND_SIGN, automation::APPROVE_BLIND_SIGN_HASH, // create: tx_hash automation::APPROVE_BLIND_SIGN_HASH, // create: aux_hash @@ -203,7 +194,7 @@ async fn test_deploy_ledger_account( ) { let (client, url) = setup_speculos(port); - client.automation(automations).await.unwrap(); + set_automation(&client, automations).await; let tempdir = tempdir().unwrap(); let accounts_file = tempdir.path().join("accounts.json"); diff --git a/crates/sncast/tests/e2e/ledger/basic.rs b/crates/sncast/tests/e2e/ledger/basic.rs index 54e21ee6e5..2ce4507d54 100644 --- a/crates/sncast/tests/e2e/ledger/basic.rs +++ b/crates/sncast/tests/e2e/ledger/basic.rs @@ -1,4 +1,4 @@ -use crate::e2e::ledger::{TEST_LEDGER_PATH, automation, setup_speculos}; +use crate::e2e::ledger::{TEST_LEDGER_PATH, automation, set_automation, setup_speculos}; use crate::helpers::runner::runner; use shared::test_utils::output_assert::{assert_stderr_contains, assert_stdout_contains}; @@ -12,7 +12,7 @@ async fn test_get_app_version() { .assert() .success(); - assert_stdout_contains(output, "App Version: 2.3.4"); + assert_stdout_contains(output, "App Version: 2.4.0"); } #[tokio::test] @@ -63,13 +63,14 @@ async fn test_get_public_key_with_confirmation() { async fn test_sign_hash() { let (client, url) = setup_speculos(4004); - client - .automation(&[ + set_automation( + &client, + &[ automation::ENABLE_BLIND_SIGN, automation::APPROVE_BLIND_SIGN_HASH, - ]) - .await - .unwrap(); + ], + ) + .await; let output = runner(&[ "ledger", diff --git a/crates/sncast/tests/e2e/ledger/mod.rs b/crates/sncast/tests/e2e/ledger/mod.rs index 08176d0701..d746e7fbe7 100644 --- a/crates/sncast/tests/e2e/ledger/mod.rs +++ b/crates/sncast/tests/e2e/ledger/mod.rs @@ -1,6 +1,5 @@ #![cfg(not(target_arch = "wasm32"))] -use std::env; use std::{borrow::Cow, sync::Arc}; use crate::helpers::constants::URL; @@ -13,16 +12,15 @@ use sncast::helpers::braavos::BraavosAccountFactory; use sncast::helpers::constants::{ BRAAVOS_BASE_ACCOUNT_CLASS_HASH, BRAAVOS_CLASS_HASH, OZ_CLASS_HASH, READY_CLASS_HASH, }; -use sncast::helpers::ledger::{DerivationPathParser, create_ledger_app}; +use sncast::helpers::ledger::{DerivationPathParser, SncastLedgerTransport}; use sncast::response::ui::UI; -use speculos_client::{ - AutomationAction, AutomationCondition, AutomationRule, Button, DeviceModel, SpeculosClient, -}; +use speculos::{AutomationAction, AutomationCondition, AutomationRule, Button, SpeculosClient}; use starknet_rust::accounts::{AccountFactory, ArgentAccountFactory, OpenZeppelinAccountFactory}; use starknet_rust::core::types::{BlockId, BlockTag}; use starknet_rust::providers::Provider; use starknet_rust::providers::jsonrpc::{HttpTransport, JsonRpcClient}; use starknet_rust::signers::LedgerSigner; +use starknet_rust::signers::ledger::LedgerStarknetApp; use starknet_types_core::felt::Felt; use tempfile::TempDir; use url::Url; @@ -30,6 +28,7 @@ use url::Url; mod account; mod basic; mod network; +pub(crate) mod speculos; pub(crate) const OZ_LEDGER_PATH: &str = "m//starknet'/sncast'/0'/0'/0"; pub(crate) const READY_LEDGER_PATH: &str = "m//starknet'/sncast'/0'/1'/0"; @@ -38,8 +37,7 @@ pub(crate) const TEST_LEDGER_PATH: &str = OZ_LEDGER_PATH; pub(crate) const TEST_LEDGER_PATH_STORED: &str = "m/2645'/1195502025'/355113700'/0'/0'/0"; -// TODO(#4221): Update to latest version and build in workflow -const APP_PATH: &str = "tests/data/ledger-app/nanox#strk#0.25.13.elf"; +const APP_PATH: &str = "tests/data/ledger-app/nanox.elf"; pub(crate) const LEDGER_PUBLIC_KEY: &str = "0x51f3e99d539868d8f45ca705ad6f75e68229a6037a919b15216b4e92a4d6d8"; @@ -47,11 +45,24 @@ pub(crate) const LEDGER_PUBLIC_KEY: &str = pub(crate) const LEDGER_ACCOUNT_NAME: &str = "my_ledger"; pub(crate) fn setup_speculos(port: u16) -> (Arc, String) { - let client = Arc::new(SpeculosClient::new(DeviceModel::Nanox, port, APP_PATH).unwrap()); + let client = Arc::new(SpeculosClient::new(port, APP_PATH).unwrap()); let url = format!("http://127.0.0.1:{port}"); (client, url) } +/// Sets automation rules and, when `ENABLE_BLIND_SIGN` is among them, presses RIGHT so the +/// blind-sign flow advances immediately. +pub(crate) async fn set_automation( + client: &SpeculosClient, + rules: &[speculos::AutomationRule<'static>], +) { + client.automation(rules).await.unwrap(); + let needs_blind_sign = rules.iter().any(|r| r == &automation::ENABLE_BLIND_SIGN); + if needs_blind_sign { + client.click_button(Button::Right).await.unwrap(); + } +} + fn create_jsonrpc_client() -> JsonRpcClient { JsonRpcClient::new(HttpTransport::new(Url::parse(URL).unwrap())) } @@ -114,15 +125,8 @@ pub(crate) async fn deploy_ledger_account_of_type( parsed.print_warnings(&ui); let parsed_path = parsed.path; - // SAFETY: All tests share the same devnet instance, so even if a race condition causes - // `set_var` to race with another test using a different ledger emulator URL, the account - // deployment will still reach the correct devnet and remain accessible to the original test. - // The ledger emulator URL has no effect on account deployment when using the emulator. - unsafe { - env::set_var("LEDGER_EMULATOR_URL", speculos_url); - }; - - let app = create_ledger_app().await.unwrap(); + let transport = SncastLedgerTransport::new(speculos_url.to_string()).unwrap(); + let app = LedgerStarknetApp::from_transport(transport); let ledger_signer = LedgerSigner::new_with_app(parsed_path, app).unwrap(); let chain_id = starknet_rust::core::chain_id::SEPOLIA; @@ -168,23 +172,16 @@ pub(crate) async fn deploy_ledger_account_of_type( pub(crate) mod automation { use super::*; + // Screen flow: "Public Key (1/2)" -> Right -> "Public Key (2/2)" -> Right -> "Approve" -> Both + // Trigger fires on "Public Key (1/2)" and navigates to Approve, then confirms. pub(crate) const APPROVE_PUBLIC_KEY: AutomationRule<'static> = AutomationRule { - text: Some(Cow::Borrowed("Confirm Public Key")), + text: Some(Cow::Borrowed("Public Key (1/2)")), regexp: None, x: None, y: None, conditions: &[], actions: &[ - // Press right - AutomationAction::Button { - button: Button::Right, - pressed: true, - }, - AutomationAction::Button { - button: Button::Right, - pressed: false, - }, - // Press right + // Right (to "Public Key (2/2)") AutomationAction::Button { button: Button::Right, pressed: true, @@ -193,7 +190,7 @@ pub(crate) mod automation { button: Button::Right, pressed: false, }, - // Press right + // Right (to "Approve") AutomationAction::Button { button: Button::Right, pressed: true, @@ -202,7 +199,7 @@ pub(crate) mod automation { button: Button::Right, pressed: false, }, - // Press both + // Both (confirm) AutomationAction::Button { button: Button::Left, pressed: true, @@ -222,9 +219,13 @@ pub(crate) mod automation { ], }; + // Screen flow: navigate to "App settings" (via press_button RIGHT from home) -> + // Both (enter settings, shows "Blind signing" toggle OFF) -> Both (toggle ON). + // The trigger is "App settings" so the caller must press RIGHT after registering this + // rule to navigate away from the home screen and land on "App settings". pub(crate) const ENABLE_BLIND_SIGN: AutomationRule<'static> = AutomationRule { - text: None, - regexp: Some(Cow::Borrowed("^(S)?tarknet$")), + text: Some(Cow::Borrowed("App settings")), + regexp: None, x: None, y: None, conditions: &[AutomationCondition { @@ -232,25 +233,7 @@ pub(crate) mod automation { value: false, }], actions: &[ - // Right - AutomationAction::Button { - button: Button::Right, - pressed: true, - }, - AutomationAction::Button { - button: Button::Right, - pressed: false, - }, - // Right - AutomationAction::Button { - button: Button::Right, - pressed: true, - }, - AutomationAction::Button { - button: Button::Right, - pressed: false, - }, - // Both + // Both (enter settings, shows "Blind signing" toggle OFF) AutomationAction::Button { button: Button::Left, pressed: true, @@ -267,7 +250,7 @@ pub(crate) mod automation { button: Button::Right, pressed: false, }, - // Both + // Both (toggle blind signing ON) AutomationAction::Button { button: Button::Left, pressed: true, @@ -284,15 +267,6 @@ pub(crate) mod automation { button: Button::Right, pressed: false, }, - // Left - AutomationAction::Button { - button: Button::Left, - pressed: true, - }, - AutomationAction::Button { - button: Button::Left, - pressed: false, - }, // Mark as done AutomationAction::Setbool { varname: Cow::Borrowed("blind_enabled"), @@ -301,10 +275,12 @@ pub(crate) mod automation { ], }; + // Screen flow: "Blind signing ahead." -> Both -> "Review hash" -> Right -> "Hash (1/2)" -> + // Right -> "Hash (2/2)" -> Right -> "Sign Hash ?" -> Both -> "Message signed" /// Must be used with [`ENABLE_BLIND_SIGN`]. pub(crate) const APPROVE_BLIND_SIGN_HASH: AutomationRule<'static> = AutomationRule { text: None, - regexp: Some(Cow::Borrowed("^Cancel$")), + regexp: Some(Cow::Borrowed("^Blind signing ahead")), x: None, y: None, conditions: &[AutomationCondition { @@ -312,16 +288,7 @@ pub(crate) mod automation { value: true, }], actions: &[ - // Right - AutomationAction::Button { - button: Button::Right, - pressed: true, - }, - AutomationAction::Button { - button: Button::Right, - pressed: false, - }, - // Both + // Both (accept "Blind signing ahead" warning) AutomationAction::Button { button: Button::Left, pressed: true, @@ -338,7 +305,7 @@ pub(crate) mod automation { button: Button::Right, pressed: false, }, - // Right + // Right (to "Hash (1/2)") AutomationAction::Button { button: Button::Right, pressed: true, @@ -347,7 +314,7 @@ pub(crate) mod automation { button: Button::Right, pressed: false, }, - // Right + // Right (to "Hash (2/2)") AutomationAction::Button { button: Button::Right, pressed: true, @@ -356,7 +323,7 @@ pub(crate) mod automation { button: Button::Right, pressed: false, }, - // Right + // Right (to "Sign Hash ?") AutomationAction::Button { button: Button::Right, pressed: true, @@ -365,7 +332,7 @@ pub(crate) mod automation { button: Button::Right, pressed: false, }, - // Both + // Both (confirm) AutomationAction::Button { button: Button::Left, pressed: true, diff --git a/crates/sncast/tests/e2e/ledger/network.rs b/crates/sncast/tests/e2e/ledger/network.rs index d5f7a7877e..cce28c196d 100644 --- a/crates/sncast/tests/e2e/ledger/network.rs +++ b/crates/sncast/tests/e2e/ledger/network.rs @@ -1,7 +1,7 @@ use crate::e2e::ledger::{ BRAAVOS_LEDGER_PATH, LEDGER_ACCOUNT_NAME, OZ_LEDGER_PATH, TEST_LEDGER_PATH, automation, create_temp_accounts_json, deploy_ledger_account, deploy_ledger_account_of_type, - setup_speculos, + set_automation, setup_speculos, }; use crate::helpers::constants::{ CONSTRUCTOR_WITH_PARAMS_CONTRACT_CLASS_HASH_SEPOLIA, CONTRACTS_DIR, @@ -24,13 +24,14 @@ use test_case::test_case; async fn test_ledger_invoke_happy_case() { let (client, url) = setup_speculos(5001); - client - .automation(&[ + set_automation( + &client, + &[ automation::ENABLE_BLIND_SIGN, automation::APPROVE_BLIND_SIGN_HASH, - ]) - .await - .unwrap(); + ], + ) + .await; let account_address = deploy_ledger_account(&url, TEST_LEDGER_PATH, Felt::from(5001_u32)).await; let tempdir = create_temp_accounts_json(account_address); @@ -71,13 +72,14 @@ async fn test_ledger_invoke_happy_case() { async fn test_ledger_invoke_with_wait() { let (client, url) = setup_speculos(5002); - client - .automation(&[ + set_automation( + &client, + &[ automation::ENABLE_BLIND_SIGN, automation::APPROVE_BLIND_SIGN_HASH, - ]) - .await - .unwrap(); + ], + ) + .await; let account_address = deploy_ledger_account(&url, TEST_LEDGER_PATH, Felt::from(5002_u32)).await; let tempdir = create_temp_accounts_json(account_address); @@ -120,13 +122,14 @@ async fn test_ledger_invoke_with_wait() { async fn test_ledger_deploy_happy_case() { let (client, url) = setup_speculos(5004); - client - .automation(&[ + set_automation( + &client, + &[ automation::ENABLE_BLIND_SIGN, automation::APPROVE_BLIND_SIGN_HASH, - ]) - .await - .unwrap(); + ], + ) + .await; let account_address = deploy_ledger_account(&url, TEST_LEDGER_PATH, Felt::from(5004_u32)).await; let tempdir = create_temp_accounts_json(account_address); @@ -166,13 +169,14 @@ async fn test_ledger_deploy_happy_case() { async fn test_ledger_deploy_with_constructor() { let (client, url) = setup_speculos(5005); - client - .automation(&[ + set_automation( + &client, + &[ automation::ENABLE_BLIND_SIGN, automation::APPROVE_BLIND_SIGN_HASH, - ]) - .await - .unwrap(); + ], + ) + .await; let account_address = deploy_ledger_account(&url, TEST_LEDGER_PATH, Felt::from(6001_u32)).await; let tempdir = create_temp_accounts_json(account_address); @@ -214,13 +218,14 @@ async fn test_ledger_deploy_with_constructor() { async fn test_ledger_declare() { let (client, url) = setup_speculos(5006); - client - .automation(&[ + set_automation( + &client, + &[ automation::ENABLE_BLIND_SIGN, automation::APPROVE_BLIND_SIGN_HASH, - ]) - .await - .unwrap(); + ], + ) + .await; let account_address = deploy_ledger_account(&url, TEST_LEDGER_PATH, Felt::from(5006_u32)).await; @@ -273,14 +278,15 @@ async fn test_ledger_import_and_invoke( ) { let (client, url) = setup_speculos(port); - client - .automation(&[ + set_automation( + &client, + &[ automation::ENABLE_BLIND_SIGN, automation::APPROVE_BLIND_SIGN_HASH, automation::APPROVE_PUBLIC_KEY, - ]) - .await - .unwrap(); + ], + ) + .await; let account_id_path_buf; let deploy_path = match (ledger_path, ledger_account_id) { @@ -361,13 +367,14 @@ async fn test_ledger_import_and_invoke( async fn test_ledger_multicall() { let (client, url) = setup_speculos(5007); - client - .automation(&[ + set_automation( + &client, + &[ automation::ENABLE_BLIND_SIGN, automation::APPROVE_BLIND_SIGN_HASH, - ]) - .await - .unwrap(); + ], + ) + .await; let account_address = deploy_ledger_account(&url, TEST_LEDGER_PATH, Felt::from(5007_u32)).await; let tempdir = create_temp_accounts_json(account_address); diff --git a/crates/sncast/tests/e2e/ledger/speculos.rs b/crates/sncast/tests/e2e/ledger/speculos.rs new file mode 100644 index 0000000000..d391d03101 --- /dev/null +++ b/crates/sncast/tests/e2e/ledger/speculos.rs @@ -0,0 +1,216 @@ +use std::{ + borrow::Cow, + io::{BufRead, BufReader}, + path::Path, + process::{Child, Command, Stdio}, + time::Duration, +}; + +use reqwest::{Client, ClientBuilder}; +use serde::{Serialize, ser::SerializeSeq}; + +#[derive(Debug)] +pub struct SpeculosClient { + process: Child, + port: u16, + client: Client, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct AutomationRule<'a> { + #[serde(skip_serializing_if = "Option::is_none")] + pub text: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub regexp: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub x: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub y: Option, + pub conditions: &'a [AutomationCondition<'a>], + pub actions: &'a [AutomationAction<'a>], +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AutomationAction<'a> { + Button { button: Button, pressed: bool }, + Setbool { varname: Cow<'a, str>, value: bool }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AutomationCondition<'a> { + pub varname: Cow<'a, str>, + pub value: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Button { + Left, + Right, +} + +#[derive(Debug)] +pub enum SpeculosError { + IoError(std::io::Error), + ReqwestError(reqwest::Error), +} + +#[derive(Serialize)] +struct PostAutomationRequest<'a> { + version: u32, + rules: &'a [AutomationRule<'a>], +} + +impl SpeculosClient { + pub fn new>(port: u16, app: P) -> Result { + Self::new_with_timeout(port, app, Duration::from_secs(10)) + } + + pub fn new_with_timeout>( + port: u16, + app: P, + timeout: Duration, + ) -> Result { + let mut process = Command::new("speculos") + .args([ + "--api-port", + &port.to_string(), + "--apdu-port", + "0", + "-m", + "nanox", + "--display", + "headless", + &app.as_ref().display().to_string(), + ]) + .stderr(Stdio::piped()) + .spawn()?; + + if let Some(stderr) = process.stderr.take() { + let reader = BufReader::new(stderr); + for line in reader.lines().map_while(Result::ok) { + if line.contains("launcher: using default app name & version") + || line.contains("[*] Env app version:") + { + break; + } + } + } + + let addr = format!("127.0.0.1:{port}"); + let deadline = std::time::Instant::now() + timeout; + 'poll: while std::time::Instant::now() < deadline { + if let Ok(mut stream) = std::net::TcpStream::connect(&addr) { + use std::io::{Read, Write}; + let req = format!("GET /events HTTP/1.0\r\nHost: localhost:{port}\r\n\r\n"); + if stream.write_all(req.as_bytes()).is_ok() { + let mut resp = String::new(); + let _ = stream.read_to_string(&mut resp); + if resp.contains("\"text\"") { + break 'poll; + } + } + } + std::thread::sleep(Duration::from_millis(100)); + } + + Ok(Self { + process, + port, + client: ClientBuilder::new().timeout(timeout).build().unwrap(), + }) + } + + pub async fn automation(&self, rules: &[AutomationRule<'_>]) -> Result<(), SpeculosError> { + let response = self + .client + .post(format!("http://localhost:{}/automation", self.port)) + .json(&PostAutomationRequest { version: 1, rules }) + .send() + .await?; + response.error_for_status()?; + Ok(()) + } + + pub async fn click_button(&self, button: Button) -> Result<(), SpeculosError> { + #[derive(serde::Serialize)] + struct ButtonRequest { + action: &'static str, + } + let name = match button { + Button::Left => "left", + Button::Right => "right", + }; + let response = self + .client + .post(format!("http://localhost:{}/button/{name}", self.port)) + .json(&ButtonRequest { + action: "press-and-release", + }) + .send() + .await?; + response.error_for_status()?; + Ok(()) + } +} + +impl Drop for SpeculosClient { + fn drop(&mut self) { + let _ = self.process.kill(); + } +} + +impl Serialize for AutomationCondition<'_> { + fn serialize(&self, serializer: S) -> Result { + let mut seq = serializer.serialize_seq(Some(2))?; + seq.serialize_element(&self.varname)?; + seq.serialize_element(&self.value)?; + seq.end() + } +} + +impl Serialize for AutomationAction<'_> { + fn serialize(&self, serializer: S) -> Result { + match self { + Self::Button { button, pressed } => { + let mut seq = serializer.serialize_seq(Some(3))?; + seq.serialize_element("button")?; + seq.serialize_element(&match button { + Button::Left => 1, + Button::Right => 2, + })?; + seq.serialize_element(pressed)?; + seq.end() + } + Self::Setbool { varname, value } => { + let mut seq = serializer.serialize_seq(Some(3))?; + seq.serialize_element("setbool")?; + seq.serialize_element(varname)?; + seq.serialize_element(value)?; + seq.end() + } + } + } +} + +impl From for SpeculosError { + fn from(value: std::io::Error) -> Self { + Self::IoError(value) + } +} + +impl From for SpeculosError { + fn from(value: reqwest::Error) -> Self { + Self::ReqwestError(value) + } +} + +impl std::fmt::Display for SpeculosError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::IoError(e) => write!(f, "{e}"), + Self::ReqwestError(e) => write!(f, "{e}"), + } + } +} + +impl std::error::Error for SpeculosError {} diff --git a/crates/sncast/tests/e2e/mod.rs b/crates/sncast/tests/e2e/mod.rs index c448d22e00..31d572d2a5 100644 --- a/crates/sncast/tests/e2e/mod.rs +++ b/crates/sncast/tests/e2e/mod.rs @@ -9,6 +9,7 @@ mod deploy; mod devnet_accounts; mod fee; mod invoke; +#[cfg(feature = "ledger-emulator")] pub(crate) mod ledger; mod main_tests; mod multicall; diff --git a/docs/src/appendix/sncast/ledger/app-version.md b/docs/src/appendix/sncast/ledger/app-version.md index 6ab9f86c1e..377834bd43 100644 --- a/docs/src/appendix/sncast/ledger/app-version.md +++ b/docs/src/appendix/sncast/ledger/app-version.md @@ -15,7 +15,7 @@ $ sncast ledger app-version Output: ```shell -App Version: 2.3.4 +App Version: 2.4.0 ```
diff --git a/docs/src/starknet/ledger.md b/docs/src/starknet/ledger.md index e4b667df62..f4616b42ce 100644 --- a/docs/src/starknet/ledger.md +++ b/docs/src/starknet/ledger.md @@ -4,7 +4,7 @@ ## Prerequisites -`sncast` communicates with the [Starknet Ledger app](https://github.com/LedgerHQ/app-starknet) running on your device (currently supported version: 2.3.4). Make sure the Starknet app is installed and open before running any `sncast ledger` commands. +`sncast` communicates with the [Starknet Ledger app](https://github.com/LedgerHQ/app-starknet) running on your device (currently supported version: 2.4.0). Make sure the Starknet app is installed and open before running any `sncast ledger` commands. > 📝 **Note** > @@ -29,7 +29,7 @@ $ sncast ledger app-version Output: ```shell -App Version: 2.3.4 +App Version: 2.4.0 ```