Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
120b679
Introduce `devnet` option for `--network`
MKowalski8 Oct 2, 2025
61b534d
Implement heuristic detection
MKowalski8 Oct 2, 2025
807dcba
Update docs
MKowalski8 Oct 2, 2025
9a1ea80
Changelog info
MKowalski8 Oct 2, 2025
dcbb672
Change displaying blockexplorer to heuristic detection
MKowalski8 Oct 2, 2025
7d95b14
Fix clippy
MKowalski8 Oct 2, 2025
0e04f81
Rewrite url detection
MKowalski8 Oct 3, 2025
a5510dd
Better error message
MKowalski8 Oct 3, 2025
a18a02d
Fix tests
MKowalski8 Oct 3, 2025
7299ae0
Rename
MKowalski8 Oct 3, 2025
f4b31ea
Fix new clippy
MKowalski8 Oct 3, 2025
2d0aa6c
Change docs
MKowalski8 Oct 6, 2025
eafe1ca
Return error when two instances found
MKowalski8 Oct 6, 2025
88071ee
Merge branch 'master' into 3764-support-devnet-via-network-flag
MKowalski8 Oct 6, 2025
a9b1b51
Merge fixes
MKowalski8 Oct 6, 2025
016fc2e
Fix comments
MKowalski8 Oct 6, 2025
7793096
Prepare to review
MKowalski8 Oct 7, 2025
4613458
Merge branch 'master' into 3764-support-devnet-via-network-flag
MKowalski8 Oct 7, 2025
7aaf4d4
Rewrite docker data extracting
MKowalski8 Oct 7, 2025
f73163e
Update docs
MKowalski8 Oct 7, 2025
f8c1ac6
Nits from review
MKowalski8 Oct 7, 2025
71357ba
Change verification to return error
MKowalski8 Oct 7, 2025
ed83201
Change filtering to `grep`
MKowalski8 Oct 7, 2025
079e8db
Merge branch 'master' into 3764-support-devnet-via-network-flag
MKowalski8 Oct 7, 2025
3513d28
Fix fmt
MKowalski8 Oct 7, 2025
629bb90
Review chnages
MKowalski8 Oct 8, 2025
d31aa8a
Rewrite to regex
MKowalski8 Oct 8, 2025
d097ad4
Update `is_port_reachable` to use `DevnetProvider`
MKowalski8 Oct 8, 2025
59134b9
Refactor tests
MKowalski8 Oct 8, 2025
ad680dd
Docs update
MKowalski8 Oct 8, 2025
50e9025
Add different port handling
MKowalski8 Oct 10, 2025
ccb3107
Rewrite docker extraction
MKowalski8 Oct 10, 2025
36e8ef9
Review nits
MKowalski8 Oct 13, 2025
0e5361b
Merge branch 'master' into 3764-support-devnet-via-network-flag
MKowalski8 Oct 13, 2025
ff75ecf
Error test cases
MKowalski8 Oct 13, 2025
625a05a
Fix lint
MKowalski8 Oct 13, 2025
184ec6e
Funcs relocation
MKowalski8 Oct 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Debug logging for `sncast` commands that can be enabled by setting `CAST_LOG` env variable.
- `sncast declare` command now outputs a ready-to-use deployment command after successful declaration.
- 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)
- Support for `--network devnet` flag that attempts to auto-detect running `starknet-devnet` instance and connect to it.
- Support for automatically declaring the contract when running `sncast deploy`, by providing `--contract-name` flag instead of `--class-hash`.

## [0.50.0] - 2025-09-29
Expand Down
2 changes: 1 addition & 1 deletion crates/sncast/src/helpers/account.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
NestedMap, build_account, check_account_file_exists, helpers::devnet_provider::DevnetProvider,
NestedMap, build_account, check_account_file_exists, helpers::devnet::provider::DevnetProvider,
};
use anyhow::{Result, ensure};
use camino::Utf8PathBuf;
Expand Down
3 changes: 2 additions & 1 deletion crates/sncast/src/helpers/block_explorer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}
}
Expand All @@ -36,8 +37,8 @@ pub trait LinkProvider {

const fn network_subdomain(network: Network) -> &'static str {
match network {
Network::Mainnet => "",
Network::Sepolia => "sepolia.",
Network::Mainnet | Network::Devnet => "",
}
}

Expand Down
115 changes: 115 additions & 0 deletions crates/sncast/src/helpers/devnet/detection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
mod direct;
mod docker;
mod flag_parsing;

use std::process::Command;

use crate::helpers::devnet::provider::DevnetProvider;

pub(super) const DEFAULT_DEVNET_HOST: &str = "127.0.0.1";
pub(super) const DEFAULT_DEVNET_PORT: u16 = 5050;

#[derive(Debug, Clone)]
pub(super) struct ProcessInfo {
pub host: String,
pub port: u16,
}

#[derive(Debug, thiserror::Error)]
pub enum DevnetDetectionError {
#[error(
"Could not detect running starknet-devnet instance. Please use `--url <URL>` instead or start devnet."
)]
NoInstance,
#[error(
"Multiple starknet-devnet instances found. Please use `--url <URL>` to specify which one to use."
)]
MultipleInstances,
#[error("Failed to execute process detection command.")]
CommandFailed,
#[error(
"Found starknet-devnet process, but could not reach it. Please use `--url <URL>` to specify the correct URL."
)]
ProcessNotReachable,
}

pub async fn detect_devnet_url() -> Result<String, DevnetDetectionError> {
detect_devnet_from_processes().await
}

#[must_use]
pub async fn is_devnet_running() -> bool {
detect_devnet_from_processes().await.is_ok()
}

