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;