Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7267154
Add `DevetClient`
franciszekjob Sep 26, 2025
0e933b4
Rename `DevnetClient` -> `DevnetProvider`
franciszekjob Sep 26, 2025
5db4ffe
Remove `account_exists_in_accounts_file`
franciszekjob Sep 26, 2025
c2baa3e
Refactor test
franciszekjob Sep 26, 2025
2213285
Remove functions
franciszekjob Sep 26, 2025
64c8c5b
Fix typo
franciszekjob Sep 26, 2025
fa4af76
Little refactor
franciszekjob Sep 29, 2025
53de064
Use fixed version of cairo deps
cptartur Sep 29, 2025
b32ed80
Release 0.50.0
cptartur Sep 29, 2025
3227541
Close #3723
cptartur Sep 29, 2025
11215b7
Update tests
cptartur Sep 29, 2025
1d8443a
Lint
cptartur Sep 29, 2025
9d52309
Merge branch 'master' into 3603-devnet-client
franciszekjob Sep 30, 2025
bb2f942
Merge branch 'release-0.50.0' into 3603-devnet-client
franciszekjob Sep 30, 2025
42e7847
Update CHANGELOG.md
cptartur Sep 30, 2025
66fa96c
Release 0.50.0 lint (#3769)
cptartur Sep 30, 2025
f71fce4
Merge branch 'release-0.50.0' into 3603-devnet-client
franciszekjob Sep 30, 2025
efc0be7
Merge branch 'master' of https://github.com/foundry-rs/starknet-found…
franciszekjob Oct 1, 2025
29d976d
Merge branch '3603-devnet-client' of https://github.com/foundry-rs/st…
franciszekjob Oct 1, 2025
dc03f27
Apply code review suggestion
franciszekjob Oct 1, 2025
3a1f463
Fix `Cargo.lock`
franciszekjob Oct 1, 2025
a6c8252
Fix changelog
franciszekjob Oct 1, 2025
de848e8
Remove redundant comments
franciszekjob Oct 1, 2025
2718462
Apply code review suggestion
franciszekjob Oct 1, 2025
24802a6
Apply code review suggestions
franciszekjob Oct 2, 2025
fd67052
Add `DevnetProvider.ensure_alive()`
franciszekjob Oct 2, 2025
fe1214d
Apply code review suggestion
franciszekjob Oct 2, 2025
fc0bb1d
Remove assertions
franciszekjob Oct 3, 2025
fdb41d4
Fix linting
franciszekjob Oct 3, 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
130 changes: 130 additions & 0 deletions crates/sncast/src/helpers/devnet_provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use crate::AccountData;
use ::serde::{Deserialize, Serialize, de::DeserializeOwned};
use anyhow::{Context, Error, ensure};
use reqwest::Client;
use serde_json::json;
use starknet_types_core::felt::Felt;
use url::Url;

/// A Devnet-RPC client.
#[derive(Debug, Clone)]
pub struct DevnetProvider {
client: Client,
url: Url,
}

/// All Devnet-RPC methods as listed in the official docs.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum DevnetProviderMethod {
#[serde(rename = "devnet_getConfig")]
GetConfig,

#[serde(rename = "devnet_getPredeployedAccounts")]
GetPredeployedAccounts,
}

impl DevnetProvider {
#[must_use]
pub fn new(url: &str) -> Self {
let url = Url::parse(url).expect("Invalid URL");
Self {
client: Client::new(),
url,
}
}
}

