From a5f02595a434e168c443fbb7147264a6e180821b Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Fri, 20 Dec 2024 18:39:35 +1000 Subject: [PATCH 1/2] keys: valid keys when added (#1812) Co-authored-by: Willem Wyndham Co-authored-by: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> --- FULL_HELP_DOCS.md | 4 +-- cmd/soroban-cli/src/config/secret.rs | 45 ++++++++-------------------- 2 files changed, 14 insertions(+), 35 deletions(-) diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index 208ec1e84..6b67e32a0 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -961,8 +961,8 @@ Add a new identity (keypair, ledger, macOS keychain) ###### **Options:** -* `--secret-key` — Add using `secret_key` Can provide with `SOROBAN_SECRET_KEY` -* `--seed-phrase` — Add using 12 word seed phrase to generate `secret_key` +* `--secret-key` — (deprecated) Enter secret (S) key when prompted +* `--seed-phrase` — (deprecated) Enter key using 12-24 word seed phrase * `--global` — Use global config * `--config-dir ` — Location of config directory, default is "." diff --git a/cmd/soroban-cli/src/config/secret.rs b/cmd/soroban-cli/src/config/secret.rs index d6de1b6ea..00cec12f6 100644 --- a/cmd/soroban-cli/src/config/secret.rs +++ b/cmd/soroban-cli/src/config/secret.rs @@ -11,8 +11,6 @@ use crate::{ #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("invalid secret key")] - InvalidSecretKey, // #[error("seed_phrase must be 12 words long, found {len}")] // InvalidSeedPhrase { len: usize }, #[error("secret input error")] @@ -23,8 +21,8 @@ pub enum Error { SeedPhrase(#[from] sep5::error::Error), #[error(transparent)] Ed25519(#[from] ed25519_dalek::SignatureError), - #[error("Invalid address {0}")] - InvalidAddress(String), + #[error("cannot parse secret (S) or seed phrase (12 or 24 word)")] + InvalidSecretOrSeedPhrase, #[error(transparent)] Signer(#[from] signer::Error), } @@ -32,12 +30,11 @@ pub enum Error { #[derive(Debug, clap::Args, Clone)] #[group(skip)] pub struct Args { - /// Add using `secret_key` - /// Can provide with `SOROBAN_SECRET_KEY` - #[arg(long, conflicts_with = "seed_phrase")] + /// (deprecated) Enter secret (S) key when prompted + #[arg(long)] pub secret_key: bool, - /// Add using 12 word seed phrase to generate `secret_key` - #[arg(long, conflicts_with = "secret_key")] + /// (deprecated) Enter key using 12-24 word seed phrase + #[arg(long)] pub seed_phrase: bool, } @@ -45,30 +42,12 @@ impl Args { pub fn read_secret(&self) -> Result { if let Ok(secret_key) = std::env::var("SOROBAN_SECRET_KEY") { Ok(Secret::SecretKey { secret_key }) - } else if self.secret_key { - println!("Type a secret key: "); - let secret_key = read_password()?; - let secret_key = PrivateKey::from_string(&secret_key) - .map_err(|_| Error::InvalidSecretKey)? - .to_string(); - Ok(Secret::SecretKey { secret_key }) - } else if self.seed_phrase { - println!("Type a 12 word seed phrase: "); - let seed_phrase = read_password()?; - let seed_phrase: Vec<&str> = seed_phrase.split_whitespace().collect(); - // if seed_phrase.len() != 12 { - // let len = seed_phrase.len(); - // return Err(Error::InvalidSeedPhrase { len }); - // } - Ok(Secret::SeedPhrase { - seed_phrase: seed_phrase - .into_iter() - .map(ToString::to_string) - .collect::>() - .join(" "), - }) } else { - Err(Error::PasswordRead {}) + println!("Type a secret key or 12/24 word seed phrase:"); + let secret_key = read_password()?; + secret_key + .parse() + .map_err(|_| Error::InvalidSecretOrSeedPhrase) } } } @@ -93,7 +72,7 @@ impl FromStr for Secret { seed_phrase: s.to_string(), }) } else { - Err(Error::InvalidAddress(s.to_string())) + Err(Error::InvalidSecretOrSeedPhrase) } } } From 1c0d4e6e528452d7a7d020ef12ce39e475434f4a Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Fri, 20 Dec 2024 13:00:39 -0500 Subject: [PATCH 2/2] Fix/rpc header (#1799) * Use network.rpc_client() to get a contract's spec this will make sure that we're adding the necessary rpc provider headers, if they are required for the given provider * Use self.rpc_client() in network for creating a new rpc client * Refactor TestEnv to have a network field * Test that rpc_headers are being passed on rpc provider requests * Fix operations int test * Clippy * Add CC-1.0 to allowed licenses in deny.toml https://stellarfoundation.slack.com/archives/C92PPVBPT/p1734657921992129 * fix * Use workspace httpmock dep also - move soroban-test httpmock dep to dev-dependencies - use workspace dep for stellar-ledger test --------- Co-authored-by: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> --- Cargo.lock | 1 + Cargo.toml | 1 + cmd/crates/soroban-test/Cargo.toml | 2 +- cmd/crates/soroban-test/src/lib.rs | 45 ++++++--- .../tests/it/integration/bindings.rs | 4 +- .../tests/it/integration/hello_world.rs | 4 +- .../tests/it/integration/tx/operations.rs | 24 ++--- cmd/crates/soroban-test/tests/it/main.rs | 1 + .../soroban-test/tests/it/rpc_provider.rs | 97 +++++++++++++++++++ cmd/crates/stellar-ledger/Cargo.toml | 2 +- cmd/soroban-cli/src/config/network.rs | 2 +- cmd/soroban-cli/src/get_spec.rs | 2 +- deny.toml | 1 + 13 files changed, 155 insertions(+), 31 deletions(-) create mode 100644 cmd/crates/soroban-test/tests/it/rpc_provider.rs diff --git a/Cargo.lock b/Cargo.lock index cbe1678d6..d69fbaa51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4601,6 +4601,7 @@ dependencies = [ "ed25519-dalek", "fs_extra", "hex", + "httpmock", "predicates", "sep5", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 839ca9cd0..702de93c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,6 +105,7 @@ toml_edit = "0.22.20" toml = "0.8.19" reqwest = "0.12.7" predicates = "3.1.2" +httpmock = "0.7.0" [profile.test-wasms] inherits = "release" diff --git a/cmd/crates/soroban-test/Cargo.toml b/cmd/crates/soroban-test/Cargo.toml index d035652d1..ddb10d5f7 100644 --- a/cmd/crates/soroban-test/Cargo.toml +++ b/cmd/crates/soroban-test/Cargo.toml @@ -33,7 +33,6 @@ predicates = { workspace = true } fs_extra = "1.3.0" toml = { workspace = true } - [dev-dependencies] serde_json = "1.0.93" which = { workspace = true } @@ -42,6 +41,7 @@ walkdir = "2.4.0" ulid.workspace = true ed25519-dalek = { workspace = true } hex = { workspace = true } +httpmock = { workspace = true } [features] it = [] diff --git a/cmd/crates/soroban-test/src/lib.rs b/cmd/crates/soroban-test/src/lib.rs index 492d3bb97..4f36a0b33 100644 --- a/cmd/crates/soroban-test/src/lib.rs +++ b/cmd/crates/soroban-test/src/lib.rs @@ -58,8 +58,7 @@ pub enum Error { /// its own `TempDir` where it will save test-specific configuration. pub struct TestEnv { pub temp_dir: TempDir, - pub rpc_url: String, - pub network_passphrase: String, + pub network: network::Network, } impl Default for TestEnv { @@ -67,8 +66,11 @@ impl Default for TestEnv { let temp_dir = TempDir::new().unwrap(); Self { temp_dir, - rpc_url: "http://localhost:8889/soroban/rpc".to_string(), - network_passphrase: LOCAL_NETWORK_PASSPHRASE.to_string(), + network: network::Network { + rpc_url: "http://localhost:8889/soroban/rpc".to_string(), + network_passphrase: LOCAL_NETWORK_PASSPHRASE.to_string(), + rpc_headers: [].to_vec(), + }, } } } @@ -102,12 +104,21 @@ impl TestEnv { } pub fn with_rpc_url(rpc_url: &str) -> TestEnv { - let mut env = TestEnv { - rpc_url: rpc_url.to_string(), - ..Default::default() + let mut env = TestEnv::default(); + env.network.rpc_url = rpc_url.to_string(); + if let Ok(network_passphrase) = std::env::var("STELLAR_NETWORK_PASSPHRASE") { + env.network.network_passphrase = network_passphrase; }; + env.generate_account("test", None).assert().success(); + env + } + + pub fn with_rpc_provider(rpc_url: &str, rpc_headers: Vec<(String, String)>) -> TestEnv { + let mut env = TestEnv::default(); + env.network.rpc_url = rpc_url.to_string(); + env.network.rpc_headers = rpc_headers; if let Ok(network_passphrase) = std::env::var("STELLAR_NETWORK_PASSPHRASE") { - env.network_passphrase = network_passphrase; + env.network.network_passphrase = network_passphrase; }; env.generate_account("test", None).assert().success(); env @@ -131,13 +142,25 @@ impl TestEnv { /// to be the internal `temp_dir`. pub fn new_assert_cmd(&self, subcommand: &str) -> Command { let mut cmd: Command = self.bin(); + cmd.arg(subcommand) .env("SOROBAN_ACCOUNT", TEST_ACCOUNT) - .env("SOROBAN_RPC_URL", &self.rpc_url) + .env("SOROBAN_RPC_URL", &self.network.rpc_url) .env("SOROBAN_NETWORK_PASSPHRASE", LOCAL_NETWORK_PASSPHRASE) .env("XDG_CONFIG_HOME", self.temp_dir.join("config").as_os_str()) .env("XDG_DATA_HOME", self.temp_dir.join("data").as_os_str()) .current_dir(&self.temp_dir); + + if !self.network.rpc_headers.is_empty() { + cmd.env( + "STELLAR_RPC_HEADERS", + format!( + "{}:{}", + &self.network.rpc_headers[0].0, &self.network.rpc_headers[0].1 + ), + ); + } + cmd } @@ -234,7 +257,7 @@ impl TestEnv { let config_dir = Some(self.dir().to_path_buf()); config::Args { network: network::Args { - rpc_url: Some(self.rpc_url.clone()), + rpc_url: Some(self.network.rpc_url.clone()), rpc_headers: [].to_vec(), network_passphrase: Some(LOCAL_NETWORK_PASSPHRASE.to_string()), network: None, @@ -305,7 +328,7 @@ impl TestEnv { } pub fn client(&self) -> soroban_rpc::Client { - soroban_rpc::Client::new(&self.rpc_url).unwrap() + self.network.rpc_client().unwrap() } } diff --git a/cmd/crates/soroban-test/tests/it/integration/bindings.rs b/cmd/crates/soroban-test/tests/it/integration/bindings.rs index feb7f2ef8..4080cc110 100644 --- a/cmd/crates/soroban-test/tests/it/integration/bindings.rs +++ b/cmd/crates/soroban-test/tests/it/integration/bindings.rs @@ -13,7 +13,7 @@ async fn invoke_test_generate_typescript_bindings() { "--network-passphrase", LOCAL_NETWORK_PASSPHRASE, "--rpc-url", - &sandbox.rpc_url, + &sandbox.network.rpc_url, "--output-dir", &outdir.display().to_string(), "--overwrite", @@ -43,7 +43,7 @@ async fn invoke_test_bindings_context_failure() { "--network-passphrase", LOCAL_NETWORK_PASSPHRASE, "--rpc-url", - &sandbox.rpc_url, + &sandbox.network.rpc_url, "--output-dir", &outdir.display().to_string(), "--overwrite", diff --git a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs index 4464e532f..e63fdd4b4 100644 --- a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs +++ b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs @@ -27,7 +27,7 @@ async fn invoke_view_with_non_existent_source_account() { #[allow(clippy::too_many_lines)] async fn invoke() { let sandbox = &TestEnv::new(); - let c = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let c = sandbox.network.rpc_client().unwrap(); let GetLatestLedgerResponse { sequence, .. } = c.get_latest_ledger().await.unwrap(); sandbox .new_assert_cmd("keys") @@ -365,7 +365,7 @@ async fn fetch(sandbox: &TestEnv, id: &str) { let f = sandbox.dir().join("contract.wasm"); let cmd = sandbox.cmd_arr::(&[ "--rpc-url", - &sandbox.rpc_url, + &sandbox.network.rpc_url, "--network-passphrase", LOCAL_NETWORK_PASSPHRASE, "--id", diff --git a/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs b/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs index 1ce1f06c9..d9bc2ac63 100644 --- a/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs +++ b/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs @@ -66,7 +66,7 @@ async fn create_account() { .success() .stdout_as_str(); let test = test_address(sandbox); - let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let client = sandbox.network.rpc_client().unwrap(); let test_account = client.get_account(&test).await.unwrap(); println!("test account has a balance of {}", test_account.balance); let starting_balance = ONE_XLM * 100; @@ -92,7 +92,7 @@ async fn create_account() { #[tokio::test] async fn payment() { let sandbox = &TestEnv::new(); - let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let client = sandbox.network.rpc_client().unwrap(); let (test, test1) = setup_accounts(sandbox); let test_account = client.get_account(&test).await.unwrap(); println!("test account has a balance of {}", test_account.balance); @@ -125,7 +125,7 @@ async fn payment() { #[tokio::test] async fn bump_sequence() { let sandbox = &TestEnv::new(); - let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let client = sandbox.network.rpc_client().unwrap(); let test = test_address(sandbox); let before = client.get_account(&test).await.unwrap(); let amount = 50; @@ -148,7 +148,7 @@ async fn bump_sequence() { #[tokio::test] async fn account_merge() { let sandbox = &TestEnv::new(); - let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let client = sandbox.network.rpc_client().unwrap(); let (test, test1) = setup_accounts(sandbox); let before = client.get_account(&test).await.unwrap(); let before1 = client.get_account(&test1).await.unwrap(); @@ -188,7 +188,7 @@ async fn set_trustline_flags() { .success(); let id = contract_id_hash_from_asset( asset.parse::().unwrap(), - &sandbox.network_passphrase, + &sandbox.network.network_passphrase, ); // sandbox // .new_assert_cmd("contract") @@ -224,7 +224,7 @@ async fn set_trustline_flags() { #[tokio::test] async fn set_options_add_signer() { let sandbox = &TestEnv::new(); - let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let client = sandbox.network.rpc_client().unwrap(); let (test, test1) = setup_accounts(sandbox); let before = client.get_account(&test).await.unwrap(); sandbox @@ -286,7 +286,7 @@ fn build_and_run(sandbox: &TestEnv, cmd: &str, args: &[&str]) -> String { #[tokio::test] async fn set_options() { let sandbox = &TestEnv::new(); - let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let client = sandbox.network.rpc_client().unwrap(); let (test, alice) = setup_accounts(sandbox); let before = client.get_account(&test).await.unwrap(); assert!(before.inflation_dest.is_none()); @@ -356,7 +356,7 @@ async fn set_options() { #[tokio::test] async fn set_some_options() { let sandbox = &TestEnv::new(); - let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let client = sandbox.network.rpc_client().unwrap(); let test = test_address(sandbox); let before = client.get_account(&test).await.unwrap(); assert!(before.inflation_dest.is_none()); @@ -451,7 +451,7 @@ async fn change_trust() { // wrap_cmd(&asset).run().await.unwrap(); let id = contract_id_hash_from_asset( asset.parse::().unwrap(), - &sandbox.network_passphrase, + &sandbox.network.network_passphrase, ); sandbox .new_assert_cmd("contract") @@ -529,7 +529,7 @@ async fn change_trust() { async fn manage_data() { let sandbox = &TestEnv::new(); let (test, _) = setup_accounts(sandbox); - let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let client = sandbox.network.rpc_client().unwrap(); let key = "test"; let value = "beefface"; sandbox @@ -573,7 +573,7 @@ async fn manage_data() { } async fn issue_asset(sandbox: &TestEnv, test: &str, asset: &str, limit: u64, initial_balance: u64) { - let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let client = sandbox.network.rpc_client().unwrap(); let test_before = client.get_account(test).await.unwrap(); sandbox .new_assert_cmd("tx") @@ -633,7 +633,7 @@ async fn issue_asset(sandbox: &TestEnv, test: &str, asset: &str, limit: u64, ini #[tokio::test] async fn multi_create_accounts() { let sandbox = &TestEnv::new(); - let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let client = sandbox.network.rpc_client().unwrap(); let nums: Vec = (1..=3).collect(); let mut accounts: Vec<(String, String)> = nums .iter() diff --git a/cmd/crates/soroban-test/tests/it/main.rs b/cmd/crates/soroban-test/tests/it/main.rs index e06c1a47d..4dc54a194 100644 --- a/cmd/crates/soroban-test/tests/it/main.rs +++ b/cmd/crates/soroban-test/tests/it/main.rs @@ -6,5 +6,6 @@ mod init; // #[cfg(feature = "it")] mod integration; mod plugin; +mod rpc_provider; mod util; mod version; diff --git a/cmd/crates/soroban-test/tests/it/rpc_provider.rs b/cmd/crates/soroban-test/tests/it/rpc_provider.rs new file mode 100644 index 000000000..04c4c1bdc --- /dev/null +++ b/cmd/crates/soroban-test/tests/it/rpc_provider.rs @@ -0,0 +1,97 @@ +use httpmock::{prelude::*, Mock}; +use serde_json::json; +use soroban_rpc::{GetEventsResponse, GetNetworkResponse}; +use soroban_test::{TestEnv, LOCAL_NETWORK_PASSPHRASE}; + +#[tokio::test] +async fn test_use_rpc_provider_with_auth_header() { + // mock out http request to rpc provider + let server = MockServer::start(); + let generate_account_mock = mock_generate_account(&server); + let get_network_mock = mock_get_network(&server); + let get_events_mock = mock_get_events(&server); + + // create a new test environment with the mock server + let rpc_url = server.url(""); + let rpc_headers = vec![("Authorization".to_string(), "Bearer test-token".to_string())]; + let sandbox = &TestEnv::with_rpc_provider(&rpc_url, rpc_headers); + + sandbox + .new_assert_cmd("events") + .arg("--start-ledger") + .arg("1000") + .assert() + .success(); + + // generate account is being called in `with_rpc_provider` + generate_account_mock.assert(); + // get_network and get_events are being called in the `stellar events` command + get_network_mock.assert(); + get_events_mock.assert(); +} + +fn mock_generate_account(server: &MockServer) -> Mock { + server.mock(|when, then| { + when.method(GET) + .path("/friendbot") + .header("accept", "*/*") + .header("user-agent", "soroban-cli/22.0.1"); //update this to be future proof + then.status(200); + }) +} + +fn mock_get_network(server: &MockServer) -> Mock { + server.mock(|when, then| { + when.method(POST) + .path("/") + .header("authorization", "Bearer test-token") + .json_body(json!({ + "jsonrpc": "2.0", + "id": 0, + "method": "getNetwork" + })); + + then.status(200).json_body(json!({ + "jsonrpc": "2.0", + "id": 0, + "result": GetNetworkResponse { + friendbot_url: None, + passphrase: LOCAL_NETWORK_PASSPHRASE.to_string(), + protocol_version: 22} + })); + }) +} + +fn mock_get_events(server: &MockServer) -> Mock { + server.mock(|when, then| { + when.method(POST) + .path("/") + .header("authorization", "Bearer test-token") + .json_body(json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "getEvents", + "params": { + "startLedger": 1000, + "filters": [ + { + "contractIds": [], + "topics": [] + } + ], + "pagination": { + "limit": 10 + } + } + })); + + then.status(200).json_body(json!({ + "jsonrpc": "2.0", + "id": 1, + "result": GetEventsResponse { + events: vec![], + latest_ledger: 1000 + } + })); + }) +} diff --git a/cmd/crates/stellar-ledger/Cargo.toml b/cmd/crates/stellar-ledger/Cargo.toml index b3d6318a4..f06d17a2b 100644 --- a/cmd/crates/stellar-ledger/Cargo.toml +++ b/cmd/crates/stellar-ledger/Cargo.toml @@ -51,7 +51,7 @@ log = "0.4.21" once_cell = "1.19.0" pretty_assertions = "1.2.1" serial_test = "3.0.0" -httpmock = "0.7.0-rc.1" +httpmock = { workspace = true } test-case = "3.3.1" testcontainers = "0.20.1" diff --git a/cmd/soroban-cli/src/config/network.rs b/cmd/soroban-cli/src/config/network.rs index 829716753..e64a0ee77 100644 --- a/cmd/soroban-cli/src/config/network.rs +++ b/cmd/soroban-cli/src/config/network.rs @@ -170,7 +170,7 @@ impl Network { local_url.set_query(Some(&format!("addr={addr}"))); Ok(local_url) } else { - let client = Client::new(&self.rpc_url)?; + let client = self.rpc_client()?; let network = client.get_network().await?; tracing::debug!("network {network:?}"); let url = client.friendbot_url().await?; diff --git a/cmd/soroban-cli/src/get_spec.rs b/cmd/soroban-cli/src/get_spec.rs index 26e609543..f2da15863 100644 --- a/cmd/soroban-cli/src/get_spec.rs +++ b/cmd/soroban-cli/src/get_spec.rs @@ -44,7 +44,7 @@ pub async fn get_remote_contract_spec( |c| c.get_network().map_err(Error::from), )?; tracing::trace!(?network); - let client = rpc::Client::new(&network.rpc_url)?; + let client = network.rpc_client()?; // Get contract data let r = client.get_contract_data(contract_id).await?; tracing::trace!("{r:?}"); diff --git a/deny.toml b/deny.toml index 9817c3e79..600bfec02 100644 --- a/deny.toml +++ b/deny.toml @@ -110,6 +110,7 @@ allow = [ "Unicode-DFS-2016", "ISC", "BSD-2-Clause", + "CC0-1.0", ] # List of explicitly disallowed licenses # See https://spdx.org/licenses/ for list of possible licenses