async fn detect_devnet_from_processes() -> Result<String, DevnetDetectionError> {
match find_devnet_process_info() {
Ok(info) => {
if is_devnet_url_reachable(&info.host, info.port).await {
Ok(format!("http://{}:{}", info.host, info.port))
} else {
Err(DevnetDetectionError::ProcessNotReachable)
}
}
Err(DevnetDetectionError::NoInstance | DevnetDetectionError::CommandFailed) => {
// Fallback to default starknet-devnet URL if reachable
if is_devnet_url_reachable(DEFAULT_DEVNET_HOST, DEFAULT_DEVNET_PORT).await {
Ok(format!(
"http://{DEFAULT_DEVNET_HOST}:{DEFAULT_DEVNET_PORT}"
))
} else {
Err(DevnetDetectionError::NoInstance)
}
}
Err(e) => Err(e),
}
}

fn find_devnet_process_info() -> Result<ProcessInfo, DevnetDetectionError> {
let output = Command::new("sh")
.args(["-c", "ps aux | grep starknet-devnet | grep -v grep"])
.output()
.map_err(|_| DevnetDetectionError::CommandFailed)?;
let ps_output = String::from_utf8_lossy(&output.stdout);

let devnet_processes: Result<Vec<ProcessInfo>, DevnetDetectionError> = ps_output
.lines()
.map(|line| {
if line.contains("docker") || line.contains("podman") {
docker::extract_devnet_info_from_docker_run(line)
} else {
direct::extract_devnet_info_from_direct_run(line)
}
})
.collect();

let devnet_processes = devnet_processes?;

match devnet_processes.as_slice() {
[single] => Ok(single.clone()),
[] => Err(DevnetDetectionError::NoInstance),
_ => Err(DevnetDetectionError::MultipleInstances),
}
}

async fn is_devnet_url_reachable(host: &str, port: u16) -> bool {
let url = format!("http://{host}:{port}");

let provider = DevnetProvider::new(&url);
provider.ensure_alive().await.is_ok()
}

#[cfg(test)]
mod tests {
use super::*;

#[tokio::test]
async fn test_detect_devnet_url() {
let result = detect_devnet_url().await;
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
DevnetDetectionError::NoInstance
));
}
}
150 changes: 150 additions & 0 deletions crates/sncast/src/helpers/devnet/detection/direct.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
use crate::helpers::devnet::detection::flag_parsing::{
extract_port_from_flag, extract_string_from_flag,
};
use crate::helpers::devnet::detection::{
DEFAULT_DEVNET_HOST, DEFAULT_DEVNET_PORT, DevnetDetectionError, ProcessInfo,
};

pub fn extract_devnet_info_from_direct_run(
cmdline: &str,
) -> Result<ProcessInfo, DevnetDetectionError> {
let mut port = extract_port_from_flag(cmdline, "--port");
let mut host = extract_string_from_flag(cmdline, "--host");

if port.is_none()
&& let Ok(port_env) = std::env::var("PORT")
{
port = Some(
port_env
.parse()
.map_err(|_| DevnetDetectionError::ProcessNotReachable)?,
);
}

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(DEFAULT_DEVNET_PORT);
let final_host = host.unwrap_or_else(|| DEFAULT_DEVNET_HOST.to_string());

Ok(ProcessInfo {
host: final_host,
port: final_port,
})
}

#[cfg(test)]
mod tests {
use super::*;

// These tests are marked to run serially to avoid interference from environment variables
#[test]
fn test_direct_devnet_parsing() {
test_extract_devnet_info_from_cmdline();
test_extract_devnet_info_with_both_envs();
test_invalid_env();
test_cmdline_args_override_env();
test_wrong_env_var();
}

fn test_extract_devnet_info_from_cmdline() {
let cmdline1 = "starknet-devnet --port 6000 --host 127.0.0.1";
let info1 = extract_devnet_info_from_direct_run(cmdline1).unwrap();
assert_eq!(info1.port, 6000);
assert_eq!(info1.host, "127.0.0.1");

let cmdline2 = "/usr/bin/starknet-devnet --port=5000";
let info2 = extract_devnet_info_from_direct_run(cmdline2).unwrap();
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_direct_run(cmdline3).unwrap();
assert_eq!(info3.port, 5050);
assert_eq!(info3.host, "127.0.0.1");
}

fn test_extract_devnet_info_with_both_envs() {
// SAFETY: Variables are only modified within this test and cleaned up afterwards
unsafe {
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_direct_run(cmdline).unwrap();
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");
}
}

fn test_invalid_env() {
// SAFETY: Variables are only modified within this test and cleaned up afterwards
unsafe {
std::env::set_var("PORT", "asdf");
std::env::set_var("HOST", "9.9.9.9");
}
let cmdline = "starknet-devnet";
let result = extract_devnet_info_from_direct_run(cmdline);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
DevnetDetectionError::ProcessNotReachable
));

// SAFETY: Clean up environment variables to prevent interference
unsafe {
std::env::remove_var("PORT");
std::env::remove_var("HOST");
}
}

fn test_cmdline_args_override_env() {
// SAFETY: Variables are only modified within this test and cleaned up afterwards
unsafe {
std::env::set_var("PORT", "3000");
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_direct_run(cmdline).unwrap();
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");
}
}

fn test_wrong_env_var() {
// SAFETY: Variables are only modified within this test and cleaned up afterwards
unsafe {
std::env::set_var("PORT", "asdf");
}

// Empty HOST env var should be ignored and defaults should be used
let cmdline = "starknet-devnet";
let result = extract_devnet_info_from_direct_run(cmdline);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
DevnetDetectionError::ProcessNotReachable
));

// SAFETY: Clean up environment variables to prevent interference
unsafe {
std::env::remove_var("PORT");
}
}
}
Loading
Loading