diff --git a/crates/sncast/src/helpers/account.rs b/crates/sncast/src/helpers/account.rs index c01447bd5b..5d44484df7 100644 --- a/crates/sncast/src/helpers/account.rs +++ b/crates/sncast/src/helpers/account.rs @@ -1,7 +1,14 @@ -use crate::NestedMap; -use anyhow::Result; +use crate::{ + NestedMap, build_account, check_account_file_exists, helpers::devnet_provider::DevnetProvider, +}; +use anyhow::{Result, ensure}; use camino::Utf8PathBuf; -use std::collections::HashSet; +use starknet::{ + accounts::SingleOwnerAccount, + providers::{JsonRpcClient, Provider, jsonrpc::HttpTransport}, + signers::LocalWallet, +}; +use std::collections::{HashMap, HashSet}; use std::fs; use crate::{AccountData, read_and_parse_json_file}; @@ -48,3 +55,64 @@ pub fn load_accounts(accounts_file: &Utf8PathBuf) -> Result { Ok(accounts) } + +pub fn check_account_exists( + account_name: &str, + network_name: &str, + accounts_file: &Utf8PathBuf, +) -> Result { + check_account_file_exists(accounts_file)?; + + let accounts: HashMap> = + read_and_parse_json_file(accounts_file)?; + + accounts + .get(network_name) + .map(|network_accounts| network_accounts.contains_key(account_name)) + .ok_or_else(|| { + anyhow::anyhow!("Network with name {network_name} does not exist in accounts file") + }) +} + +#[must_use] +pub fn is_devnet_account(account: &str) -> bool { + account.starts_with("devnet-") +} + +pub async fn get_account_from_devnet<'a>( + account: &str, + provider: &'a JsonRpcClient, + url: &str, +) -> Result, LocalWallet>> { + let account_number: u8 = account + .strip_prefix("devnet-") + .map(|s| s.parse::().expect("Invalid devnet account number")) + .context("Failed to parse devnet account number")?; + + let devnet_provider = DevnetProvider::new(url); + devnet_provider.ensure_alive().await?; + + let devnet_config = devnet_provider.get_config().await; + let devnet_config = match devnet_config { + Ok(config) => config, + Err(err) => { + return Err(err); + } + }; + + ensure!( + account_number <= devnet_config.total_accounts && account_number != 0, + "Devnet account number must be between 1 and {}", + devnet_config.total_accounts + ); + + let devnet_accounts = devnet_provider.get_predeployed_accounts().await?; + let predeployed_account = devnet_accounts + .get((account_number - 1) as usize) + .expect("Failed to get devnet account") + .to_owned(); + + let account_data = AccountData::from(predeployed_account); + let chain_id = provider.chain_id().await?; + build_account(account_data, chain_id, provider).await +} 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/src/helpers/rpc.rs b/crates/sncast/src/helpers/rpc.rs index 96ce2dcf59..780d5609fd 100644 --- a/crates/sncast/src/helpers/rpc.rs +++ b/crates/sncast/src/helpers/rpc.rs @@ -45,7 +45,7 @@ impl RpcArgs { } #[must_use] - fn get_url(&self, config_url: &String) -> Option { + pub fn get_url(&self, config_url: &String) -> Option { if let Some(network) = self.network { let free_provider = FreeProvider::semi_random(); Some(network.url(&free_provider)) diff --git a/crates/sncast/src/lib.rs b/crates/sncast/src/lib.rs index 46c3c15e49..606624bbeb 100644 --- a/crates/sncast/src/lib.rs +++ b/crates/sncast/src/lib.rs @@ -1,10 +1,14 @@ +use crate::helpers::account::{check_account_exists, get_account_from_devnet, is_devnet_account}; +use crate::helpers::configuration::CastConfig; use crate::helpers::constants::{DEFAULT_STATE_FILE_SUFFIX, WAIT_RETRY_INTERVAL, WAIT_TIMEOUT}; +use crate::helpers::rpc::RpcArgs; use crate::response::errors::SNCastProviderError; use anyhow::{Context, Error, Result, anyhow, bail}; use camino::Utf8PathBuf; use clap::ValueEnum; use conversions::serde::serialize::CairoSerialize; use foundry_ui::UI; +use foundry_ui::components::warning::WarningMessage; use helpers::constants::{KEYSTORE_PASSWORD_ENV_VAR, UDC_ADDRESS}; use rand::RngCore; use rand::rngs::OsRng; @@ -85,7 +89,7 @@ pub const MAINNET: Felt = pub const SEPOLIA: Felt = Felt::from_hex_unchecked(const_hex::const_encode::<10, true>(b"SN_SEPOLIA").as_str()); -#[derive(ValueEnum, Clone, Copy, Debug)] +#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)] pub enum Network { Mainnet, Sepolia, @@ -245,6 +249,51 @@ pub async fn get_nonce( } pub async fn get_account<'a>( + config: &CastConfig, + provider: &'a JsonRpcClient, + rpc_args: &RpcArgs, + keystore: Option<&Utf8PathBuf>, + ui: &UI, +) -> Result, LocalWallet>> { + let chain_id = get_chain_id(provider).await?; + let network_name = chain_id_to_network_name(chain_id); + let account = &config.account; + let is_devnet_account = is_devnet_account(account); + + if is_devnet_account + && let Some(network) = rpc_args.network + && (network == Network::Mainnet || network == Network::Sepolia) + { + bail!(format!( + "Devnet accounts cannot be used with `--network {network}`" + )); + } + + let accounts_file = &config.accounts_file; + let exists_in_accounts_file = check_account_exists(account, &network_name, accounts_file)?; + + match (is_devnet_account, exists_in_accounts_file) { + (true, true) => { + ui.println(&WarningMessage::new(format!( + "Using account {account} from accounts file {accounts_file}. \ + To use an inbuilt devnet account, please rename your existing account or use an account with a different number." + ))); + ui.print_blank_line(); + return get_account_from_accounts_file(account, accounts_file, provider, keystore) + .await; + } + (true, false) => { + let url = rpc_args.get_url(&config.url).context("Failed to get url")?; + return get_account_from_devnet(account, provider, &url).await; + } + _ => { + return get_account_from_accounts_file(account, accounts_file, provider, keystore) + .await; + } + } +} + +pub async fn get_account_from_accounts_file<'a>( account: &str, accounts_file: &Utf8PathBuf, provider: &'a JsonRpcClient, diff --git a/crates/sncast/src/main.rs b/crates/sncast/src/main.rs index 7daa0a202d..01519d9a30 100644 --- a/crates/sncast/src/main.rs +++ b/crates/sncast/src/main.rs @@ -270,10 +270,11 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()> let rpc = declare.rpc.clone(); let account = get_account( - &config.account, - &config.accounts_file, + &config, &provider, + &declare.rpc, config.keystore.as_ref(), + ui, ) .await?; let manifest_path = assert_manifest_path_exists()?; @@ -320,11 +321,13 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()> let provider = declare_from.rpc.get_provider(&config, ui).await?; let rpc_args = declare_from.rpc.clone(); let source_provider = declare_from.source_rpc.get_provider(ui).await?; + let account = get_account( - &config.account, - &config.accounts_file, + &config, &provider, + &declare_from.rpc, config.keystore.as_ref(), + ui, ) .await?; @@ -368,13 +371,8 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()> let provider = rpc.get_provider(&config, ui).await?; - let account = get_account( - &config.account, - &config.accounts_file, - &provider, - config.keystore.as_ref(), - ) - .await?; + let account = + get_account(&config, &provider, &rpc, config.keystore.as_ref(), ui).await?; // safe to unwrap because "constructor" is a standardized name let selector = get_selector_from_name("constructor").unwrap(); @@ -457,13 +455,8 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()> let provider = rpc.get_provider(&config, ui).await?; - let account = get_account( - &config.account, - &config.accounts_file, - &provider, - config.keystore.as_ref(), - ) - .await?; + let account = + get_account(&config, &provider, &rpc, config.keystore.as_ref(), ui).await?; let selector = get_selector_from_name(&function) .context("Failed to convert entry point selector to FieldElement")?; diff --git a/crates/sncast/src/starknet_commands/multicall/mod.rs b/crates/sncast/src/starknet_commands/multicall/mod.rs index 2642f0540a..5bad0bc618 100644 --- a/crates/sncast/src/starknet_commands/multicall/mod.rs +++ b/crates/sncast/src/starknet_commands/multicall/mod.rs @@ -51,13 +51,8 @@ pub async fn multicall( starknet_commands::multicall::Commands::Run(run) => { let provider = run.rpc.get_provider(&config, ui).await?; - let account = get_account( - &config.account, - &config.accounts_file, - &provider, - config.keystore.as_ref(), - ) - .await?; + let account = + get_account(&config, &provider, &run.rpc, config.keystore.as_ref(), ui).await?; let result = starknet_commands::multicall::run::run(run.clone(), &account, wait_config, ui) .await; diff --git a/crates/sncast/src/starknet_commands/script/mod.rs b/crates/sncast/src/starknet_commands/script/mod.rs index 977de56adb..ffeb2c2cdf 100644 --- a/crates/sncast/src/starknet_commands/script/mod.rs +++ b/crates/sncast/src/starknet_commands/script/mod.rs @@ -1,6 +1,7 @@ use crate::starknet_commands::script::run::Run; use crate::{Cli, starknet_commands::script::init::Init}; use crate::{get_cast_config, process_command_result, starknet_commands}; +use anyhow::Context; use clap::{Args, Subcommand}; use foundry_ui::UI; use sncast::helpers::scarb_utils::{ @@ -78,12 +79,14 @@ pub fn run_script_command( ))) }; + let url = run.rpc.get_url(&config.url).context("Failed to get url")?; let result = starknet_commands::script::run::run( &run.script_name, &metadata_with_deps, &package_metadata, &mut artifacts, &provider, + &url, runtime, &config, state_file_path, diff --git a/crates/sncast/src/starknet_commands/script/run.rs b/crates/sncast/src/starknet_commands/script/run.rs index 7168a4d360..e8b5de5c39 100644 --- a/crates/sncast/src/starknet_commands/script/run.rs +++ b/crates/sncast/src/starknet_commands/script/run.rs @@ -284,6 +284,7 @@ pub fn run( package_metadata: &PackageMetadata, artifacts: &mut HashMap, provider: &JsonRpcClient, + url: &str, tokio_runtime: Runtime, config: &CastConfig, state_file_path: Option, @@ -364,11 +365,16 @@ pub fn run( let account = if config.account.is_empty() { None } else { + let rpc_args = RpcArgs { + url: Some(url.to_string()), + network: None, + }; Some(tokio_runtime.block_on(get_account( - &config.account, - &config.accounts_file, + config, provider, + &rpc_args, config.keystore.as_ref(), + ui, ))?) }; let state = StateManager::from(state_file_path)?; diff --git a/crates/sncast/tests/data/accounts/accounts.json b/crates/sncast/tests/data/accounts/accounts.json index de1f43f641..73a8e6ac07 100644 --- a/crates/sncast/tests/data/accounts/accounts.json +++ b/crates/sncast/tests/data/accounts/accounts.json @@ -140,6 +140,11 @@ "public_key": "0x36a64f9d432cce317c3e50bc467d48f2797463babe8f29c83c2e6125ddd1947", "salt": "0x25e7144fce03d200", "type": "open_zeppelin" + }, + "devnet-1": { + "private_key": "0x0000000000000000000000000000000056c12e097e49ea382ca8eadec0839401", + "public_key": "0x048234b9bc6c1e749f4b908d310d8c53dae6564110b05ccf79016dca8ce7dfac", + "address": "0x06f4621e7ad43707b3f69f9df49425c3d94fdc5ab2e444bfa0e7e4edeff7992d" } } } diff --git a/crates/sncast/tests/e2e/devnet_accounts.rs b/crates/sncast/tests/e2e/devnet_accounts.rs new file mode 100644 index 0000000000..c88d0d73c2 --- /dev/null +++ b/crates/sncast/tests/e2e/devnet_accounts.rs @@ -0,0 +1,178 @@ +use crate::helpers::constants::{MAP_CONTRACT_ADDRESS_SEPOLIA, SEPOLIA_RPC_URL, URL}; +use crate::helpers::fixtures::copy_file; +use crate::helpers::runner::runner; +use indoc::indoc; +use shared::test_utils::output_assert::{assert_stderr_contains, assert_stdout_contains}; +use tempfile::tempdir; +use test_case::test_case; + +#[test_case(1)] +#[test_case(20)] +#[tokio::test] +pub async fn happy_case(account_number: u8) { + let temp_dir = tempdir().expect("Unable to create a temporary directory"); + + let account = format!("devnet-{account_number}"); + let args = vec![ + "--account", + &account, + "invoke", + "--url", + URL, + "--contract-address", + MAP_CONTRACT_ADDRESS_SEPOLIA, + "--function", + "put", + "--calldata", + "0x1 0x2", + ]; + + let snapbox = runner(&args).current_dir(temp_dir.path()); + let output = snapbox.assert().success(); + + assert_stdout_contains( + output, + indoc! { + " + Success: Invoke completed + + Transaction Hash: 0x0[..] + " + }, + ); +} + +#[test_case(0)] +#[test_case(21)] +#[tokio::test] +pub async fn account_number_out_of_range(account_number: u8) { + let temp_dir = tempdir().expect("Unable to create a temporary directory"); + + let account = format!("devnet-{account_number}"); + let args = vec![ + "--account", + &account, + "invoke", + "--url", + URL, + "--contract-address", + MAP_CONTRACT_ADDRESS_SEPOLIA, + "--function", + "put", + "--calldata", + "0x1 0x2", + ]; + + let snapbox = runner(&args).current_dir(temp_dir.path()); + let output = snapbox.assert().failure(); + + assert_stderr_contains( + output, + indoc! { + " + Error: Devnet account number must be between 1 and 20 + " + }, + ); +} + +#[tokio::test] +pub async fn account_name_already_exists() { + let accounts_file = "accounts.json"; + let temp_dir = tempdir().expect("Unable to create a temporary directory"); + + copy_file( + "tests/data/accounts/accounts.json", + temp_dir.path().join(accounts_file), + ); + + let args = vec![ + "--accounts-file", + accounts_file, + "--account", + "devnet-1", + "invoke", + "--url", + URL, + "--contract-address", + MAP_CONTRACT_ADDRESS_SEPOLIA, + "--function", + "put", + "--calldata", + "0x1 0x2", + ]; + + let snapbox = runner(&args).current_dir(temp_dir.path()); + let output = snapbox.assert().success(); + + assert_stdout_contains( + output, + indoc! { + " + [WARNING] Using account devnet-1 from accounts file accounts.json. To use an inbuilt devnet account, please rename your existing account or use an account with a different number. + + Success: Invoke completed + + Transaction Hash: 0x0[..] + " + }, + ); +} + +#[tokio::test] +pub async fn use_devnet_account_with_network_not_being_devnet() { + let temp_dir = tempdir().expect("Unable to create a temporary directory"); + + let args = vec![ + "--account", + "devnet-1", + "invoke", + "--url", + SEPOLIA_RPC_URL, + "--contract-address", + MAP_CONTRACT_ADDRESS_SEPOLIA, + "--function", + "put", + "--calldata", + "0x1 0x2", + ]; + + let snapbox = runner(&args).current_dir(temp_dir.path()); + let output = snapbox.assert().failure(); + + assert_stderr_contains( + output, + format! {"Error: 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." + }, + ); +} + +#[test_case("mainnet")] +#[test_case("sepolia")] +#[tokio::test] +pub async fn use_devnet_account_with_network_flags(network: &str) { + let temp_dir = tempdir().expect("Unable to create a temporary directory"); + + let args = vec![ + "--account", + "devnet-1", + "invoke", + "--network", + network, + "--contract-address", + MAP_CONTRACT_ADDRESS_SEPOLIA, + "--function", + "put", + "--calldata", + "0x1 0x2", + ]; + + let snapbox = runner(&args).current_dir(temp_dir.path()); + let output = snapbox.assert().failure(); + + assert_stderr_contains( + output, + format! {"Error: Devnet accounts cannot be used with `--network {network}`" + }, + ); +} diff --git a/crates/sncast/tests/e2e/mod.rs b/crates/sncast/tests/e2e/mod.rs index 5f9d1f1263..b545d92821 100644 --- a/crates/sncast/tests/e2e/mod.rs +++ b/crates/sncast/tests/e2e/mod.rs @@ -5,6 +5,7 @@ mod completions; mod declare; mod declare_from; mod deploy; +mod devnet_accounts; mod fee; mod invoke; mod main_tests; 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/fixtures.rs b/crates/sncast/tests/helpers/fixtures.rs index 1944546ecf..b2a5b0e582 100644 --- a/crates/sncast/tests/helpers/fixtures.rs +++ b/crates/sncast/tests/helpers/fixtures.rs @@ -4,16 +4,19 @@ use anyhow::Context; use camino::{Utf8Path, Utf8PathBuf}; use conversions::string::IntoHexStr; use core::str; +use foundry_ui::UI; use fs_extra::dir::{CopyOptions, copy}; use serde::Deserialize; use serde::de::DeserializeOwned; use serde_json::{Map, Value, json}; use sncast::helpers::account::load_accounts; use sncast::helpers::braavos::BraavosAccountFactory; +use sncast::helpers::configuration::CastConfig; use sncast::helpers::constants::{ BRAAVOS_BASE_ACCOUNT_CLASS_HASH, BRAAVOS_CLASS_HASH, OZ_CLASS_HASH, READY_CLASS_HASH, }; use sncast::helpers::fee::FeeSettings; +use sncast::helpers::rpc::RpcArgs; use sncast::helpers::scarb_utils::get_package_metadata; use sncast::state::state_file::{ ScriptTransactionEntry, ScriptTransactionOutput, ScriptTransactionStatus, @@ -199,14 +202,18 @@ pub async fn invoke_contract( constructor_calldata: &[&str], ) -> InvokeTransactionResult { let provider = get_provider(URL).expect("Could not get the provider"); - let account = get_account( - account, - &Utf8PathBuf::from(ACCOUNT_FILE_PATH), - &provider, - None, - ) - .await - .expect("Could not get the account"); + let config = CastConfig { + account: account.to_string(), + accounts_file: Utf8PathBuf::from(ACCOUNT_FILE_PATH), + ..Default::default() + }; + let rpc_args = RpcArgs { + url: Some(URL.to_string()), + network: None, + }; + let account = get_account(&config, &provider, &rpc_args, None, &UI::default()) + .await + .expect("Could not get the account"); let mut calldata: Vec = vec![]; 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; diff --git a/crates/sncast/tests/integration/lib_tests.rs b/crates/sncast/tests/integration/lib_tests.rs index aacd77e056..baea0124cb 100644 --- a/crates/sncast/tests/integration/lib_tests.rs +++ b/crates/sncast/tests/integration/lib_tests.rs @@ -4,7 +4,10 @@ use crate::helpers::constants::{ use crate::helpers::fixtures::create_test_provider; use camino::Utf8PathBuf; +use foundry_ui::UI; use shared::rpc::{get_rpc_version, is_expected_version}; +use sncast::helpers::configuration::CastConfig; +use sncast::helpers::rpc::RpcArgs; use sncast::{check_if_legacy_contract, get_account, get_provider}; use starknet::accounts::Account; use starknet::macros::felt; @@ -36,14 +39,18 @@ async fn test_get_provider_empty_url() { #[tokio::test] async fn test_get_account() { let provider = create_test_provider(); - let account = get_account( - "user1", - &Utf8PathBuf::from("tests/data/accounts/accounts.json"), - &provider, - None, - ) - .await - .unwrap(); + let config = CastConfig { + account: "user1".to_string(), + accounts_file: Utf8PathBuf::from("tests/data/accounts/accounts.json"), + ..Default::default() + }; + let rpc_args = RpcArgs { + url: Some(URL.to_string()), + network: None, + }; + let account = get_account(&config, &provider, &rpc_args, None, &UI::default()) + .await + .unwrap(); assert_eq!(account.chain_id(), felt!("0x534e5f5345504f4c4941")); assert_eq!( @@ -55,13 +62,16 @@ async fn test_get_account() { #[tokio::test] async fn test_get_account_no_file() { let provider = create_test_provider(); - let account = get_account( - "user1", - &Utf8PathBuf::from("tests/data/accounts/nonexistentfile.json"), - &provider, - None, - ) - .await; + let config = CastConfig { + account: "user1".to_string(), + accounts_file: Utf8PathBuf::from("tests/data/accounts/nonexistentfile.json"), + ..Default::default() + }; + let rpc_args = RpcArgs { + url: Some(URL.to_string()), + network: None, + }; + let account = get_account(&config, &provider, &rpc_args, None, &UI::default()).await; let err = account.unwrap_err(); assert!( err.to_string() @@ -72,13 +82,16 @@ async fn test_get_account_no_file() { #[tokio::test] async fn test_get_account_invalid_file() { let provider = create_test_provider(); - let account = get_account( - "user1", - &Utf8PathBuf::from("tests/data/accounts/invalid_format.json"), - &provider, - None, - ) - .await; + let config = CastConfig { + account: "user1".to_string(), + accounts_file: Utf8PathBuf::from("tests/data/accounts/invalid_format.json"), + ..Default::default() + }; + let rpc_args = RpcArgs { + url: Some(URL.to_string()), + network: None, + }; + let account = get_account(&config, &provider, &rpc_args, None, &UI::default()).await; let err = account.unwrap_err(); assert!(err .to_string() @@ -89,13 +102,16 @@ async fn test_get_account_invalid_file() { #[tokio::test] async fn test_get_account_no_account() { let provider = create_test_provider(); - let account = get_account( - "", - &Utf8PathBuf::from("tests/data/accounts/accounts.json"), - &provider, - None, - ) - .await; + let config = CastConfig { + account: String::new(), + accounts_file: Utf8PathBuf::from("tests/data/accounts/accounts.json"), + ..Default::default() + }; + let rpc_args = RpcArgs { + url: Some(URL.to_string()), + network: None, + }; + let account = get_account(&config, &provider, &rpc_args, None, &UI::default()).await; let err = account.unwrap_err(); assert!( err.to_string() @@ -106,13 +122,16 @@ async fn test_get_account_no_account() { #[tokio::test] async fn test_get_account_no_user_for_network() { let provider = create_test_provider(); - let account = get_account( - "user100", - &Utf8PathBuf::from("tests/data/accounts/accounts.json"), - &provider, - None, - ) - .await; + let config = CastConfig { + account: "user100".to_string(), + accounts_file: Utf8PathBuf::from("tests/data/accounts/accounts.json"), + ..Default::default() + }; + let rpc_args = RpcArgs { + url: Some(URL.to_string()), + network: None, + }; + let account = get_account(&config, &provider, &rpc_args, None, &UI::default()).await; let err = account.unwrap_err(); assert!( err.to_string() @@ -123,13 +142,16 @@ async fn test_get_account_no_user_for_network() { #[tokio::test] async fn test_get_account_failed_to_convert_field_elements() { let provider = create_test_provider(); - let account1 = get_account( - "with_invalid_private_key", - &Utf8PathBuf::from("tests/data/accounts/faulty_accounts_invalid_felt.json"), - &provider, - None, - ) - .await; + let config = CastConfig { + account: "with_invalid_private_key".to_string(), + accounts_file: Utf8PathBuf::from("tests/data/accounts/faulty_accounts_invalid_felt.json"), + ..Default::default() + }; + let rpc_args = RpcArgs { + url: Some(URL.to_string()), + network: None, + }; + let account1 = get_account(&config, &provider, &rpc_args, None, &UI::default()).await; let err = account1.unwrap_err(); assert!(err.to_string().contains( diff --git a/crates/sncast/tests/integration/wait_for_tx.rs b/crates/sncast/tests/integration/wait_for_tx.rs index 9b7fb3f9ad..fca7c9fb28 100644 --- a/crates/sncast/tests/integration/wait_for_tx.rs +++ b/crates/sncast/tests/integration/wait_for_tx.rs @@ -1,15 +1,17 @@ use crate::helpers::{ - constants::{ACCOUNT, ACCOUNT_FILE_PATH}, + constants::{ACCOUNT, ACCOUNT_FILE_PATH, URL}, fixtures::{create_test_provider, invoke_contract}, }; +use camino::Utf8PathBuf; use foundry_ui::UI; -use sncast::helpers::{constants::UDC_ADDRESS, fee::FeeSettings}; +use sncast::helpers::{ + configuration::CastConfig, constants::UDC_ADDRESS, fee::FeeSettings, rpc::RpcArgs, +}; use crate::helpers::constants::{ CONSTRUCTOR_WITH_PARAMS_CONTRACT_CLASS_HASH_SEPOLIA, MAP_CONTRACT_CLASS_HASH_SEPOLIA, MAP_CONTRACT_DECLARE_TX_HASH_SEPOLIA, }; -use camino::Utf8PathBuf; use conversions::string::IntoHexStr; use sncast::{ValidatedWaitParams, get_account}; use sncast::{WaitForTx, handle_wait_for_tx, wait_for_tx}; @@ -35,14 +37,18 @@ async fn test_happy_path() { #[tokio::test] async fn test_rejected_transaction() { let provider = create_test_provider(); - let account = get_account( - ACCOUNT, - &Utf8PathBuf::from(ACCOUNT_FILE_PATH), - &provider, - None, - ) - .await - .expect("Could not get the account"); + let config = CastConfig { + account: ACCOUNT.to_string(), + accounts_file: Utf8PathBuf::from(ACCOUNT_FILE_PATH), + ..Default::default() + }; + let rpc_args = RpcArgs { + url: Some(URL.to_string()), + network: None, + }; + let account = get_account(&config, &provider, &rpc_args, None, &UI::default()) + .await + .expect("Could not get the account"); let factory = ContractFactory::new(MAP_CONTRACT_CLASS_HASH_SEPOLIA.parse().unwrap(), account); let deployment = factory