diff --git a/Cargo.lock b/Cargo.lock index 684ef7bd..233a56ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1082,6 +1082,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1203,6 +1212,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1689,6 +1713,22 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.9" @@ -2099,6 +2139,7 @@ dependencies = [ "predicates", "rand", "regex", + "reqwest", "rust-embed", "serde", "serde_derive", @@ -2250,6 +2291,23 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "normal" version = "0.1.0" @@ -2385,12 +2443,50 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2556,6 +2652,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "powerfmt" version = "0.2.0" @@ -2798,6 +2900,7 @@ checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-channel", "futures-core", "futures-util", @@ -2807,11 +2910,13 @@ dependencies = [ "http-body-util", "hyper 1.4.1", "hyper-rustls 0.27.3", + "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -2823,7 +2928,9 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper", + "system-configuration", "tokio", + "tokio-native-tls", "tokio-rustls 0.26.0", "tokio-util", "tower-service", @@ -3915,6 +4022,27 @@ dependencies = [ "futures-core", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.12.0" @@ -4083,6 +4211,16 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.24.1" @@ -4351,6 +4489,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" diff --git a/crates/loam-cli/Cargo.toml b/crates/loam-cli/Cargo.toml index f23477d6..4c9eb5ea 100644 --- a/crates/loam-cli/Cargo.toml +++ b/crates/loam-cli/Cargo.toml @@ -42,6 +42,7 @@ sha2 = { workspace = true } clap-cargo-extra = "0.3.0" +reqwest = { version = "0.12.9", features = ["json"] } thiserror = "1.0.31" serde = "1.0.82" serde_derive = "1.0.82" diff --git a/crates/loam-cli/src/commands/build/env_toml.rs b/crates/loam-cli/src/commands/build/env_toml.rs index b3570904..963c924a 100644 --- a/crates/loam-cli/src/commands/build/env_toml.rs +++ b/crates/loam-cli/src/commands/build/env_toml.rs @@ -77,7 +77,7 @@ pub struct Network { pub rpc_url: Option, pub network_passphrase: Option, pub rpc_headers: Option>, - // run_locally: Option, + pub run_locally: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/crates/loam-cli/src/commands/dev/docker.rs b/crates/loam-cli/src/commands/dev/docker.rs new file mode 100644 index 00000000..2e812a8b --- /dev/null +++ b/crates/loam-cli/src/commands/dev/docker.rs @@ -0,0 +1,56 @@ +use soroban_cli::{commands as cli, CommandParser}; +use std::error::Error; + +pub async fn start_local_stellar() -> Result<(), Box> { + let result = cli::container::StartCmd::parse_arg_vec(&["local"])? + .run(&soroban_cli::commands::global::Args::default()) + .await; + + match result { + Ok(()) => { + tokio::time::sleep(std::time::Duration::from_secs(10)).await; + } + Err(e) => { + if e.to_string().contains("already in use") + || e.to_string().contains("port is already allocated") + { + eprintln!("Container is already running, proceeding to health check..."); + } else { + return Err(Box::new(e)); + } + } + } + + wait_for_stellar_health().await?; + Ok(()) +} +async fn wait_for_stellar_health() -> Result<(), Box> { + let client = reqwest::Client::new(); + let start_time = std::time::Instant::now(); + let timeout = std::time::Duration::from_secs(60); + loop { + let elapsed_time = start_time.elapsed(); + if elapsed_time > timeout { + eprintln!("Timeout reached: stopping health checks."); + return Err("Health check timed out".into()); + } + let res = client + .post("http://localhost:8000/rpc") + .header("Content-Type", "application/json") + .body(r#"{"jsonrpc": "2.0", "id": 1, "method": "getHealth"}"#) + .send() + .await?; + if res.status().is_success() { + let health_status: serde_json::Value = res.json().await?; + if health_status["result"]["status"] == "healthy" { + break; + } + eprintln!("Stellar status is not healthy: {health_status:?}"); + } else { + eprintln!("Health check request failed with status: {}", res.status()); + } + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + eprintln!("Retrying health check."); + } + Ok(()) +} diff --git a/crates/loam-cli/src/commands/dev/mod.rs b/crates/loam-cli/src/commands/dev/mod.rs index 05b1d5b2..cb1b338f 100644 --- a/crates/loam-cli/src/commands/dev/mod.rs +++ b/crates/loam-cli/src/commands/dev/mod.rs @@ -10,11 +10,13 @@ use tokio::sync::mpsc; use tokio::sync::Mutex; use tokio::time; -use crate::commands::build; +use crate::commands::build::{self, env_toml}; use super::build::clients::LoamEnv; use super::build::env_toml::ENV_FILE; +pub mod docker; + pub enum Message { FileChanged, } @@ -34,6 +36,10 @@ pub enum Error { Build(#[from] build::Error), #[error("IO error: {0}")] Io(#[from] std::io::Error), + #[error(transparent)] + Env(#[from] env_toml::Error), + #[error("Failed to start docker container")] + DockerStart, } fn canonicalize_path(path: &Path) -> PathBuf { @@ -137,7 +143,19 @@ impl Cmd { .parent() .unwrap_or_else(|| Path::new(".")); let env_toml_dir = workspace_root; - + let Some(current_env) = + env_toml::Environment::get(workspace_root, &LoamEnv::Development.to_string())? + else { + return Ok(()); + }; + if current_env.network.run_locally.unwrap_or(false) { + eprintln!("Starting local Stellar Docker container..."); + docker::start_local_stellar().await.map_err(|e| { + eprintln!("Failed to start Stellar Docker container: {e:?}"); + Error::DockerStart + })?; + eprintln!("Local Stellar network is healthy and running."); + } let packages = self .build_cmd .list_packages()? diff --git a/crates/loam-cli/tests/it/build_clients/dev.rs b/crates/loam-cli/tests/it/build_clients/dev.rs index 0bd376a4..87658a02 100644 --- a/crates/loam-cli/tests/it/build_clients/dev.rs +++ b/crates/loam-cli/tests/it/build_clients/dev.rs @@ -130,3 +130,61 @@ soroban_token_contract.client = false }) .await; } + +#[tokio::test] +async fn dev_command_start_local_stellar_with_run_locally() { + TestEnv::from_async("soroban-init-boilerplate", |env| async { + Box::pin(async move { + // Set environments.toml with run_locally enabled + env.set_environments_toml( + r#" +development.accounts = [ + { name = "alice" }, +] + +[development.network] +rpc-url = "http://localhost:8000/rpc" +network-passphrase = "Standalone Network ; February 2017" +run-locally = true + +[development.contracts] +hello_world.client = true +soroban_increment_contract.client = true +soroban_custom_types_contract.client = false +soroban_auth_contract.client = false +soroban_token_contract.client = false +"#, + ); + + let mut dev_process = env + .loam_process("dev", &["--build-clients"]) + .current_dir(&env.cwd) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("Failed to spawn dev process"); + + let stderr = dev_process.stderr.take().unwrap(); + let mut stderr_lines = LinesStream::new(BufReader::new(stderr).lines()); + + TestEnv::wait_for_output( + &mut stderr_lines, + "Starting local Stellar Docker container...", + ) + .await; + + TestEnv::wait_for_output( + &mut stderr_lines, + "Local Stellar network is healthy and running.", + ) + .await; + + dev_process + .kill() + .await + .expect("Failed to kill dev process"); + }) + .await; + }) + .await; +}