From 120b679cb4fef61c40b4667e14e12135742641c0 Mon Sep 17 00:00:00 2001 From: Maksymilian Kowalski Date: Thu, 2 Oct 2025 17:29:21 +0200 Subject: [PATCH 01/18] Introduce `devnet` option for `--network` --- crates/sncast/src/helpers/block_explorer.rs | 2 ++ crates/sncast/src/helpers/mod.rs | 1 + crates/sncast/src/helpers/rpc.rs | 10 +++++++++- crates/sncast/src/lib.rs | 7 +++++++ crates/sncast/src/response/explorer_link.rs | 2 ++ crates/sncast/src/starknet_commands/verify/voyager.rs | 1 + crates/sncast/src/starknet_commands/verify/walnut.rs | 1 + 7 files changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/sncast/src/helpers/block_explorer.rs b/crates/sncast/src/helpers/block_explorer.rs index 566fb1bf99..c47377765d 100644 --- a/crates/sncast/src/helpers/block_explorer.rs +++ b/crates/sncast/src/helpers/block_explorer.rs @@ -24,6 +24,7 @@ impl Service { (Service::ViewBlock, Network::Mainnet) => Ok(Box::new(ViewBlock)), (Service::OkLink, Network::Mainnet) => Ok(Box::new(OkLink)), (_, Network::Sepolia) => Err(ExplorerError::SepoliaNotSupported), + (_, Network::Devnet) => Err(ExplorerError::DevnetNotSupported), } } } @@ -38,6 +39,7 @@ const fn network_subdomain(network: Network) -> &'static str { match network { Network::Mainnet => "", Network::Sepolia => "sepolia.", + Network::Devnet => "", } } diff --git a/crates/sncast/src/helpers/mod.rs b/crates/sncast/src/helpers/mod.rs index a7e4b8e405..cb97500c82 100644 --- a/crates/sncast/src/helpers/mod.rs +++ b/crates/sncast/src/helpers/mod.rs @@ -5,6 +5,7 @@ pub mod command; pub mod config; pub mod configuration; pub mod constants; +pub mod devnet; pub mod fee; pub mod interactive; pub mod output_format; diff --git a/crates/sncast/src/helpers/rpc.rs b/crates/sncast/src/helpers/rpc.rs index 96ce2dcf59..0bec65b0ae 100644 --- a/crates/sncast/src/helpers/rpc.rs +++ b/crates/sncast/src/helpers/rpc.rs @@ -1,4 +1,5 @@ use crate::helpers::configuration::CastConfig; +use crate::helpers::devnet; use crate::{Network, get_provider}; use anyhow::{Context, Result, bail}; use clap::Args; @@ -15,7 +16,9 @@ pub struct RpcArgs { #[arg(short, long)] pub url: Option, - /// Use predefined network with a public provider. Note that this option may result in rate limits or other unexpected behavior + /// Use predefined network with a public provider. Note that this option may result in rate limits or other unexpected behavior. + /// For devnet, attempts to auto-detect running starknet-devnet instances or falls back to http://localhost:5050. + /// If auto-detection fails or you're using a custom devnet address, use --url instead. #[arg(long)] pub network: Option, } @@ -86,6 +89,7 @@ impl Network { match self { Network::Mainnet => Self::free_mainnet_rpc(provider), Network::Sepolia => Self::free_sepolia_rpc(provider), + Network::Devnet => Self::free_devnet_rpc(provider), } } @@ -96,6 +100,10 @@ impl Network { fn free_sepolia_rpc(_provider: &FreeProvider) -> String { format!("https://starknet-sepolia.public.blastapi.io/rpc/{RPC_URL_VERSION}") } + + fn free_devnet_rpc(_provider: &FreeProvider) -> String { + devnet::detect_devnet_url() + } } #[cfg(test)] diff --git a/crates/sncast/src/lib.rs b/crates/sncast/src/lib.rs index 46c3c15e49..c187e75094 100644 --- a/crates/sncast/src/lib.rs +++ b/crates/sncast/src/lib.rs @@ -85,10 +85,14 @@ pub const MAINNET: Felt = pub const SEPOLIA: Felt = Felt::from_hex_unchecked(const_hex::const_encode::<10, true>(b"SN_SEPOLIA").as_str()); +pub const DEVNET: Felt = + Felt::from_hex_unchecked(const_hex::const_encode::<6, true>(b"SN_DEV").as_str()); + #[derive(ValueEnum, Clone, Copy, Debug)] pub enum Network { Mainnet, Sepolia, + Devnet, } impl Display for Network { @@ -96,6 +100,7 @@ impl Display for Network { match self { Network::Mainnet => write!(f, "mainnet"), Network::Sepolia => write!(f, "sepolia"), + Network::Devnet => write!(f, "devnet"), } } } @@ -108,6 +113,8 @@ impl TryFrom for Network { Ok(Network::Mainnet) } else if value == SEPOLIA { Ok(Network::Sepolia) + } else if value == DEVNET { + Ok(Network::Devnet) } else { bail!("Given network is neither Mainnet nor Sepolia") } diff --git a/crates/sncast/src/response/explorer_link.rs b/crates/sncast/src/response/explorer_link.rs index 35da81d18a..4af55f831f 100644 --- a/crates/sncast/src/response/explorer_link.rs +++ b/crates/sncast/src/response/explorer_link.rs @@ -48,6 +48,8 @@ pub enum ExplorerError { SepoliaNotSupported, #[error("Custom network is not recognized by block explorer service")] UnrecognizedNetwork, + #[error("Block explorer service is not available for Devnet Network")] + DevnetNotSupported, } pub fn block_explorer_link_if_allowed( diff --git a/crates/sncast/src/starknet_commands/verify/voyager.rs b/crates/sncast/src/starknet_commands/verify/voyager.rs index 6229cbd458..310e11b0fb 100644 --- a/crates/sncast/src/starknet_commands/verify/voyager.rs +++ b/crates/sncast/src/starknet_commands/verify/voyager.rs @@ -392,6 +392,7 @@ impl<'a> VerificationInterface<'a> for Voyager<'a> { Err(_) => match self.network { Network::Mainnet => "https://api.voyager.online/beta".to_string(), Network::Sepolia => "https://sepolia-api.voyager.online/beta".to_string(), + Network::Devnet => "".to_string(), }, } } diff --git a/crates/sncast/src/starknet_commands/verify/walnut.rs b/crates/sncast/src/starknet_commands/verify/walnut.rs index 790f766fa9..946d77feab 100644 --- a/crates/sncast/src/starknet_commands/verify/walnut.rs +++ b/crates/sncast/src/starknet_commands/verify/walnut.rs @@ -114,6 +114,7 @@ impl VerificationInterface<'_> for WalnutVerificationInterface { let path = match self.network { Network::Mainnet => "/v1/sn_main/verify", Network::Sepolia => "/v1/sn_sepolia/verify", + Network::Devnet => "", }; format!("{api_base_url}{path}") } From 61b534dfbef5ecda18cd830d25542f6f8faf3a20 Mon Sep 17 00:00:00 2001 From: Maksymilian Kowalski Date: Thu, 2 Oct 2025 17:29:48 +0200 Subject: [PATCH 02/18] Implement heuristic detection --- crates/sncast/src/helpers/devnet.rs | 177 ++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 crates/sncast/src/helpers/devnet.rs diff --git a/crates/sncast/src/helpers/devnet.rs b/crates/sncast/src/helpers/devnet.rs new file mode 100644 index 0000000000..f6adecfe3f --- /dev/null +++ b/crates/sncast/src/helpers/devnet.rs @@ -0,0 +1,177 @@ +use std::net::TcpStream; +use std::time::Duration; + +/// Detects devnet by scanning running processes for starknet-devnet and extracting the port +pub fn detect_devnet_url() -> String { + detect_devnet_from_processes().unwrap_or_else(|| "http://localhost:5050".to_string()) +} + +/// Detects devnet by scanning running processes for starknet-devnet and extracting the port +fn detect_devnet_from_processes() -> Option { + if let Some(port) = find_devnet_process_port() { + return Some(format!("http://localhost:{}", port)); + } + + let common_ports = [5050, 5000, 8545, 3000, 8000]; + for &port in &common_ports { + if is_port_reachable("localhost", port) { + return Some(format!("http://localhost:{}", port)); + } + } + + None +} + +fn find_devnet_process_port() -> Option { + use std::process::Command; + + let output = Command::new("ps").args(&["aux"]).output().ok()?; + let ps_output = String::from_utf8_lossy(&output.stdout); + + for line in ps_output.lines() { + if line.contains("starknet-devnet") { + // First try to extract port from command line arguments (faster) + if let Some(port) = extract_port_from_cmdline(line) { + return Some(port); + } + + // If that fails, try to get port from PID + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() > 1 { + if let Ok(pid) = parts[1].parse::() { + if let Some(port) = get_port_from_pid(pid) { + return Some(port); + } + } + } + } + } + None +} + +#[cfg(any(target_os = "macos", target_os = "linux"))] +fn get_port_from_pid(pid: u32) -> Option { + if let Some(port) = try_lsof_for_port(pid) { + return Some(port); + } + + #[cfg(target_os = "linux")] + { + try_linux_nettools_for_port(pid) + } + + None +} + +#[cfg(any(target_os = "macos", target_os = "linux"))] +fn try_lsof_for_port(pid: u32) -> Option { + use std::process::Command; + + let output = Command::new("lsof") + .args(&["-P", "-p", &pid.to_string(), "-i"]) + .output() + .ok()?; + + let lsof_output = String::from_utf8_lossy(&output.stdout); + + for line in lsof_output.lines() { + if line.contains("TCP") && line.contains("LISTEN") { + if let Some(port_part) = line.split_whitespace().last() { + if let Some(port_str) = port_part.split(':').last() { + if let Ok(port) = port_str.parse::() { + return Some(port); + } + } + } + } + } + None +} + +#[cfg(target_os = "linux")] +fn try_linux_nettools_for_port(pid: u32) -> Option { + use std::process::Command; + + let output = Command::new("ss") + .args(&["-tlnp"]) + .output() + .or_else(|_| Command::new("netstat").args(&["-tlnp"]).output()) + .ok()?; + + let net_output = String::from_utf8_lossy(&output.stdout); + + for line in net_output.lines() { + if line.contains(&pid.to_string()) { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() > 3 { + if let Some(port_str) = parts[3].split(':').last() { + if let Ok(port) = port_str.parse::() { + return Some(port); + } + } + } + } + } + None +} + +fn extract_port_from_cmdline(cmdline: &str) -> Option { + let patterns = ["--port", "--host", ":", "localhost:"]; + + for pattern in &patterns { + if let Some(pos) = cmdline.find(pattern) { + let after_pattern = &cmdline[pos + pattern.len()..]; + let port_str = after_pattern + .split_whitespace() + .next() + .unwrap_or("") + .trim_start_matches('=') + .trim_start_matches(':'); + + if let Ok(port) = port_str.parse::() { + if port > 1000 && port < 65535 { + return Some(port); + } + } + } + } + + for word in cmdline.split_whitespace() { + if let Ok(port) = word.parse::() { + return Some(port); + } + } + + None +} + +fn is_port_reachable(host: &str, port: u16) -> bool { + if let Ok(addr) = format!("{}:{}", host, port).parse() { + match TcpStream::connect_timeout(&addr, Duration::from_millis(50)) { + Ok(_) => true, + Err(_) => false, + } + } else { + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_devnet_process_detection() { + let cmdline1 = "starknet-devnet --port 5050 --host localhost"; + assert_eq!(extract_port_from_cmdline(cmdline1), Some(5050)); + + let cmdline3 = "/usr/bin/starknet-devnet --port=5000"; + assert_eq!(extract_port_from_cmdline(cmdline3), Some(5000)); + + // Test devnet URL generation + let devnet_url = detect_devnet_url(); + assert!(devnet_url.starts_with("http://localhost:")); + + let _reachable = is_port_reachable("localhost", 5050); + } +} From 807dcbae0c1140e64c3773a5ee9f7349ef4e68bf Mon Sep 17 00:00:00 2001 From: Maksymilian Kowalski Date: Thu, 2 Oct 2025 17:35:32 +0200 Subject: [PATCH 03/18] Update docs --- crates/sncast/src/helpers/rpc.rs | 2 +- docs/src/starknet/sncast-overview.md | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/sncast/src/helpers/rpc.rs b/crates/sncast/src/helpers/rpc.rs index 0bec65b0ae..d252f29fd6 100644 --- a/crates/sncast/src/helpers/rpc.rs +++ b/crates/sncast/src/helpers/rpc.rs @@ -17,7 +17,7 @@ pub struct RpcArgs { pub url: Option, /// Use predefined network with a public provider. Note that this option may result in rate limits or other unexpected behavior. - /// For devnet, attempts to auto-detect running starknet-devnet instances or falls back to http://localhost:5050. + /// For devnet, attempts to auto-detect running starknet-devnet instances or falls back to http://localhost:5050. /// If auto-detection fails or you're using a custom devnet address, use --url instead. #[arg(long)] pub network: Option, diff --git a/docs/src/starknet/sncast-overview.md b/docs/src/starknet/sncast-overview.md index ead7935205..3c71f6c97a 100644 --- a/docs/src/starknet/sncast-overview.md +++ b/docs/src/starknet/sncast-overview.md @@ -56,8 +56,16 @@ Response: [0x0, 0x0, 0x43686172697a617264, 0x9, 0x0, 0x0, 0x41a78e741e5af2fec34b ### Network and RPC Providers -When providing `--network` flag, `sncast` will randomly select on of the free RPC providers. -When using free provider you may experience rate limits and other unexpected behavior. +The `--network` flag supports the following networks: + +- **mainnet** - Connects to Starknet mainnet using a free RPC provider +- **sepolia** - Connects to Starknet Sepolia testnet using a free RPC provider +- **devnet** - Attempts to auto-detect running starknet-devnet instances or falls back to `http://localhost:5050` + +When using **mainnet** or **sepolia**, `sncast` will randomly select one of the free RPC providers. +When using free providers you may experience rate limits and other unexpected behavior. + +For **devnet**, `sncast` will try to detect running `starknet-devnet` instance and connect to it, but it may fail. If detection fails, then use the `--url` flag instead. If using `sncast` extensively, we recommend getting access to a dedicated RPC node and providing its URL to sncast with `--url` flag. From 9a1ea805581b726b625a0cf1cc078ad52376e09e Mon Sep 17 00:00:00 2001 From: Maksymilian Kowalski Date: Thu, 2 Oct 2025 17:39:15 +0200 Subject: [PATCH 04/18] Changelog info --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f078b42de3..f7c4602de0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Added - Debug logging for `sncast` commands that can be enabled by setting `CAST_LOG` env variable. +- Support for `--network devnet` flag that attempts to auto-detect running `starknet-devnet` instance and connect to it. ## [0.50.0] - 2025-09-29 From dcbb672c1750e0acf532047d3a143b8d965c87fb Mon Sep 17 00:00:00 2001 From: Maksymilian Kowalski Date: Thu, 2 Oct 2025 18:18:06 +0200 Subject: [PATCH 05/18] Change displaying blockexplorer to heuristic detection --- crates/sncast/src/helpers/devnet.rs | 4 ++++ crates/sncast/src/helpers/rpc.rs | 9 -------- crates/sncast/src/main.rs | 17 +++++---------- crates/sncast/src/response/explorer_link.rs | 21 ++++++++++++------- .../src/starknet_commands/account/mod.rs | 16 ++++---------- .../src/starknet_commands/multicall/mod.rs | 8 ++----- 6 files changed, 28 insertions(+), 47 deletions(-) diff --git a/crates/sncast/src/helpers/devnet.rs b/crates/sncast/src/helpers/devnet.rs index f6adecfe3f..9b9fed5d07 100644 --- a/crates/sncast/src/helpers/devnet.rs +++ b/crates/sncast/src/helpers/devnet.rs @@ -6,6 +6,10 @@ pub fn detect_devnet_url() -> String { detect_devnet_from_processes().unwrap_or_else(|| "http://localhost:5050".to_string()) } +pub fn is_devnet_running() -> bool { + detect_devnet_from_processes().is_some() +} + /// Detects devnet by scanning running processes for starknet-devnet and extracting the port fn detect_devnet_from_processes() -> Option { if let Some(port) = find_devnet_process_port() { diff --git a/crates/sncast/src/helpers/rpc.rs b/crates/sncast/src/helpers/rpc.rs index d252f29fd6..0ecc0d270b 100644 --- a/crates/sncast/src/helpers/rpc.rs +++ b/crates/sncast/src/helpers/rpc.rs @@ -7,7 +7,6 @@ use foundry_ui::UI; use shared::consts::RPC_URL_VERSION; use shared::verify_and_warn_if_incompatible_rpc_version; use starknet::providers::{JsonRpcClient, jsonrpc::HttpTransport}; -use url::Url; #[derive(Args, Clone, Debug, Default)] #[group(required = false, multiple = false)] @@ -62,14 +61,6 @@ impl RpcArgs { }) } } - - #[must_use] - pub fn is_localhost(&self, config_url: &String) -> bool { - self.get_url(config_url) - .and_then(|url_str| Url::parse(&url_str).ok()) - .and_then(|url| url.host_str().map(str::to_string)) - .is_some_and(|host| host == "localhost" || host == "127.0.0.1" || host == "::1") - } } pub enum FreeProvider { diff --git a/crates/sncast/src/main.rs b/crates/sncast/src/main.rs index 41648f2eda..8505bedfa0 100644 --- a/crates/sncast/src/main.rs +++ b/crates/sncast/src/main.rs @@ -265,8 +265,6 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()> Commands::Declare(declare) => { let provider = declare.rpc.get_provider(&config, ui).await?; - let rpc = declare.rpc.clone(); - let account = get_account( &config.account, &config.accounts_file, @@ -308,7 +306,7 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()> }); let block_explorer_link = - block_explorer_link_if_allowed(&result, provider.chain_id().await?, &rpc, &config); + block_explorer_link_if_allowed(&result, provider.chain_id().await?, &config); process_command_result("declare", result, ui, block_explorer_link); Ok(()) @@ -316,7 +314,6 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()> Commands::DeclareFrom(declare_from) => { let provider = declare_from.rpc.get_provider(&config, ui).await?; - let rpc_args = declare_from.rpc.clone(); let source_provider = declare_from.source_rpc.get_provider(ui).await?; let account = get_account( &config.account, @@ -345,12 +342,8 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()> } }); - let block_explorer_link = block_explorer_link_if_allowed( - &result, - provider.chain_id().await?, - &rpc_args, - &config, - ); + let block_explorer_link = + block_explorer_link_if_allowed(&result, provider.chain_id().await?, &config); process_command_result("declare-from", result, ui, block_explorer_link); Ok(()) @@ -397,7 +390,7 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()> .map_err(handle_starknet_command_error); let block_explorer_link = - block_explorer_link_if_allowed(&result, provider.chain_id().await?, &rpc, &config); + block_explorer_link_if_allowed(&result, provider.chain_id().await?, &config); process_command_result("deploy", result, ui, block_explorer_link); Ok(()) @@ -485,7 +478,7 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()> .map_err(handle_starknet_command_error); let block_explorer_link = - block_explorer_link_if_allowed(&result, provider.chain_id().await?, &rpc, &config); + block_explorer_link_if_allowed(&result, provider.chain_id().await?, &config); process_command_result("invoke", result, ui, block_explorer_link); diff --git a/crates/sncast/src/response/explorer_link.rs b/crates/sncast/src/response/explorer_link.rs index 4af55f831f..f05c8e6a16 100644 --- a/crates/sncast/src/response/explorer_link.rs +++ b/crates/sncast/src/response/explorer_link.rs @@ -1,8 +1,12 @@ -use crate::helpers::{block_explorer::LinkProvider, configuration::CastConfig, rpc::RpcArgs}; +use crate::Network; +use crate::helpers::{ + block_explorer::LinkProvider, configuration::CastConfig, devnet, rpc::RpcArgs, +}; use foundry_ui::Message; use serde::Serialize; use serde_json::{Value, json}; use starknet_types_core::felt::Felt; +use url::Url; const SNCAST_FORCE_SHOW_EXPLORER_LINKS_ENV: &str = "SNCAST_FORCE_SHOW_EXPLORER_LINKS"; @@ -55,24 +59,25 @@ pub enum ExplorerError { pub fn block_explorer_link_if_allowed( result: &anyhow::Result, chain_id: Felt, - rpc: &RpcArgs, config: &CastConfig, ) -> Option where T: OutputLink + Clone, { - if (!config.show_explorer_links || rpc.is_localhost(&config.url)) - && !is_explorer_link_overridden() - { - return None; - } - let Ok(response) = result else { return None; }; let network = chain_id.try_into().ok()?; + let is_devnet_network = matches!(network, Network::Devnet); + let is_devnet_running = devnet::is_devnet_running(); + let is_devnet = is_devnet_network || is_devnet_running; + + if (!config.show_explorer_links || is_devnet) && !is_explorer_link_overridden() { + return None; + } + config .block_explorer .unwrap_or_default() diff --git a/crates/sncast/src/starknet_commands/account/mod.rs b/crates/sncast/src/starknet_commands/account/mod.rs index 5e3db05867..7c53de2f6c 100644 --- a/crates/sncast/src/starknet_commands/account/mod.rs +++ b/crates/sncast/src/starknet_commands/account/mod.rs @@ -257,12 +257,8 @@ pub async fn account( ) .await; - let block_explorer_link = block_explorer_link_if_allowed( - &result, - provider.chain_id().await?, - &create.rpc, - &config, - ); + let block_explorer_link = + block_explorer_link_if_allowed(&result, provider.chain_id().await?, &config); process_command_result("account create", result, ui, block_explorer_link); Ok(()) @@ -305,12 +301,8 @@ pub async fn account( )); } - let block_explorer_link = block_explorer_link_if_allowed( - &result, - provider.chain_id().await?, - &deploy.rpc, - &config, - ); + let block_explorer_link = + block_explorer_link_if_allowed(&result, provider.chain_id().await?, &config); process_command_result("account deploy", result, ui, block_explorer_link); Ok(()) } diff --git a/crates/sncast/src/starknet_commands/multicall/mod.rs b/crates/sncast/src/starknet_commands/multicall/mod.rs index 2642f0540a..4a7cbbd295 100644 --- a/crates/sncast/src/starknet_commands/multicall/mod.rs +++ b/crates/sncast/src/starknet_commands/multicall/mod.rs @@ -62,12 +62,8 @@ pub async fn multicall( starknet_commands::multicall::run::run(run.clone(), &account, wait_config, ui) .await; - let block_explorer_link = block_explorer_link_if_allowed( - &result, - provider.chain_id().await?, - &run.rpc, - &config, - ); + let block_explorer_link = + block_explorer_link_if_allowed(&result, provider.chain_id().await?, &config); process_command_result("multicall run", result, ui, block_explorer_link); Ok(()) } From 7d95b148cb1fbd8c539a1f6f94bf04172376d17b Mon Sep 17 00:00:00 2001 From: Maksymilian Kowalski Date: Thu, 2 Oct 2025 18:26:11 +0200 Subject: [PATCH 06/18] Fix clippy --- crates/sncast/src/helpers/block_explorer.rs | 3 +-- crates/sncast/src/helpers/devnet.rs | 19 +++++++++---------- crates/sncast/src/helpers/rpc.rs | 3 +-- crates/sncast/src/response/explorer_link.rs | 3 +-- .../src/starknet_commands/verify/voyager.rs | 2 +- 5 files changed, 13 insertions(+), 17 deletions(-) diff --git a/crates/sncast/src/helpers/block_explorer.rs b/crates/sncast/src/helpers/block_explorer.rs index c47377765d..bd4ed8b39d 100644 --- a/crates/sncast/src/helpers/block_explorer.rs +++ b/crates/sncast/src/helpers/block_explorer.rs @@ -37,9 +37,8 @@ pub trait LinkProvider { const fn network_subdomain(network: Network) -> &'static str { match network { - Network::Mainnet => "", Network::Sepolia => "sepolia.", - Network::Devnet => "", + Network::Mainnet | Network::Devnet => "", } } diff --git a/crates/sncast/src/helpers/devnet.rs b/crates/sncast/src/helpers/devnet.rs index 9b9fed5d07..2e7a2d9864 100644 --- a/crates/sncast/src/helpers/devnet.rs +++ b/crates/sncast/src/helpers/devnet.rs @@ -2,10 +2,12 @@ use std::net::TcpStream; use std::time::Duration; /// Detects devnet by scanning running processes for starknet-devnet and extracting the port +#[must_use] pub fn detect_devnet_url() -> String { detect_devnet_from_processes().unwrap_or_else(|| "http://localhost:5050".to_string()) } +#[must_use] pub fn is_devnet_running() -> bool { detect_devnet_from_processes().is_some() } @@ -13,13 +15,13 @@ pub fn is_devnet_running() -> bool { /// Detects devnet by scanning running processes for starknet-devnet and extracting the port fn detect_devnet_from_processes() -> Option { if let Some(port) = find_devnet_process_port() { - return Some(format!("http://localhost:{}", port)); + return Some(format!("http://localhost:{port}")); } let common_ports = [5050, 5000, 8545, 3000, 8000]; for &port in &common_ports { if is_port_reachable("localhost", port) { - return Some(format!("http://localhost:{}", port)); + return Some(format!("http://localhost:{port}")); } } @@ -29,7 +31,7 @@ fn detect_devnet_from_processes() -> Option { fn find_devnet_process_port() -> Option { use std::process::Command; - let output = Command::new("ps").args(&["aux"]).output().ok()?; + let output = Command::new("ps").args(["aux"]).output().ok()?; let ps_output = String::from_utf8_lossy(&output.stdout); for line in ps_output.lines() { @@ -72,7 +74,7 @@ fn try_lsof_for_port(pid: u32) -> Option { use std::process::Command; let output = Command::new("lsof") - .args(&["-P", "-p", &pid.to_string(), "-i"]) + .args(["-P", "-p", &pid.to_string(), "-i"]) .output() .ok()?; @@ -81,7 +83,7 @@ fn try_lsof_for_port(pid: u32) -> Option { for line in lsof_output.lines() { if line.contains("TCP") && line.contains("LISTEN") { if let Some(port_part) = line.split_whitespace().last() { - if let Some(port_str) = port_part.split(':').last() { + if let Some(port_str) = port_part.split(':').next_back() { if let Ok(port) = port_str.parse::() { return Some(port); } @@ -150,11 +152,8 @@ fn extract_port_from_cmdline(cmdline: &str) -> Option { } fn is_port_reachable(host: &str, port: u16) -> bool { - if let Ok(addr) = format!("{}:{}", host, port).parse() { - match TcpStream::connect_timeout(&addr, Duration::from_millis(50)) { - Ok(_) => true, - Err(_) => false, - } + if let Ok(addr) = format!("{host}:{port}").parse() { + TcpStream::connect_timeout(&addr, Duration::from_millis(50)).is_ok() } else { false } diff --git a/crates/sncast/src/helpers/rpc.rs b/crates/sncast/src/helpers/rpc.rs index 0ecc0d270b..843ffd6db8 100644 --- a/crates/sncast/src/helpers/rpc.rs +++ b/crates/sncast/src/helpers/rpc.rs @@ -16,8 +16,7 @@ pub struct RpcArgs { pub url: Option, /// Use predefined network with a public provider. Note that this option may result in rate limits or other unexpected behavior. - /// For devnet, attempts to auto-detect running starknet-devnet instances or falls back to http://localhost:5050. - /// If auto-detection fails or you're using a custom devnet address, use --url instead. + /// For devnet, attempts to auto-detect running starknet-devnet instance. If auto-detection fails, use --url instead. #[arg(long)] pub network: Option, } diff --git a/crates/sncast/src/response/explorer_link.rs b/crates/sncast/src/response/explorer_link.rs index f05c8e6a16..898975f1b3 100644 --- a/crates/sncast/src/response/explorer_link.rs +++ b/crates/sncast/src/response/explorer_link.rs @@ -1,12 +1,11 @@ use crate::Network; use crate::helpers::{ - block_explorer::LinkProvider, configuration::CastConfig, devnet, rpc::RpcArgs, + block_explorer::LinkProvider, configuration::CastConfig, devnet, }; use foundry_ui::Message; use serde::Serialize; use serde_json::{Value, json}; use starknet_types_core::felt::Felt; -use url::Url; const SNCAST_FORCE_SHOW_EXPLORER_LINKS_ENV: &str = "SNCAST_FORCE_SHOW_EXPLORER_LINKS"; diff --git a/crates/sncast/src/starknet_commands/verify/voyager.rs b/crates/sncast/src/starknet_commands/verify/voyager.rs index 310e11b0fb..79bf1e802f 100644 --- a/crates/sncast/src/starknet_commands/verify/voyager.rs +++ b/crates/sncast/src/starknet_commands/verify/voyager.rs @@ -392,7 +392,7 @@ impl<'a> VerificationInterface<'a> for Voyager<'a> { Err(_) => match self.network { Network::Mainnet => "https://api.voyager.online/beta".to_string(), Network::Sepolia => "https://sepolia-api.voyager.online/beta".to_string(), - Network::Devnet => "".to_string(), + Network::Devnet => String::new(), }, } } From 0e04f8123a653ee557d357da456d243753e73fd6 Mon Sep 17 00:00:00 2001 From: Maksymilian Kowalski Date: Fri, 3 Oct 2025 15:37:02 +0200 Subject: [PATCH 07/18] Rewrite url detection --- crates/sncast/src/helpers/devnet.rs | 319 ++++++++++++++++++---------- crates/sncast/src/helpers/rpc.rs | 2 +- 2 files changed, 203 insertions(+), 118 deletions(-) diff --git a/crates/sncast/src/helpers/devnet.rs b/crates/sncast/src/helpers/devnet.rs index 2e7a2d9864..031962038b 100644 --- a/crates/sncast/src/helpers/devnet.rs +++ b/crates/sncast/src/helpers/devnet.rs @@ -1,10 +1,8 @@ -use std::net::TcpStream; -use std::time::Duration; - -/// Detects devnet by scanning running processes for starknet-devnet and extracting the port #[must_use] -pub fn detect_devnet_url() -> String { - detect_devnet_from_processes().unwrap_or_else(|| "http://localhost:5050".to_string()) +pub fn detect_devnet_url() -> Result { + detect_devnet_from_processes().ok_or_else(|| { + "Could not detect running starknet-devnet instance. Please use --url instead.".to_string() + }) } #[must_use] @@ -12,169 +10,256 @@ pub fn is_devnet_running() -> bool { detect_devnet_from_processes().is_some() } -/// Detects devnet by scanning running processes for starknet-devnet and extracting the port fn detect_devnet_from_processes() -> Option { - if let Some(port) = find_devnet_process_port() { - return Some(format!("http://localhost:{port}")); - } - - let common_ports = [5050, 5000, 8545, 3000, 8000]; - for &port in &common_ports { - if is_port_reachable("localhost", port) { - return Some(format!("http://localhost:{port}")); - } + if let Some(info) = find_devnet_process_info() { + return Some(format!("http://{}:{}", info.host, info.port)); } None } -fn find_devnet_process_port() -> Option { +#[derive(Debug, Clone)] +struct DevnetInfo { + host: String, + port: u16, +} + +fn find_devnet_process_info() -> Option { use std::process::Command; let output = Command::new("ps").args(["aux"]).output().ok()?; let ps_output = String::from_utf8_lossy(&output.stdout); - for line in ps_output.lines() { - if line.contains("starknet-devnet") { - // First try to extract port from command line arguments (faster) - if let Some(port) = extract_port_from_cmdline(line) { - return Some(port); + ps_output + .lines() + .filter(|line| line.contains("starknet-devnet")) + .find_map(|line| { + if line.contains("docker") { + Some(extract_devnet_info_from_docker_line(line)) + } else { + Some(extract_devnet_info_from_cmdline(line)) } + }) +} - // If that fails, try to get port from PID - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() > 1 { - if let Ok(pid) = parts[1].parse::() { - if let Some(port) = get_port_from_pid(pid) { - return Some(port); - } - } - } +fn extract_string_from_flag(cmdline: &str, flag: &str) -> Option { + if let Some(pos) = cmdline.find(flag) { + let after_pattern = &cmdline[pos + flag.len()..]; + let value_str = after_pattern + .split_whitespace() + .next() + .unwrap_or("") + .trim_start_matches('=') + .trim_start_matches(':'); + + if !value_str.is_empty() { + return Some(value_str.to_string()); } } None } -#[cfg(any(target_os = "macos", target_os = "linux"))] -fn get_port_from_pid(pid: u32) -> Option { - if let Some(port) = try_lsof_for_port(pid) { - return Some(port); - } - - #[cfg(target_os = "linux")] - { - try_linux_nettools_for_port(pid) +fn extract_port_from_flag(cmdline: &str, flag: &str) -> Option { + if let Some(port_str) = extract_string_from_flag(cmdline, flag) { + if let Ok(p) = port_str.parse::() { + if p > 1024 && p < 65535 { + return Some(p); + } + } } - None } -#[cfg(any(target_os = "macos", target_os = "linux"))] -fn try_lsof_for_port(pid: u32) -> Option { - use std::process::Command; - - let output = Command::new("lsof") - .args(["-P", "-p", &pid.to_string(), "-i"]) - .output() - .ok()?; +fn extract_docker_port_mapping(cmdline: &str) -> Option<(String, u16)> { + if let Some(pos) = cmdline.find("-p ") { + let after_pattern = &cmdline[pos + 3..]; // "-p ".len() = 3 + let port_mapping = after_pattern.split_whitespace().next().unwrap_or(""); - let lsof_output = String::from_utf8_lossy(&output.stdout); - - for line in lsof_output.lines() { - if line.contains("TCP") && line.contains("LISTEN") { - if let Some(port_part) = line.split_whitespace().last() { - if let Some(port_str) = port_part.split(':').next_back() { - if let Ok(port) = port_str.parse::() { - return Some(port); - } - } + let parts: Vec<&str> = port_mapping.split(':').collect(); + if parts.len() == 3 { + if let Ok(external_port) = parts[1].parse::() { + return Some((parts[0].to_string(), external_port)); + } + } else if parts.len() == 2 { + if let Ok(external_port) = parts[0].parse::() { + return Some(("127.0.0.1".to_string(), external_port)); } } } None } -#[cfg(target_os = "linux")] -fn try_linux_nettools_for_port(pid: u32) -> Option { - use std::process::Command; +fn extract_devnet_info_from_docker_line(cmdline: &str) -> DevnetInfo { + let mut port = None; + let mut host = None; - let output = Command::new("ss") - .args(&["-tlnp"]) - .output() - .or_else(|_| Command::new("netstat").args(&["-tlnp"]).output()) - .ok()?; - - let net_output = String::from_utf8_lossy(&output.stdout); - - for line in net_output.lines() { - if line.contains(&pid.to_string()) { - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() > 3 { - if let Some(port_str) = parts[3].split(':').last() { - if let Ok(port) = port_str.parse::() { - return Some(port); - } - } - } - } + if let Some((docker_host, docker_port)) = extract_docker_port_mapping(cmdline) { + host = Some(docker_host); + port = Some(docker_port); + } + + if port.is_none() { + port = extract_port_from_flag(cmdline, "--port"); + } + + let final_host = host.unwrap_or_else(|| "127.0.0.1".to_string()); + let final_port = port.unwrap_or(5050); + + DevnetInfo { + host: final_host, + port: final_port, } - None } -fn extract_port_from_cmdline(cmdline: &str) -> Option { - let patterns = ["--port", "--host", ":", "localhost:"]; - - for pattern in &patterns { - if let Some(pos) = cmdline.find(pattern) { - let after_pattern = &cmdline[pos + pattern.len()..]; - let port_str = after_pattern - .split_whitespace() - .next() - .unwrap_or("") - .trim_start_matches('=') - .trim_start_matches(':'); - - if let Ok(port) = port_str.parse::() { - if port > 1000 && port < 65535 { - return Some(port); +fn extract_devnet_info_from_cmdline(cmdline: &str) -> DevnetInfo { + let mut port = extract_port_from_flag(cmdline, "--port"); + let mut host = extract_string_from_flag(cmdline, "--host"); + + if port.is_none() { + if let Ok(port_env) = std::env::var("PORT") { + if let Ok(p) = port_env.parse::() { + if p > 0 && p < 65535 { + port = Some(p); } } } } - for word in cmdline.split_whitespace() { - if let Ok(port) = word.parse::() { - return Some(port); + if host.is_none() { + if let Ok(host_env) = std::env::var("HOST") { + if !host_env.is_empty() { + host = Some(host_env); + } } } - None -} + let final_port = port.unwrap_or(5050); + let final_host = host.unwrap_or_else(|| "127.0.0.1".to_string()); -fn is_port_reachable(host: &str, port: u16) -> bool { - if let Ok(addr) = format!("{host}:{port}").parse() { - TcpStream::connect_timeout(&addr, Duration::from_millis(50)).is_ok() - } else { - false + DevnetInfo { + host: final_host, + port: final_port, } } #[cfg(test)] mod tests { use super::*; + use std::net::TcpStream; + use std::process::{Command, Stdio}; + use std::thread; + use std::time::{Duration, Instant}; + + #[test] + fn test_extract_devnet_info_from_cmdline() { + let cmdline1 = "starknet-devnet --port 5050 --host 127.0.0.1"; + let info1 = extract_devnet_info_from_cmdline(cmdline1); + assert_eq!(info1.port, 5050); + assert_eq!(info1.host, "127.0.0.1"); + + let cmdline2 = "/usr/bin/starknet-devnet --port=5000"; + let info2 = extract_devnet_info_from_cmdline(cmdline2); + assert_eq!(info2.port, 5000); + assert_eq!(info2.host, "127.0.0.1"); + + let cmdline3 = "starknet-devnet --host 127.0.0.1"; + let info3 = extract_devnet_info_from_cmdline(cmdline3); + assert_eq!(info3.port, 5050); + assert_eq!(info3.host, "127.0.0.1"); + } + + #[test] + fn test_extract_devnet_info_from_docker_line() { + let cmdline1 = "docker run -p 127.0.0.1:5055:5050 shardlabs/starknet-devnet-rs"; + let info1 = extract_devnet_info_from_docker_line(cmdline1); + assert_eq!(info1.port, 5055); + assert_eq!(info1.host, "127.0.0.1"); + + let cmdline2 = "docker run -p 8080:5050 shardlabs/starknet-devnet-rs"; + let info2 = extract_devnet_info_from_docker_line(cmdline2); + assert_eq!(info2.port, 8080); + assert_eq!(info2.host, "127.0.0.1"); + + let cmdline3 = "docker run --network host shardlabs/starknet-devnet-rs --port 5055"; + let info3 = extract_devnet_info_from_docker_line(cmdline3); + assert_eq!(info3.port, 5055); + assert_eq!(info3.host, "127.0.0.1"); + } + + #[test] + fn test_extract_devnet_info_with_both_envs() { + // SAFETY: Tests run in parallel and share the same environment variables. + // However, this modification applies only to this one test. + unsafe { + std::env::set_var("PORT", "8080"); + std::env::set_var("HOST", "0.0.0.0"); + } + + let cmdline = "starknet-devnet"; + let info = extract_devnet_info_from_cmdline(cmdline); + assert_eq!(info.port, 8080); + assert_eq!(info.host, "0.0.0.0"); + } #[test] - fn test_devnet_process_detection() { - let cmdline1 = "starknet-devnet --port 5050 --host localhost"; - assert_eq!(extract_port_from_cmdline(cmdline1), Some(5050)); + fn test_cmdline_args_override_env() { + // SAFETY: Tests run in parallel and share the same environment variables. + // However, this modification applies only to this one test. + unsafe { + std::env::set_var("PORT", "3000"); + std::env::set_var("HOST", "localhost"); + } + + let cmdline = "starknet-devnet --port 9999 --host 192.168.1.1"; + let info = extract_devnet_info_from_cmdline(cmdline); + assert_eq!(info.port, 9999); + assert_eq!(info.host, "192.168.1.1"); + } + + #[test] + fn test_detect_devnet_url() { + let child = spawn_devnet("5090"); + + let result = detect_devnet_url().expect("Failed to detect devnet URL"); + assert_eq!(result, "http://127.0.0.1:5090"); + + cleanup_process(child); + } - let cmdline3 = "/usr/bin/starknet-devnet --port=5000"; - assert_eq!(extract_port_from_cmdline(cmdline3), Some(5000)); + fn spawn_devnet(port: &str) -> std::process::Child { + let mut child = Command::new("starknet-devnet") + .args(["--port", port]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("Failed to spawn starknet-devnet process"); - // Test devnet URL generation - let devnet_url = detect_devnet_url(); - assert!(devnet_url.starts_with("http://localhost:")); + let port_num: u16 = port.parse().expect("Invalid port number"); + let start_time = Instant::now(); + let timeout = Duration::from_secs(10); - let _reachable = is_port_reachable("localhost", 5050); + while start_time.elapsed() < timeout { + if is_port_reachable("127.0.0.1", port_num) { + return child; + } + thread::sleep(Duration::from_millis(500)); + } + + let _ = child.kill(); + let _ = child.wait(); + panic!("Devnet did not start in time on port {port}"); + } + + fn cleanup_process(mut child: std::process::Child) { + child.kill().expect("Failed to kill devnet process"); + child.wait().expect("Failed to wait for devnet process"); + } + + fn is_port_reachable(host: &str, port: u16) -> bool { + if let Ok(addr) = format!("{host}:{port}").parse() { + TcpStream::connect_timeout(&addr, Duration::from_millis(50)).is_ok() + } else { + false + } } } diff --git a/crates/sncast/src/helpers/rpc.rs b/crates/sncast/src/helpers/rpc.rs index 843ffd6db8..7cbdd985db 100644 --- a/crates/sncast/src/helpers/rpc.rs +++ b/crates/sncast/src/helpers/rpc.rs @@ -92,7 +92,7 @@ impl Network { } fn free_devnet_rpc(_provider: &FreeProvider) -> String { - devnet::detect_devnet_url() + devnet::detect_devnet_url().unwrap() } } From a5510ddd456bdddb45dae7e9af82ac62cdb27cd2 Mon Sep 17 00:00:00 2001 From: Maksymilian Kowalski Date: Fri, 3 Oct 2025 16:19:14 +0200 Subject: [PATCH 08/18] Better error message --- crates/sncast/src/helpers/devnet.rs | 29 +++++++------- crates/sncast/src/helpers/rpc.rs | 39 ++++++++----------- .../src/starknet_commands/declare_from.rs | 2 +- .../src/starknet_commands/verify/mod.rs | 2 +- 4 files changed, 35 insertions(+), 37 deletions(-) diff --git a/crates/sncast/src/helpers/devnet.rs b/crates/sncast/src/helpers/devnet.rs index 031962038b..3e0283b791 100644 --- a/crates/sncast/src/helpers/devnet.rs +++ b/crates/sncast/src/helpers/devnet.rs @@ -1,8 +1,8 @@ +use std::{net::TcpStream, time::Duration}; + #[must_use] -pub fn detect_devnet_url() -> Result { - detect_devnet_from_processes().ok_or_else(|| { - "Could not detect running starknet-devnet instance. Please use --url instead.".to_string() - }) +pub fn detect_devnet_url() -> Option { + detect_devnet_from_processes() } #[must_use] @@ -15,6 +15,11 @@ fn detect_devnet_from_processes() -> Option { return Some(format!("http://{}:{}", info.host, info.port)); } + // Fallback to default 127.0.0.1:5050 if reachable + if is_port_reachable("127.0.0.1", 5050) { + return Some("http://127.0.0.1:5050".to_string()); + } + None } @@ -142,10 +147,16 @@ fn extract_devnet_info_from_cmdline(cmdline: &str) -> DevnetInfo { } } +fn is_port_reachable(host: &str, port: u16) -> bool { + if let Ok(addr) = format!("{host}:{port}").parse() { + TcpStream::connect_timeout(&addr, Duration::from_millis(50)).is_ok() + } else { + false + } +} #[cfg(test)] mod tests { use super::*; - use std::net::TcpStream; use std::process::{Command, Stdio}; use std::thread; use std::time::{Duration, Instant}; @@ -254,12 +265,4 @@ mod tests { child.kill().expect("Failed to kill devnet process"); child.wait().expect("Failed to wait for devnet process"); } - - fn is_port_reachable(host: &str, port: u16) -> bool { - if let Ok(addr) = format!("{host}:{port}").parse() { - TcpStream::connect_timeout(&addr, Duration::from_millis(50)).is_ok() - } else { - false - } - } } diff --git a/crates/sncast/src/helpers/rpc.rs b/crates/sncast/src/helpers/rpc.rs index 7cbdd985db..d6549a8891 100644 --- a/crates/sncast/src/helpers/rpc.rs +++ b/crates/sncast/src/helpers/rpc.rs @@ -33,9 +33,7 @@ impl RpcArgs { ) } - let url = self - .get_url(&config.url) - .context("Either `--network` or `--url` must be provided")?; + let url = self.get_url(&config.url)?; assert!(!url.is_empty(), "url cannot be empty"); let provider = get_provider(&url)?; @@ -45,19 +43,15 @@ impl RpcArgs { Ok(provider) } - #[must_use] - fn get_url(&self, config_url: &String) -> Option { - if let Some(network) = self.network { - let free_provider = FreeProvider::semi_random(); - Some(network.url(&free_provider)) - } else { - self.url.clone().or_else(|| { - if config_url.is_empty() { - None - } else { - Some(config_url.to_string()) - } - }) + fn get_url(&self, config_url: &str) -> Result { + match (&self.network, &self.url, config_url.is_empty()) { + (Some(network), None, _) => { + let free_provider = FreeProvider::semi_random(); + network.url(&free_provider) + } + (None, Some(url), _) => Ok(url.clone()), + (None, None, false) => Ok(config_url.to_string()), + _ => bail!("Either `--network` or `--url` must be provided"), } } } @@ -74,11 +68,10 @@ impl FreeProvider { } impl Network { - #[must_use] - pub fn url(self, provider: &FreeProvider) -> String { + pub fn url(self, provider: &FreeProvider) -> Result { match self { - Network::Mainnet => Self::free_mainnet_rpc(provider), - Network::Sepolia => Self::free_sepolia_rpc(provider), + Network::Mainnet => Ok(Self::free_mainnet_rpc(provider)), + Network::Sepolia => Ok(Self::free_sepolia_rpc(provider)), Network::Devnet => Self::free_devnet_rpc(provider), } } @@ -91,8 +84,10 @@ impl Network { format!("https://starknet-sepolia.public.blastapi.io/rpc/{RPC_URL_VERSION}") } - fn free_devnet_rpc(_provider: &FreeProvider) -> String { - devnet::detect_devnet_url().unwrap() + fn free_devnet_rpc(_provider: &FreeProvider) -> Result { + devnet::detect_devnet_url().with_context( + || "Could not detect running starknet-devnet instance. Please use --url instead.", + ) } } diff --git a/crates/sncast/src/starknet_commands/declare_from.rs b/crates/sncast/src/starknet_commands/declare_from.rs index b2b68a8204..ec4d5c7563 100644 --- a/crates/sncast/src/starknet_commands/declare_from.rs +++ b/crates/sncast/src/starknet_commands/declare_from.rs @@ -74,7 +74,7 @@ impl SourceRpcArgs { fn get_url(&self) -> Option { if let Some(network) = self.source_network { let free_provider = FreeProvider::semi_random(); - Some(network.url(&free_provider)) + Some(network.url(&free_provider).ok()?) } else { self.source_url .as_ref() diff --git a/crates/sncast/src/starknet_commands/verify/mod.rs b/crates/sncast/src/starknet_commands/verify/mod.rs index 2d18d43aec..c087c6db23 100644 --- a/crates/sncast/src/starknet_commands/verify/mod.rs +++ b/crates/sncast/src/starknet_commands/verify/mod.rs @@ -142,7 +142,7 @@ pub async fn verify( if config.url.is_empty() { let network = network.ok_or_else(|| anyhow!("Either --network or --url must be provided"))?; - let free_rpc_provider = network.url(&FreeProvider::semi_random()); + let free_rpc_provider = network.url(&FreeProvider::semi_random())?; Url::parse(&free_rpc_provider)? } else { Url::parse(&config.url)? From a18a02d89bf70b12e2dd9be735a23751c881f28d Mon Sep 17 00:00:00 2001 From: Maksymilian Kowalski Date: Fri, 3 Oct 2025 16:57:45 +0200 Subject: [PATCH 09/18] Fix tests --- crates/sncast/src/helpers/devnet.rs | 52 ++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/crates/sncast/src/helpers/devnet.rs b/crates/sncast/src/helpers/devnet.rs index 3e0283b791..025f7e31eb 100644 --- a/crates/sncast/src/helpers/devnet.rs +++ b/crates/sncast/src/helpers/devnet.rs @@ -38,13 +38,14 @@ fn find_devnet_process_info() -> Option { ps_output .lines() .filter(|line| line.contains("starknet-devnet")) - .find_map(|line| { + .map(|line| { if line.contains("docker") { - Some(extract_devnet_info_from_docker_line(line)) + extract_devnet_info_from_docker_line(line) } else { - Some(extract_devnet_info_from_cmdline(line)) + extract_devnet_info_from_cmdline(line) } }) + .next() } fn extract_string_from_flag(cmdline: &str, flag: &str) -> Option { @@ -161,7 +162,20 @@ mod tests { use std::thread; use std::time::{Duration, Instant}; + // Those tests are marked to run serially to avoid interference from env vars #[test] + fn test_devnet_parsing() { + test_extract_devnet_info_from_cmdline(); + + test_extract_devnet_info_from_docker_line(); + + test_extract_devnet_info_with_both_envs(); + + test_cmdline_args_override_env(); + + test_detect_devnet_url(); + } + fn test_extract_devnet_info_from_cmdline() { let cmdline1 = "starknet-devnet --port 5050 --host 127.0.0.1"; let info1 = extract_devnet_info_from_cmdline(cmdline1); @@ -179,7 +193,6 @@ mod tests { assert_eq!(info3.host, "127.0.0.1"); } - #[test] fn test_extract_devnet_info_from_docker_line() { let cmdline1 = "docker run -p 127.0.0.1:5055:5050 shardlabs/starknet-devnet-rs"; let info1 = extract_devnet_info_from_docker_line(cmdline1); @@ -197,37 +210,44 @@ mod tests { assert_eq!(info3.host, "127.0.0.1"); } - #[test] fn test_extract_devnet_info_with_both_envs() { - // SAFETY: Tests run in parallel and share the same environment variables. - // However, this modification applies only to this one test. + // SAFETY: Variables are only modified within this test and cleaned up afterwards unsafe { - std::env::set_var("PORT", "8080"); - std::env::set_var("HOST", "0.0.0.0"); + std::env::set_var("PORT", "9999"); + std::env::set_var("HOST", "9.9.9.9"); } let cmdline = "starknet-devnet"; let info = extract_devnet_info_from_cmdline(cmdline); - assert_eq!(info.port, 8080); - assert_eq!(info.host, "0.0.0.0"); + assert_eq!(info.port, 9999); + assert_eq!(info.host, "9.9.9.9"); + + // SAFETY: Clean up environment variables to prevent interference + unsafe { + std::env::remove_var("PORT"); + std::env::remove_var("HOST"); + } } - #[test] fn test_cmdline_args_override_env() { - // SAFETY: Tests run in parallel and share the same environment variables. - // However, this modification applies only to this one test. + // SAFETY: Variables are only modified within this test and cleaned up afterwards unsafe { std::env::set_var("PORT", "3000"); - std::env::set_var("HOST", "localhost"); + std::env::set_var("HOST", "7.7.7.7"); } let cmdline = "starknet-devnet --port 9999 --host 192.168.1.1"; let info = extract_devnet_info_from_cmdline(cmdline); assert_eq!(info.port, 9999); assert_eq!(info.host, "192.168.1.1"); + + // SAFETY: Clean up environment variables to prevent interference + unsafe { + std::env::remove_var("PORT"); + std::env::remove_var("HOST"); + } } - #[test] fn test_detect_devnet_url() { let child = spawn_devnet("5090"); From 7299ae09370022bfa17d527e673587018b2d88cd Mon Sep 17 00:00:00 2001 From: Maksymilian Kowalski Date: Fri, 3 Oct 2025 17:07:43 +0200 Subject: [PATCH 10/18] Rename --- crates/sncast/src/helpers/{devnet.rs => devnet_detection.rs} | 0 crates/sncast/src/helpers/mod.rs | 2 +- crates/sncast/src/helpers/rpc.rs | 4 ++-- crates/sncast/src/response/explorer_link.rs | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) rename crates/sncast/src/helpers/{devnet.rs => devnet_detection.rs} (100%) diff --git a/crates/sncast/src/helpers/devnet.rs b/crates/sncast/src/helpers/devnet_detection.rs similarity index 100% rename from crates/sncast/src/helpers/devnet.rs rename to crates/sncast/src/helpers/devnet_detection.rs diff --git a/crates/sncast/src/helpers/mod.rs b/crates/sncast/src/helpers/mod.rs index cb97500c82..9f59796cd5 100644 --- a/crates/sncast/src/helpers/mod.rs +++ b/crates/sncast/src/helpers/mod.rs @@ -5,7 +5,7 @@ pub mod command; pub mod config; pub mod configuration; pub mod constants; -pub mod devnet; +pub mod devnet_detection; pub mod fee; pub mod interactive; pub mod output_format; diff --git a/crates/sncast/src/helpers/rpc.rs b/crates/sncast/src/helpers/rpc.rs index d6549a8891..04ba9060b0 100644 --- a/crates/sncast/src/helpers/rpc.rs +++ b/crates/sncast/src/helpers/rpc.rs @@ -1,5 +1,5 @@ use crate::helpers::configuration::CastConfig; -use crate::helpers::devnet; +use crate::helpers::devnet_detection; use crate::{Network, get_provider}; use anyhow::{Context, Result, bail}; use clap::Args; @@ -85,7 +85,7 @@ impl Network { } fn free_devnet_rpc(_provider: &FreeProvider) -> Result { - devnet::detect_devnet_url().with_context( + devnet_detection::detect_devnet_url().with_context( || "Could not detect running starknet-devnet instance. Please use --url instead.", ) } diff --git a/crates/sncast/src/response/explorer_link.rs b/crates/sncast/src/response/explorer_link.rs index 898975f1b3..7c3a7ed859 100644 --- a/crates/sncast/src/response/explorer_link.rs +++ b/crates/sncast/src/response/explorer_link.rs @@ -1,6 +1,6 @@ use crate::Network; use crate::helpers::{ - block_explorer::LinkProvider, configuration::CastConfig, devnet, + block_explorer::LinkProvider, configuration::CastConfig, devnet_detection, }; use foundry_ui::Message; use serde::Serialize; @@ -70,7 +70,7 @@ where let network = chain_id.try_into().ok()?; let is_devnet_network = matches!(network, Network::Devnet); - let is_devnet_running = devnet::is_devnet_running(); + let is_devnet_running = devnet_detection::is_devnet_running(); let is_devnet = is_devnet_network || is_devnet_running; if (!config.show_explorer_links || is_devnet) && !is_explorer_link_overridden() { From f4b31eac5c6ccd035696ddf78ad7a48f9c0e1e4f Mon Sep 17 00:00:00 2001 From: Maksymilian Kowalski Date: Fri, 3 Oct 2025 17:38:14 +0200 Subject: [PATCH 11/18] Fix new clippy --- crates/sncast/src/helpers/devnet_detection.rs | 55 +++++++++---------- crates/sncast/src/response/explorer_link.rs | 8 +-- 2 files changed, 29 insertions(+), 34 deletions(-) diff --git a/crates/sncast/src/helpers/devnet_detection.rs b/crates/sncast/src/helpers/devnet_detection.rs index 025f7e31eb..458f7fe1a7 100644 --- a/crates/sncast/src/helpers/devnet_detection.rs +++ b/crates/sncast/src/helpers/devnet_detection.rs @@ -66,13 +66,14 @@ fn extract_string_from_flag(cmdline: &str, flag: &str) -> Option { } fn extract_port_from_flag(cmdline: &str, flag: &str) -> Option { - if let Some(port_str) = extract_string_from_flag(cmdline, flag) { - if let Ok(p) = port_str.parse::() { - if p > 1024 && p < 65535 { - return Some(p); - } - } + if let Some(port_str) = extract_string_from_flag(cmdline, flag) + && let Ok(p) = port_str.parse::() + && p > 1024 + && p < 65535 + { + return Some(p); } + None } @@ -82,14 +83,14 @@ fn extract_docker_port_mapping(cmdline: &str) -> Option<(String, u16)> { let port_mapping = after_pattern.split_whitespace().next().unwrap_or(""); let parts: Vec<&str> = port_mapping.split(':').collect(); - if parts.len() == 3 { - if let Ok(external_port) = parts[1].parse::() { - return Some((parts[0].to_string(), external_port)); - } - } else if parts.len() == 2 { - if let Ok(external_port) = parts[0].parse::() { - return Some(("127.0.0.1".to_string(), external_port)); - } + if parts.len() == 3 + && let Ok(external_port) = parts[1].parse::() + { + return Some((parts[0].to_string(), external_port)); + } else if parts.len() == 2 + && let Ok(external_port) = parts[0].parse::() + { + return Some(("127.0.0.1".to_string(), external_port)); } } None @@ -121,22 +122,20 @@ fn extract_devnet_info_from_cmdline(cmdline: &str) -> DevnetInfo { let mut port = extract_port_from_flag(cmdline, "--port"); let mut host = extract_string_from_flag(cmdline, "--host"); - if port.is_none() { - if let Ok(port_env) = std::env::var("PORT") { - if let Ok(p) = port_env.parse::() { - if p > 0 && p < 65535 { - port = Some(p); - } - } - } + if port.is_none() + && let Ok(port_env) = std::env::var("PORT") + && let Ok(p) = port_env.parse::() + && p > 1024 + && p < 65535 + { + port = Some(p); } - if host.is_none() { - if let Ok(host_env) = std::env::var("HOST") { - if !host_env.is_empty() { - host = Some(host_env); - } - } + if host.is_none() + && let Ok(host_env) = std::env::var("HOST") + && !host_env.is_empty() + { + host = Some(host_env); } let final_port = port.unwrap_or(5050); diff --git a/crates/sncast/src/response/explorer_link.rs b/crates/sncast/src/response/explorer_link.rs index 7c3a7ed859..a9aa0367d0 100644 --- a/crates/sncast/src/response/explorer_link.rs +++ b/crates/sncast/src/response/explorer_link.rs @@ -1,7 +1,5 @@ use crate::Network; -use crate::helpers::{ - block_explorer::LinkProvider, configuration::CastConfig, devnet_detection, -}; +use crate::helpers::{block_explorer::LinkProvider, configuration::CastConfig, devnet_detection}; use foundry_ui::Message; use serde::Serialize; use serde_json::{Value, json}; @@ -69,9 +67,7 @@ where let network = chain_id.try_into().ok()?; - let is_devnet_network = matches!(network, Network::Devnet); - let is_devnet_running = devnet_detection::is_devnet_running(); - let is_devnet = is_devnet_network || is_devnet_running; + let is_devnet = matches!(network, Network::Devnet) || devnet_detection::is_devnet_running(); if (!config.show_explorer_links || is_devnet) && !is_explorer_link_overridden() { return None; From 2d0aa6c1c992363e7711447ae2d43981572f21e5 Mon Sep 17 00:00:00 2001 From: Maksymilian Kowalski Date: Mon, 6 Oct 2025 12:57:30 +0200 Subject: [PATCH 12/18] Change docs --- crates/sncast/src/helpers/devnet_detection.rs | 1 + docs/src/starknet/sncast-overview.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/sncast/src/helpers/devnet_detection.rs b/crates/sncast/src/helpers/devnet_detection.rs index 458f7fe1a7..313c024f82 100644 --- a/crates/sncast/src/helpers/devnet_detection.rs +++ b/crates/sncast/src/helpers/devnet_detection.rs @@ -154,6 +154,7 @@ fn is_port_reachable(host: &str, port: u16) -> bool { false } } + #[cfg(test)] mod tests { use super::*; diff --git a/docs/src/starknet/sncast-overview.md b/docs/src/starknet/sncast-overview.md index 3c71f6c97a..52cb7aec45 100644 --- a/docs/src/starknet/sncast-overview.md +++ b/docs/src/starknet/sncast-overview.md @@ -60,7 +60,7 @@ The `--network` flag supports the following networks: - **mainnet** - Connects to Starknet mainnet using a free RPC provider - **sepolia** - Connects to Starknet Sepolia testnet using a free RPC provider -- **devnet** - Attempts to auto-detect running starknet-devnet instances or falls back to `http://localhost:5050` +- **devnet** - Attempts to auto-detect running starknet-devnet instances When using **mainnet** or **sepolia**, `sncast` will randomly select one of the free RPC providers. When using free providers you may experience rate limits and other unexpected behavior. From eafe1cade7d5b8661b635a5030fad4b83cde6e41 Mon Sep 17 00:00:00 2001 From: Maksymilian Kowalski Date: Mon, 6 Oct 2025 16:51:42 +0200 Subject: [PATCH 13/18] Return error when two instances found --- crates/sncast/src/helpers/devnet_detection.rs | 84 ++++++++++++------- crates/sncast/src/helpers/rpc.rs | 6 +- 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/crates/sncast/src/helpers/devnet_detection.rs b/crates/sncast/src/helpers/devnet_detection.rs index 313c024f82..8d70ac6cd7 100644 --- a/crates/sncast/src/helpers/devnet_detection.rs +++ b/crates/sncast/src/helpers/devnet_detection.rs @@ -1,51 +1,68 @@ -use std::{net::TcpStream, time::Duration}; +use std::process::Command; -#[must_use] -pub fn detect_devnet_url() -> Option { +#[derive(Debug, Clone)] +struct DevnetInfo { + host: String, + port: u16, +} + +#[derive(Debug)] +enum FindDevnetError { + None, + Multiple, + CommandFailed, +} + +pub fn detect_devnet_url() -> Result { detect_devnet_from_processes() } #[must_use] pub fn is_devnet_running() -> bool { - detect_devnet_from_processes().is_some() + detect_devnet_from_processes().is_ok() } -fn detect_devnet_from_processes() -> Option { - if let Some(info) = find_devnet_process_info() { - return Some(format!("http://{}:{}", info.host, info.port)); - } - - // Fallback to default 127.0.0.1:5050 if reachable - if is_port_reachable("127.0.0.1", 5050) { - return Some("http://127.0.0.1:5050".to_string()); +fn detect_devnet_from_processes() -> Result { + match find_devnet_process_info() { + Ok(info) => Ok(format!("http://{}:{}", info.host, info.port)), + Err(FindDevnetError::Multiple) => { + Err("Multiple starknet-devnet instances found. Please use --url to specify which one to use.".to_string()) + } + Err(FindDevnetError::None | FindDevnetError::CommandFailed) => { + // Fallback to default starknet- + if is_port_reachable("127.0.0.1", 5050) { + Ok("http://127.0.0.1:5050".to_string()) + } else { + Err("Could not detect running starknet-devnet instance. Please use --url instead.".to_string()) + } + } } - - None } -#[derive(Debug, Clone)] -struct DevnetInfo { - host: String, - port: u16, -} - -fn find_devnet_process_info() -> Option { - use std::process::Command; - - let output = Command::new("ps").args(["aux"]).output().ok()?; +fn find_devnet_process_info() -> Result { + let output = Command::new("ps") + .args(["aux"]) + .output() + .map_err(|_| FindDevnetError::CommandFailed)?; let ps_output = String::from_utf8_lossy(&output.stdout); - ps_output + let devnet_processes: Vec = ps_output .lines() .filter(|line| line.contains("starknet-devnet")) .map(|line| { - if line.contains("docker") { + if line.contains("docker") || line.contains("podman") { extract_devnet_info_from_docker_line(line) } else { extract_devnet_info_from_cmdline(line) } }) - .next() + .collect(); + + match devnet_processes.as_slice() { + [] => Err(FindDevnetError::None), + [single] => Ok(single.clone()), + _ => Err(FindDevnetError::Multiple), + } } fn extract_string_from_flag(cmdline: &str, flag: &str) -> Option { @@ -148,11 +165,14 @@ fn extract_devnet_info_from_cmdline(cmdline: &str) -> DevnetInfo { } fn is_port_reachable(host: &str, port: u16) -> bool { - if let Ok(addr) = format!("{host}:{port}").parse() { - TcpStream::connect_timeout(&addr, Duration::from_millis(50)).is_ok() - } else { - false - } + let url = format!("http://{host}:{port}/is_alive"); + + // TODO: Try to use a DevnetProvider::ensure_alive() from https://github.com/foundry-rs/starknet-foundry/pull/3760/ + std::process::Command::new("curl") + .args(["-s", "-f", "--max-time", "1", &url]) + .output() + .map(|output| output.status.success()) + .unwrap_or(false) } #[cfg(test)] diff --git a/crates/sncast/src/helpers/rpc.rs b/crates/sncast/src/helpers/rpc.rs index 04ba9060b0..7d6046e28d 100644 --- a/crates/sncast/src/helpers/rpc.rs +++ b/crates/sncast/src/helpers/rpc.rs @@ -1,7 +1,7 @@ use crate::helpers::configuration::CastConfig; use crate::helpers::devnet_detection; use crate::{Network, get_provider}; -use anyhow::{Context, Result, bail}; +use anyhow::{Result, bail}; use clap::Args; use foundry_ui::UI; use shared::consts::RPC_URL_VERSION; @@ -85,9 +85,7 @@ impl Network { } fn free_devnet_rpc(_provider: &FreeProvider) -> Result { - devnet_detection::detect_devnet_url().with_context( - || "Could not detect running starknet-devnet instance. Please use --url instead.", - ) + devnet_detection::detect_devnet_url().map_err(|e| anyhow::anyhow!(e)) } } From a9b1b5139952938676b1eab8f1b3a840266a128f Mon Sep 17 00:00:00 2001 From: Maksymilian Kowalski Date: Mon, 6 Oct 2025 17:21:57 +0200 Subject: [PATCH 14/18] Merge fixes --- crates/sncast/src/helpers/devnet_detection.rs | 2 +- crates/sncast/src/helpers/rpc.rs | 4 ++-- crates/sncast/src/main.rs | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/sncast/src/helpers/devnet_detection.rs b/crates/sncast/src/helpers/devnet_detection.rs index 8d70ac6cd7..80306b54cc 100644 --- a/crates/sncast/src/helpers/devnet_detection.rs +++ b/crates/sncast/src/helpers/devnet_detection.rs @@ -167,7 +167,7 @@ fn extract_devnet_info_from_cmdline(cmdline: &str) -> DevnetInfo { fn is_port_reachable(host: &str, port: u16) -> bool { let url = format!("http://{host}:{port}/is_alive"); - // TODO: Try to use a DevnetProvider::ensure_alive() from https://github.com/foundry-rs/starknet-foundry/pull/3760/ + // TODO: Try to use a DevnetProvider::ensure_alive() from https://github.com/foundry-rs/starknet-foundry/blob/master/crates/sncast/src/helpers/devnet_provider.rs#L82 std::process::Command::new("curl") .args(["-s", "-f", "--max-time", "1", &url]) .output() diff --git a/crates/sncast/src/helpers/rpc.rs b/crates/sncast/src/helpers/rpc.rs index 2f7059cf0c..887a3ffb1b 100644 --- a/crates/sncast/src/helpers/rpc.rs +++ b/crates/sncast/src/helpers/rpc.rs @@ -72,7 +72,7 @@ impl Network { match self { Network::Mainnet => Ok(Self::free_mainnet_rpc(provider)), Network::Sepolia => Ok(Self::free_sepolia_rpc(provider)), - Network::Devnet => Self::free_devnet_rpc(provider), + Network::Devnet => Self::devnet_rpc(provider), } } @@ -84,7 +84,7 @@ impl Network { format!("https://starknet-sepolia.public.blastapi.io/rpc/{RPC_URL_VERSION}") } - fn free_devnet_rpc(_provider: &FreeProvider) -> Result { + fn devnet_rpc(_provider: &FreeProvider) -> Result { devnet_detection::detect_devnet_url().map_err(|e| anyhow::anyhow!(e)) } } diff --git a/crates/sncast/src/main.rs b/crates/sncast/src/main.rs index dd624628b5..19ca0fb2d7 100644 --- a/crates/sncast/src/main.rs +++ b/crates/sncast/src/main.rs @@ -266,6 +266,8 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()> Commands::Declare(declare) => { let provider = declare.rpc.get_provider(&config, ui).await?; + let rpc = declare.rpc.clone(); + let account = get_account( &config.account, &config.accounts_file, @@ -318,7 +320,7 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()> serde_json::from_str(&contract_artifacts.sierra) .context("Failed to parse sierra artifact")?; let network_flag = generate_network_flag( - rpc.get_url(&config.url).as_deref(), + rpc.get_url(&config.url).ok().as_deref(), rpc.network.as_ref(), ); Some(DeployCommandMessage::new( From 016fc2ee3c7ee025c4ac873ddac7bc1d8b5ac78a Mon Sep 17 00:00:00 2001 From: Maksymilian Kowalski Date: Mon, 6 Oct 2025 17:25:53 +0200 Subject: [PATCH 15/18] Fix comments --- crates/sncast/src/helpers/devnet_detection.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/sncast/src/helpers/devnet_detection.rs b/crates/sncast/src/helpers/devnet_detection.rs index 80306b54cc..969fea9758 100644 --- a/crates/sncast/src/helpers/devnet_detection.rs +++ b/crates/sncast/src/helpers/devnet_detection.rs @@ -29,7 +29,7 @@ fn detect_devnet_from_processes() -> Result { Err("Multiple starknet-devnet instances found. Please use --url to specify which one to use.".to_string()) } Err(FindDevnetError::None | FindDevnetError::CommandFailed) => { - // Fallback to default starknet- + // Fallback to default starknet-devnet URL if reachable if is_port_reachable("127.0.0.1", 5050) { Ok("http://127.0.0.1:5050".to_string()) } else { @@ -182,7 +182,7 @@ mod tests { use std::thread; use std::time::{Duration, Instant}; - // Those tests are marked to run serially to avoid interference from env vars + // These tests are marked to run serially to avoid interference from environment variables #[test] fn test_devnet_parsing() { test_extract_devnet_info_from_cmdline(); From 7793096184da7f4cab3cffaba8cb9b8c07a1af13 Mon Sep 17 00:00:00 2001 From: Maksymilian Kowalski Date: Tue, 7 Oct 2025 10:48:00 +0200 Subject: [PATCH 16/18] Prepare to review --- crates/sncast/src/helpers/devnet_detection.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/sncast/src/helpers/devnet_detection.rs b/crates/sncast/src/helpers/devnet_detection.rs index 969fea9758..87bd81ce70 100644 --- a/crates/sncast/src/helpers/devnet_detection.rs +++ b/crates/sncast/src/helpers/devnet_detection.rs @@ -33,7 +33,10 @@ fn detect_devnet_from_processes() -> Result { if is_port_reachable("127.0.0.1", 5050) { Ok("http://127.0.0.1:5050".to_string()) } else { - Err("Could not detect running starknet-devnet instance. Please use --url instead.".to_string()) + Err( + "Could not detect running starknet-devnet instance. Please use --url instead." + .to_string(), + ) } } } @@ -167,7 +170,6 @@ fn extract_devnet_info_from_cmdline(cmdline: &str) -> DevnetInfo { fn is_port_reachable(host: &str, port: u16) -> bool { let url = format!("http://{host}:{port}/is_alive"); - // TODO: Try to use a DevnetProvider::ensure_alive() from https://github.com/foundry-rs/starknet-foundry/blob/master/crates/sncast/src/helpers/devnet_provider.rs#L82 std::process::Command::new("curl") .args(["-s", "-f", "--max-time", "1", &url]) .output() From 7aaf4d4303a9669d8d25c62831130f5e3806665b Mon Sep 17 00:00:00 2001 From: Maksymilian Kowalski Date: Tue, 7 Oct 2025 11:37:44 +0200 Subject: [PATCH 17/18] Rewrite docker data extracting --- crates/sncast/src/helpers/devnet_detection.rs | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/crates/sncast/src/helpers/devnet_detection.rs b/crates/sncast/src/helpers/devnet_detection.rs index 87bd81ce70..2802f95dde 100644 --- a/crates/sncast/src/helpers/devnet_detection.rs +++ b/crates/sncast/src/helpers/devnet_detection.rs @@ -97,22 +97,24 @@ fn extract_port_from_flag(cmdline: &str, flag: &str) -> Option { None } -fn extract_docker_port_mapping(cmdline: &str) -> Option<(String, u16)> { - if let Some(pos) = cmdline.find("-p ") { - let after_pattern = &cmdline[pos + 3..]; // "-p ".len() = 3 - let port_mapping = after_pattern.split_whitespace().next().unwrap_or(""); - - let parts: Vec<&str> = port_mapping.split(':').collect(); - if parts.len() == 3 - && let Ok(external_port) = parts[1].parse::() - { - return Some((parts[0].to_string(), external_port)); - } else if parts.len() == 2 - && let Ok(external_port) = parts[0].parse::() - { - return Some(("127.0.0.1".to_string(), external_port)); +fn extract_docker_mapping(cmdline: &str) -> Option<(String, u16)> { + let port_flags = ["-p", "--publish"]; + + for flag in &port_flags { + if let Some(port_mapping) = extract_string_from_flag(cmdline, flag) { + let parts: Vec<&str> = port_mapping.split(':').collect(); + if parts.len() == 3 + && let Ok(host_port) = parts[1].parse::() + { + return Some((parts[0].to_string(), host_port)); + } else if parts.len() == 2 + && let Ok(host_port) = parts[0].parse::() + { + return Some(("127.0.0.1".to_string(), host_port)); + } } } + None } @@ -120,7 +122,7 @@ fn extract_devnet_info_from_docker_line(cmdline: &str) -> DevnetInfo { let mut port = None; let mut host = None; - if let Some((docker_host, docker_port)) = extract_docker_port_mapping(cmdline) { + if let Some((docker_host, docker_port)) = extract_docker_mapping(cmdline) { host = Some(docker_host); port = Some(docker_port); } @@ -199,9 +201,9 @@ mod tests { } fn test_extract_devnet_info_from_cmdline() { - let cmdline1 = "starknet-devnet --port 5050 --host 127.0.0.1"; + let cmdline1 = "starknet-devnet --port 6000 --host 127.0.0.1"; let info1 = extract_devnet_info_from_cmdline(cmdline1); - assert_eq!(info1.port, 5050); + assert_eq!(info1.port, 6000); assert_eq!(info1.host, "127.0.0.1"); let cmdline2 = "/usr/bin/starknet-devnet --port=5000"; @@ -221,7 +223,7 @@ mod tests { assert_eq!(info1.port, 5055); assert_eq!(info1.host, "127.0.0.1"); - let cmdline2 = "docker run -p 8080:5050 shardlabs/starknet-devnet-rs"; + let cmdline2 = "docker run --publish 8080:5050 shardlabs/starknet-devnet-rs"; let info2 = extract_devnet_info_from_docker_line(cmdline2); assert_eq!(info2.port, 8080); assert_eq!(info2.host, "127.0.0.1"); From f73163e2192986688fafbcbed7249be9848888f2 Mon Sep 17 00:00:00 2001 From: Maksymilian Kowalski Date: Tue, 7 Oct 2025 12:17:31 +0200 Subject: [PATCH 18/18] Update docs --- docs/src/appendix/sncast/account/create.md | 2 +- docs/src/appendix/sncast/account/delete.md | 2 +- docs/src/appendix/sncast/account/deploy.md | 2 +- docs/src/appendix/sncast/account/import.md | 2 +- docs/src/appendix/sncast/call.md | 2 +- docs/src/appendix/sncast/declare.md | 2 +- docs/src/appendix/sncast/declare_from.md | 4 ++-- docs/src/appendix/sncast/deploy.md | 2 +- docs/src/appendix/sncast/invoke.md | 2 +- docs/src/appendix/sncast/multicall/run.md | 2 +- docs/src/appendix/sncast/script/run.md | 2 +- docs/src/appendix/sncast/show_config.md | 2 +- docs/src/appendix/sncast/tx-status.md | 2 +- docs/src/appendix/sncast/utils/serialize.md | 2 +- 14 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/src/appendix/sncast/account/create.md b/docs/src/appendix/sncast/account/create.md index e6cb0d631a..52b6cd87b4 100644 --- a/docs/src/appendix/sncast/account/create.md +++ b/docs/src/appendix/sncast/account/create.md @@ -23,7 +23,7 @@ Optional. Use predefined network with a public provider -Possible values: `mainnet`, `sepolia`. +Possible values: `mainnet`, `sepolia`, `devnet`. ## `--type, -t ` Optional. Required if `--class-hash` is passed. diff --git a/docs/src/appendix/sncast/account/delete.md b/docs/src/appendix/sncast/account/delete.md index 3dfadee9ae..7f419405ec 100644 --- a/docs/src/appendix/sncast/account/delete.md +++ b/docs/src/appendix/sncast/account/delete.md @@ -18,7 +18,7 @@ Optional. Use predefined network with a public provider -Possible values: `mainnet`, `sepolia`. +Possible values: `mainnet`, `sepolia`, `devnet`. ## `--network-name` Optional. diff --git a/docs/src/appendix/sncast/account/deploy.md b/docs/src/appendix/sncast/account/deploy.md index 7d51c45419..01095cbdaa 100644 --- a/docs/src/appendix/sncast/account/deploy.md +++ b/docs/src/appendix/sncast/account/deploy.md @@ -18,7 +18,7 @@ Optional. Use predefined network with a public provider -Possible values: `mainnet`, `sepolia`. +Possible values: `mainnet`, `sepolia`, `devnet`. ## `--max-fee, -m ` Optional. diff --git a/docs/src/appendix/sncast/account/import.md b/docs/src/appendix/sncast/account/import.md index 474c52d89a..e3d21d39a8 100644 --- a/docs/src/appendix/sncast/account/import.md +++ b/docs/src/appendix/sncast/account/import.md @@ -37,7 +37,7 @@ Optional. Use predefined network with public provider -Possible values: `mainnet`, `sepolia`. +Possible values: `mainnet`, `sepolia`, `devnet`. ## `--class-hash, -c ` Optional. diff --git a/docs/src/appendix/sncast/call.md b/docs/src/appendix/sncast/call.md index d7ad3daa89..5acae1a729 100644 --- a/docs/src/appendix/sncast/call.md +++ b/docs/src/appendix/sncast/call.md @@ -23,7 +23,7 @@ Optional. Use predefined network with public provider -Possible values: `mainnet`, `sepolia`. +Possible values: `mainnet`, `sepolia`, `devnet`. ## `--calldata, -c ` Optional. diff --git a/docs/src/appendix/sncast/declare.md b/docs/src/appendix/sncast/declare.md index a1d6e51193..cb94a52c6f 100644 --- a/docs/src/appendix/sncast/declare.md +++ b/docs/src/appendix/sncast/declare.md @@ -22,7 +22,7 @@ Optional. Use predefined network with public provider -Possible values: `mainnet`, `sepolia`. +Possible values: `mainnet`, `sepolia`, `devnet`. ## `--max-fee, -m ` Optional. diff --git a/docs/src/appendix/sncast/declare_from.md b/docs/src/appendix/sncast/declare_from.md index fc8a380d3c..57cf1847c1 100644 --- a/docs/src/appendix/sncast/declare_from.md +++ b/docs/src/appendix/sncast/declare_from.md @@ -22,7 +22,7 @@ Optional. Use predefined network with public provider -Possible values: `mainnet`, `sepolia`. +Possible values: `mainnet`, `sepolia`, `devnet`. ## `--source-url, -u ` Optional. @@ -34,7 +34,7 @@ Optional. Use predefined network with public provider where the contract is already declared. -Possible values: `mainnet`, `sepolia`. +Possible values: `mainnet`, `sepolia`, `devnet`. ## `--max-fee, -m ` Optional. diff --git a/docs/src/appendix/sncast/deploy.md b/docs/src/appendix/sncast/deploy.md index 8eacc86f0d..63077d3f78 100644 --- a/docs/src/appendix/sncast/deploy.md +++ b/docs/src/appendix/sncast/deploy.md @@ -22,7 +22,7 @@ Optional. Use predefined network with public provider -Possible values: `mainnet`, `sepolia`. +Possible values: `mainnet`, `sepolia`, `devnet`. ## `--constructor-calldata, -c ` Optional. diff --git a/docs/src/appendix/sncast/invoke.md b/docs/src/appendix/sncast/invoke.md index 395862e55b..616f9a1d29 100644 --- a/docs/src/appendix/sncast/invoke.md +++ b/docs/src/appendix/sncast/invoke.md @@ -43,7 +43,7 @@ Optional. Use predefined network with public provider -Possible values: `mainnet`, `sepolia`. +Possible values: `mainnet`, `sepolia`, `devnet`. ## `--max-fee, -m ` Optional. diff --git a/docs/src/appendix/sncast/multicall/run.md b/docs/src/appendix/sncast/multicall/run.md index c81ca9b3e3..102513d1bd 100644 --- a/docs/src/appendix/sncast/multicall/run.md +++ b/docs/src/appendix/sncast/multicall/run.md @@ -23,7 +23,7 @@ Optional. Use predefined network with public provider -Possible values: `mainnet`, `sepolia`. +Possible values: `mainnet`, `sepolia`, `devnet`. ## `--max-fee, -m ` Optional. diff --git a/docs/src/appendix/sncast/script/run.md b/docs/src/appendix/sncast/script/run.md index 3c9aa0ed80..2e2941117a 100644 --- a/docs/src/appendix/sncast/script/run.md +++ b/docs/src/appendix/sncast/script/run.md @@ -22,7 +22,7 @@ Optional. Use predefined network with public provider -Possible values: `mainnet`, `sepolia`. +Possible values: `mainnet`, `sepolia`, `devnet`. ## `--package ` Optional. diff --git a/docs/src/appendix/sncast/show_config.md b/docs/src/appendix/sncast/show_config.md index 5a95a404d5..a29f52f7bb 100644 --- a/docs/src/appendix/sncast/show_config.md +++ b/docs/src/appendix/sncast/show_config.md @@ -13,4 +13,4 @@ Optional. Use predefined network with public provider -Possible values: `mainnet`, `sepolia`. \ No newline at end of file +Possible values: `mainnet`, `sepolia`, `devnet`. \ No newline at end of file diff --git a/docs/src/appendix/sncast/tx-status.md b/docs/src/appendix/sncast/tx-status.md index 8ebf1a3cfe..8692273df5 100644 --- a/docs/src/appendix/sncast/tx-status.md +++ b/docs/src/appendix/sncast/tx-status.md @@ -20,4 +20,4 @@ Optional. Use predefined network with public provider -Possible values: `mainnet`, `sepolia`. +Possible values: `mainnet`, `sepolia`, `devnet`. diff --git a/docs/src/appendix/sncast/utils/serialize.md b/docs/src/appendix/sncast/utils/serialize.md index 3f58c4f5b4..976ea86106 100644 --- a/docs/src/appendix/sncast/utils/serialize.md +++ b/docs/src/appendix/sncast/utils/serialize.md @@ -44,4 +44,4 @@ Optional. Use predefined network with public provider -Possible values: `mainnet`, `sepolia`. +Possible values: `mainnet`, `sepolia`, `devnet`.