impl DevnetProvider {
async fn send_request<P, R>(&self, method: DevnetProviderMethod, params: P) -> anyhow::Result<R>
where
P: Serialize + Send + Sync,
R: DeserializeOwned,
{
let res = self
.client
.post(self.url.clone())
.header("Content-Type", "application/json")
.json(&json!({
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": 1,
}))
.send()
.await
.context("Failed to send request")?
.json::<serde_json::Value>()
.await
.context("Failed to parse response")?;

if let Some(error) = res.get("error") {
Err(anyhow::anyhow!(error.to_string()))
} else if let Some(result) = res.get("result") {
serde_json::from_value(result.clone()).map_err(anyhow::Error::from)
} else {
panic!("Malformed RPC response: {res}")
}
}

/// Fetches the current Devnet configuration.
pub async fn get_config(&self) -> Result<Config, Error> {
self.send_request(DevnetProviderMethod::GetConfig, json!({}))
.await
}

/// Fetches the list of predeployed accounts in Devnet.
pub async fn get_predeployed_accounts(&self) -> Result<Vec<PredeployedAccount>, Error> {
self.send_request(DevnetProviderMethod::GetPredeployedAccounts, json!({}))
.await
}

/// Ensures the Devnet instance is alive.
pub async fn ensure_alive(&self) -> Result<(), Error> {
let is_alive = self
.client
.get(format!(
"{}/is_alive",
self.url.to_string().replace("/rpc", "")
))
.send()
.await
.map(|res| res.status().is_success())
.unwrap_or(false);

ensure!(
is_alive,
"Node at {} is not responding to the Devnet health check (GET `/is_alive`). It may not be a Devnet instance or it may be down.",
self.url
);
Ok(())
}
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
pub seed: u32,
pub account_contract_class_hash: Felt,
pub total_accounts: u8,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct PredeployedAccount {
pub address: Felt,
pub private_key: Felt,
pub public_key: Felt,
}

impl From<&PredeployedAccount> for AccountData {
fn from(predeployed_account: &PredeployedAccount) -> Self {
Self {
address: Some(predeployed_account.address),
private_key: predeployed_account.private_key,
public_key: predeployed_account.public_key,
class_hash: None,
salt: None,
deployed: None,
legacy: None,
account_type: None,
}
}
}
1 change: 1 addition & 0 deletions crates/sncast/src/helpers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod braavos;
pub mod config;
pub mod configuration;
pub mod constants;
pub mod devnet_provider;
pub mod fee;
pub mod interactive;
pub mod output_format;
Expand Down
5 changes: 3 additions & 2 deletions crates/sncast/tests/helpers/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ pub const SEPOLIA_RPC_URL: &str = "http://188.34.188.184:7070/rpc/v0_9";

pub const URL: &str = "http://127.0.0.1:5055/rpc";
pub const NETWORK: &str = "testnet";
pub const SEED: u32 = 1_053_545_548;
pub const DEVNET_SEED: u32 = 1_053_545_548;
pub const DEVNET_ACCOUNTS_NUMBER: u8 = 20;

// Block number used by devnet to fork the Sepolia testnet network in the tests
pub const FORK_BLOCK_NUMBER: u32 = 721_720;
pub const DEVNET_FORK_BLOCK_NUMBER: u32 = 721_720;

pub const CONTRACTS_DIR: &str = "tests/data/contracts";
pub const SCRIPTS_DIR: &str = "tests/data/scripts";
Expand Down
12 changes: 7 additions & 5 deletions crates/sncast/tests/helpers/devnet.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::helpers::constants::{FORK_BLOCK_NUMBER, SEED, SEPOLIA_RPC_URL, URL};
use crate::helpers::constants::{
DEVNET_ACCOUNTS_NUMBER, DEVNET_FORK_BLOCK_NUMBER, DEVNET_SEED, SEPOLIA_RPC_URL, URL,
};
use crate::helpers::fixtures::{
deploy_braavos_account, deploy_cairo_0_account, deploy_keystore_account,
deploy_latest_oz_account, deploy_ready_account,
Expand Down Expand Up @@ -39,17 +41,17 @@ fn start_devnet() {
"--port",
&port,
"--seed",
&SEED.to_string(),
&DEVNET_SEED.to_string(),
"--state-archive-capacity",
"full",
"--fork-network",
SEPOLIA_RPC_URL,
"--fork-block",
&FORK_BLOCK_NUMBER.to_string(),
&DEVNET_FORK_BLOCK_NUMBER.to_string(),
"--initial-balance",
"9999999999999999999999999999999",
"--accounts",
"20",
&DEVNET_ACCOUNTS_NUMBER.to_string(),
])
.stdout(Stdio::null())
.spawn()
Expand Down Expand Up @@ -80,7 +82,7 @@ fn start_devnet() {
#[dtor]
fn stop_devnet() {
let port = Url::parse(URL).unwrap().port().unwrap_or(80).to_string();
let pattern = format!("starknet-devnet.*{port}.*{SEED}");
let pattern = format!("starknet-devnet.*{port}.*{DEVNET_SEED}");

Command::new("pkill")
.args(["-f", &pattern])
Expand Down
51 changes: 51 additions & 0 deletions crates/sncast/tests/helpers/devnet_provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use crate::helpers::constants::{DEVNET_ACCOUNTS_NUMBER, DEVNET_SEED, SEPOLIA_RPC_URL, URL};
use num_traits::ToPrimitive;
use sncast::helpers::{constants::OZ_CLASS_HASH, devnet_provider::DevnetProvider};

#[tokio::test]
async fn test_get_config() {
let devnet_provider = DevnetProvider::new(URL);
let config = devnet_provider
.get_config()
.await
.expect("Failed to get config");

assert!(config.account_contract_class_hash == OZ_CLASS_HASH);
assert!(config.seed == DEVNET_SEED);
assert!(config.total_accounts == DEVNET_ACCOUNTS_NUMBER);
}

#[tokio::test]
async fn test_get_predeployed_accounts() {
let devnet_provider = DevnetProvider::new(URL);
let predeployed_accounts = devnet_provider
.get_predeployed_accounts()
.await
.expect("Failed to get predeployed accounts");

assert!(predeployed_accounts.len().to_u8().unwrap() == DEVNET_ACCOUNTS_NUMBER);
}

#[tokio::test]
async fn test_is_alive_happy_case() {
let devnet_provider = DevnetProvider::new(URL);
devnet_provider
.ensure_alive()
.await
.expect("Failed to ensure the devnet is alive");
}

#[tokio::test]
async fn test_is_alive_fails_on_sepolia_node() {
let devnet_provider = DevnetProvider::new(SEPOLIA_RPC_URL);
let res = devnet_provider.ensure_alive().await;
assert!(res.is_err(), "Expected an error");

let err = res.unwrap_err().to_string();
assert!(
err == format!(
"Node at {SEPOLIA_RPC_URL} is not responding to the Devnet health check (GET `/is_alive`). It may not be a Devnet instance or it may be down."
),
"Unexpected error message: {err}"
);
}
1 change: 1 addition & 0 deletions crates/sncast/tests/helpers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod constants;
pub mod devnet;
pub mod devnet_provider;
pub mod env;
pub mod fee;
pub mod fixtures;
Expand Down
Loading