diff --git a/crates/sncast/src/helpers/devnet_provider.rs b/crates/sncast/src/helpers/devnet_provider.rs
new file mode 100644
index 0000000000..88cdcc028e
--- /dev/null
+++ b/crates/sncast/src/helpers/devnet_provider.rs
@@ -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
(&self, method: DevnetProviderMethod, params: P) -> anyhow::Result
+ 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::()
+ .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 {
+ self.send_request(DevnetProviderMethod::GetConfig, json!({}))
+ .await
+ }
+
+ /// Fetches the list of predeployed accounts in Devnet.
+ pub async fn get_predeployed_accounts(&self) -> Result, 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,
+ }
+ }
+}
diff --git a/crates/sncast/src/helpers/mod.rs b/crates/sncast/src/helpers/mod.rs
index 66f7c099c3..fb7ba5df70 100644
--- a/crates/sncast/src/helpers/mod.rs
+++ b/crates/sncast/src/helpers/mod.rs
@@ -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;
diff --git a/crates/sncast/tests/helpers/constants.rs b/crates/sncast/tests/helpers/constants.rs
index 320b4ad038..b41d0a5437 100644
--- a/crates/sncast/tests/helpers/constants.rs
+++ b/crates/sncast/tests/helpers/constants.rs
@@ -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";
diff --git a/crates/sncast/tests/helpers/devnet.rs b/crates/sncast/tests/helpers/devnet.rs
index ae74d36f70..9b2c1539eb 100644
--- a/crates/sncast/tests/helpers/devnet.rs
+++ b/crates/sncast/tests/helpers/devnet.rs
@@ -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,
@@ -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()
@@ -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])
diff --git a/crates/sncast/tests/helpers/devnet_provider.rs b/crates/sncast/tests/helpers/devnet_provider.rs
new file mode 100644
index 0000000000..7ca47d2c49
--- /dev/null
+++ b/crates/sncast/tests/helpers/devnet_provider.rs
@@ -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}"
+ );
+}
diff --git a/crates/sncast/tests/helpers/mod.rs b/crates/sncast/tests/helpers/mod.rs
index 0a90c4ae2b..6530eebdc3 100644
--- a/crates/sncast/tests/helpers/mod.rs
+++ b/crates/sncast/tests/helpers/mod.rs
@@ -1,5 +1,6 @@
pub mod constants;
pub mod devnet;
+pub mod devnet_provider;
pub mod env;
pub mod fee;
pub mod fixtures;