Skip to content

Commit df0c7ed

Browse files
authored
Support devnet in --network flag (#3786)
<!-- Reference any GitHub issues resolved by this PR --> Closes #3764 ## Introduced changes <!-- A brief description of the changes --> - Implement `devnet` detection for `--network` flag ## Checklist <!-- Make sure all of these are complete --> - [x] Linked relevant issue - [x] Updated relevant documentation - [x] Added relevant tests - [x] Performed self-review of the code - [x] Added changes to `CHANGELOG.md`
1 parent 1e34c10 commit df0c7ed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+586
-109
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121
- Debug logging for `sncast` commands that can be enabled by setting `CAST_LOG` env variable.
2222
- `sncast declare` command now outputs a ready-to-use deployment command after successful declaration.
2323
- Possibility to use [`starknet-devnet`](https://github.com/0xSpaceShard/starknet-devnet) predeployed accounts directly in `sncast` without needing to import them. They are available under specific names - `devnet-1`, `devnet-2`, ..., `devnet-<N>`. Read more [here](https://foundry-rs.github.io/starknet-foundry/starknet/integration_with_devnet.html#predeployed-accounts)
24+
- Support for `--network devnet` flag that attempts to auto-detect running `starknet-devnet` instance and connect to it.
2425
- Support for automatically declaring the contract when running `sncast deploy`, by providing `--contract-name` flag instead of `--class-hash`.
2526

2627
## [0.50.0] - 2025-09-29

crates/sncast/src/helpers/account.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::{
2-
NestedMap, build_account, check_account_file_exists, helpers::devnet_provider::DevnetProvider,
2+
NestedMap, build_account, check_account_file_exists, helpers::devnet::provider::DevnetProvider,
33
};
44
use anyhow::{Result, ensure};
55
use camino::Utf8PathBuf;

crates/sncast/src/helpers/block_explorer.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ impl Service {
2424
(Service::ViewBlock, Network::Mainnet) => Ok(Box::new(ViewBlock)),
2525
(Service::OkLink, Network::Mainnet) => Ok(Box::new(OkLink)),
2626
(_, Network::Sepolia) => Err(ExplorerError::SepoliaNotSupported),
27+
(_, Network::Devnet) => Err(ExplorerError::DevnetNotSupported),
2728
}
2829
}
2930
}
@@ -36,8 +37,8 @@ pub trait LinkProvider {
3637

3738
const fn network_subdomain(network: Network) -> &'static str {
3839
match network {
39-
Network::Mainnet => "",
4040
Network::Sepolia => "sepolia.",
41+
Network::Mainnet | Network::Devnet => "",
4142
}
4243
}
4344

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
mod direct;
2+
mod docker;
3+
mod flag_parsing;
4+
5+
use std::process::Command;
6+
7+
use crate::helpers::devnet::provider::DevnetProvider;
8+
9+
pub(super) const DEFAULT_DEVNET_HOST: &str = "127.0.0.1";
10+
pub(super) const DEFAULT_DEVNET_PORT: u16 = 5050;
11+
12+
#[derive(Debug, Clone)]
13+
pub(super) struct ProcessInfo {
14+
pub host: String,
15+
pub port: u16,
16+
}
17+
18+
#[derive(Debug, thiserror::Error)]
19+
pub enum DevnetDetectionError {
20+
#[error(
21+
"Could not detect running starknet-devnet instance. Please use `--url <URL>` instead or start devnet."
22+
)]
23+
NoInstance,
24+
#[error(
25+
"Multiple starknet-devnet instances found. Please use `--url <URL>` to specify which one to use."
26+
)]
27+
MultipleInstances,
28+
#[error("Failed to execute process detection command.")]
29+
CommandFailed,
30+
#[error(
31+
"Found starknet-devnet process, but could not reach it. Please use `--url <URL>` to specify the correct URL."
32+
)]
33+
ProcessNotReachable,
34+
}
35+
36+
pub async fn detect_devnet_url() -> Result<String, DevnetDetectionError> {
37+
detect_devnet_from_processes().await
38+
}
39+
40+
#[must_use]
41+
pub async fn is_devnet_running() -> bool {
42+
detect_devnet_from_processes().await.is_ok()
43+
}
44+
45+
async fn detect_devnet_from_processes() -> Result<String, DevnetDetectionError> {
46+
match find_devnet_process_info() {
47+
Ok(info) => {
48+
if is_devnet_url_reachable(&info.host, info.port).await {
49+
Ok(format!("http://{}:{}", info.host, info.port))
50+
} else {
51+
Err(DevnetDetectionError::ProcessNotReachable)
52+
}
53+
}
54+
Err(DevnetDetectionError::NoInstance | DevnetDetectionError::CommandFailed) => {
55+
// Fallback to default starknet-devnet URL if reachable
56+
if is_devnet_url_reachable(DEFAULT_DEVNET_HOST, DEFAULT_DEVNET_PORT).await {
57+
Ok(format!(
58+
"http://{DEFAULT_DEVNET_HOST}:{DEFAULT_DEVNET_PORT}"
59+
))
60+
} else {
61+
Err(DevnetDetectionError::NoInstance)
62+
}
63+
}
64+
Err(e) => Err(e),
65+
}
66+
}
67+
68+
fn find_devnet_process_info() -> Result<ProcessInfo, DevnetDetectionError> {
69+
let output = Command::new("sh")
70+
.args(["-c", "ps aux | grep starknet-devnet | grep -v grep"])
71+
.output()
72+
.map_err(|_| DevnetDetectionError::CommandFailed)?;
73+
let ps_output = String::from_utf8_lossy(&output.stdout);
74+
75+
let devnet_processes: Result<Vec<ProcessInfo>, DevnetDetectionError> = ps_output
76+
.lines()
77+
.map(|line| {
78+
if line.contains("docker") || line.contains("podman") {
79+
docker::extract_devnet_info_from_docker_run(line)
80+
} else {
81+
direct::extract_devnet_info_from_direct_run(line)
82+
}
83+
})
84+
.collect();
85+
86+
let devnet_processes = devnet_processes?;
87+
88+
match devnet_processes.as_slice() {
89+
[single] => Ok(single.clone()),
90+
[] => Err(DevnetDetectionError::NoInstance),
91+
_ => Err(DevnetDetectionError::MultipleInstances),
92+
}
93+
}
94+
95+
async fn is_devnet_url_reachable(host: &str, port: u16) -> bool {
96+
let url = format!("http://{host}:{port}");
97+
98+
let provider = DevnetProvider::new(&url);
99+
provider.ensure_alive().await.is_ok()
100+
}
101+
102+
#[cfg(test)]
103+
mod tests {
104+
use super::*;
105+
106+
#[tokio::test]
107+
async fn test_detect_devnet_url() {
108+
let result = detect_devnet_url().await;
109+
assert!(result.is_err());
110+
assert!(matches!(
111+
result.unwrap_err(),
112+
DevnetDetectionError::NoInstance
113+
));
114+
}
115+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
use crate::helpers::devnet::detection::flag_parsing::{
2+
extract_port_from_flag, extract_string_from_flag,
3+
};
4+
use crate::helpers::devnet::detection::{
5+
DEFAULT_DEVNET_HOST, DEFAULT_DEVNET_PORT, DevnetDetectionError, ProcessInfo,
6+
};
7+
8+
pub fn extract_devnet_info_from_direct_run(
9+
cmdline: &str,
10+
) -> Result<ProcessInfo, DevnetDetectionError> {
11+
let mut port = extract_port_from_flag(cmdline, "--port");
12+
let mut host = extract_string_from_flag(cmdline, "--host");
13+
14+
if port.is_none()
15+
&& let Ok(port_env) = std::env::var("PORT")
16+
{
17+
port = Some(
18+
port_env
19+
.parse()
20+
.map_err(|_| DevnetDetectionError::ProcessNotReachable)?,
21+
);
22+
}
23+
24+
if host.is_none()
25+
&& let Ok(host_env) = std::env::var("HOST")
26+
&& !host_env.is_empty()
27+
{
28+
host = Some(host_env);
29+
}
30+
31+
let final_port = port.unwrap_or(DEFAULT_DEVNET_PORT);
32+
let final_host = host.unwrap_or_else(|| DEFAULT_DEVNET_HOST.to_string());
33+
34+
Ok(ProcessInfo {
35+
host: final_host,
36+
port: final_port,
37+
})
38+
}
39+
40+
#[cfg(test)]
41+
mod tests {
42+
use super::*;
43+
44+
// These tests are marked to run serially to avoid interference from environment variables
45+
#[test]
46+
fn test_direct_devnet_parsing() {
47+
test_extract_devnet_info_from_cmdline();
48+
test_extract_devnet_info_with_both_envs();
49+
test_invalid_env();
50+
test_cmdline_args_override_env();
51+
test_wrong_env_var();
52+
}
53+
54+
fn test_extract_devnet_info_from_cmdline() {
55+
let cmdline1 = "starknet-devnet --port 6000 --host 127.0.0.1";
56+
let info1 = extract_devnet_info_from_direct_run(cmdline1).unwrap();
57+
assert_eq!(info1.port, 6000);
58+
assert_eq!(info1.host, "127.0.0.1");
59+
60+
let cmdline2 = "/usr/bin/starknet-devnet --port=5000";
61+
let info2 = extract_devnet_info_from_direct_run(cmdline2).unwrap();
62+
assert_eq!(info2.port, 5000);
63+
assert_eq!(info2.host, "127.0.0.1");
64+
65+
let cmdline3 = "starknet-devnet --host 127.0.0.1";
66+
let info3 = extract_devnet_info_from_direct_run(cmdline3).unwrap();
67+
assert_eq!(info3.port, 5050);
68+
assert_eq!(info3.host, "127.0.0.1");
69+
}
70+
71+
fn test_extract_devnet_info_with_both_envs() {
72+
// SAFETY: Variables are only modified within this test and cleaned up afterwards
73+
unsafe {
74+
std::env::set_var("PORT", "9999");
75+
std::env::set_var("HOST", "9.9.9.9");
76+
}
77+
78+
let cmdline = "starknet-devnet";
79+
let info = extract_devnet_info_from_direct_run(cmdline).unwrap();
80+
assert_eq!(info.port, 9999);
81+
assert_eq!(info.host, "9.9.9.9");
82+
83+
// SAFETY: Clean up environment variables to prevent interference
84+
unsafe {
85+
std::env::remove_var("PORT");
86+
std::env::remove_var("HOST");
87+
}
88+
}
89+
90+
fn test_invalid_env() {
91+
// SAFETY: Variables are only modified within this test and cleaned up afterwards
92+
unsafe {
93+
std::env::set_var("PORT", "asdf");
94+
std::env::set_var("HOST", "9.9.9.9");
95+
}
96+
let cmdline = "starknet-devnet";
97+
let result = extract_devnet_info_from_direct_run(cmdline);
98+
assert!(result.is_err());
99+
assert!(matches!(
100+
result.unwrap_err(),
101+
DevnetDetectionError::ProcessNotReachable
102+
));
103+
104+
// SAFETY: Clean up environment variables to prevent interference
105+
unsafe {
106+
std::env::remove_var("PORT");
107+
std::env::remove_var("HOST");
108+
}
109+
}
110+
111+
fn test_cmdline_args_override_env() {
112+
// SAFETY: Variables are only modified within this test and cleaned up afterwards
113+
unsafe {
114+
std::env::set_var("PORT", "3000");
115+
std::env::set_var("HOST", "7.7.7.7");
116+
}
117+
118+
let cmdline = "starknet-devnet --port 9999 --host 192.168.1.1";
119+
let info = extract_devnet_info_from_direct_run(cmdline).unwrap();
120+
assert_eq!(info.port, 9999);
121+
assert_eq!(info.host, "192.168.1.1");
122+
123+
// SAFETY: Clean up environment variables to prevent interference
124+
unsafe {
125+
std::env::remove_var("PORT");
126+
std::env::remove_var("HOST");
127+
}
128+
}
129+
130+
fn test_wrong_env_var() {
131+
// SAFETY: Variables are only modified within this test and cleaned up afterwards
132+
unsafe {
133+
std::env::set_var("PORT", "asdf");
134+
}
135+
136+
// Empty HOST env var should be ignored and defaults should be used
137+
let cmdline = "starknet-devnet";
138+
let result = extract_devnet_info_from_direct_run(cmdline);
139+
assert!(result.is_err());
140+
assert!(matches!(
141+
result.unwrap_err(),
142+
DevnetDetectionError::ProcessNotReachable
143+
));
144+
145+
// SAFETY: Clean up environment variables to prevent interference
146+
unsafe {
147+
std::env::remove_var("PORT");
148+
}
149+
}
150+
}

0 commit comments

Comments
 (0)