Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
7267154
Add `DevetClient`
franciszekjob Sep 26, 2025
ab2a7cd
Support inbuilt devnet accounts
franciszekjob Sep 26, 2025
0e933b4
Rename `DevnetClient` -> `DevnetProvider`
franciszekjob Sep 26, 2025
2f7bd43
Merge branch '3603-devnet-client' of https://github.com/foundry-rs/st…
franciszekjob Sep 26, 2025
5db4ffe
Remove `account_exists_in_accounts_file`
franciszekjob Sep 26, 2025
7a57e73
Merge branch '3603-devnet-client' of https://github.com/foundry-rs/st…
franciszekjob Sep 26, 2025
9c42ddd
Add `account_exists_in_accounts_file`
franciszekjob Sep 26, 2025
c2baa3e
Refactor test
franciszekjob Sep 26, 2025
31e77fa
Merge branch '3603-devnet-client' of https://github.com/foundry-rs/st…
franciszekjob Sep 26, 2025
2213285
Remove functions
franciszekjob Sep 26, 2025
8640934
Merge branch '3603-devnet-client' of https://github.com/foundry-rs/st…
franciszekjob Sep 26, 2025
245d69f
Fix imports
franciszekjob Sep 26, 2025
ec03598
Add tests
franciszekjob Sep 26, 2025
64c8c5b
Fix typo
franciszekjob Sep 26, 2025
fff8c9d
Merge branch '3603-devnet-client' of https://github.com/foundry-rs/st…
franciszekjob Sep 26, 2025
ef13d2a
Fix test
franciszekjob Sep 26, 2025
dbb17a0
Fix newline
franciszekjob Sep 26, 2025
a534600
Add `use_devnet_account_with_node_not_being_devnet` test
franciszekjob Sep 26, 2025
b3e4cfb
Fix tests
franciszekjob Sep 26, 2025
f8a4019
Fix tests
franciszekjob Sep 28, 2025
fa4af76
Little refactor
franciszekjob Sep 29, 2025
656dd30
Merge branch '3603-devnet-client' of https://github.com/foundry-rs/st…
franciszekjob Sep 29, 2025
1604d50
Remove enters
franciszekjob Sep 29, 2025
ce48a81
Remove enters
franciszekjob Sep 29, 2025
944c3a2
Update warning message
franciszekjob Sep 29, 2025
d7ea750
Fix devnet path
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
7354f77
Merge branch '3603-devnet-client' into 3603-2-support-inbuilt-devnet-…
franciszekjob Sep 30, 2025
bb2f942
Merge branch 'release-0.50.0' into 3603-devnet-client
franciszekjob Sep 30, 2025
646c0ea
Merge branch '3603-devnet-client' into 3603-2-support-inbuilt-devnet-…
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
78c6cbd
git
franciszekjob Oct 1, 2025
3850d70
Apply code review suggestion
franciszekjob Oct 1, 2025
98d254b
Fix error message
franciszekjob Oct 1, 2025
7adb617
Merge branch '3603-2-support-inbuilt-devnet-accounts' of https://gith…
franciszekjob Oct 1, 2025
ec6f02e
Remove files
franciszekjob Oct 1, 2025
c9ca88d
Fix errors
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
354b537
Merge branch '3603-devnet-client' into 3603-2-support-inbuilt-devnet-…
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
9a5c346
Merge branch '3603-devnet-client' of https://github.com/foundry-rs/st…
franciszekjob Oct 2, 2025
0fcf44c
Ensure devnet is alive in `get_account_from_devnet`
franciszekjob Oct 2, 2025
2d52837
Merge branch '3603-2-support-inbuilt-devnet-accounts' of https://gith…
franciszekjob Oct 2, 2025
9c97bf1
Formatting
franciszekjob Oct 2, 2025
d8edc5f
Fix test
franciszekjob Oct 2, 2025
4a4899d
Apply code review suggestions
franciszekjob Oct 2, 2025
fc0bb1d
Remove assertions
franciszekjob Oct 3, 2025
3180a75
Apply code review suggestion
franciszekjob Oct 3, 2025
02b9c6e
Fix test
franciszekjob Oct 3, 2025
fdb41d4
Fix linting
franciszekjob Oct 3, 2025
30717f3
Merge branch '3603-devnet-client' into 3603-2-support-inbuilt-devnet-…
franciszekjob Oct 3, 2025
623041b
Disallow devnet accounts usage with sepolia and mainnet networks
franciszekjob Oct 6, 2025
bd3bd47
Merge branch '3603-2-support-inbuilt-devnet-accounts' of https://gith…
franciszekjob Oct 6, 2025
1072c02
Add `use_devnet_account_with_network_flags`
franciszekjob Oct 6, 2025
e7af6c5
Fix devnet path
franciszekjob Oct 6, 2025
5f47a99
Update error msg
franciszekjob Oct 6, 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
66 changes: 63 additions & 3 deletions crates/sncast/src/helpers/account.rs
Original file line number Diff line number Diff line change
@@ -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, bail};
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};
Expand Down Expand Up @@ -48,3 +55,56 @@ pub fn load_accounts(accounts_file: &Utf8PathBuf) -> Result<Value> {

Ok(accounts)
}

pub fn check_account_exists(
account_name: &str,
network_name: &str,
accounts_file: &Utf8PathBuf,
) -> Result<bool> {
check_account_file_exists(accounts_file)?;

let accounts: HashMap<String, HashMap<String, AccountData>> =
read_and_parse_json_file(accounts_file)?;

if let Some(network_accounts) = accounts.get(network_name) {
Ok(network_accounts.contains_key(account_name))
} else {
bail!("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<HttpTransport>,
url: &str,
) -> Result<SingleOwnerAccount<&'a JsonRpcClient<HttpTransport>, LocalWallet>> {
let account_number: u8 = account
.strip_prefix("devnet-")
.map(|s| s.parse::<u8>().expect("Invalid devnet account number"))
.context("Failed to parse devnet account number")?;

let devnet_provider = DevnetProvider::new(url);
let devnet_config = devnet_provider.get_config().await?;

if account_number > devnet_config.total_accounts || account_number == 0 {
bail!(
"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
}
116 changes: 116 additions & 0 deletions crates/sncast/src/helpers/devnet_provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use crate::AccountData;
use ::serde::{Deserialize, Serialize, de::DeserializeOwned};
use anyhow::Error;
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 {
/// The `devnet_getConfig` method.
#[serde(rename = "devnet_getConfig")]
GetConfig,

/// The `devnet_getPredeployedAccounts` method.
#[serde(rename = "devnet_getPredeployedAccounts")]
GetPredeployedAccounts,
}

impl DevnetProvider {
/// Constructs a new [`DevnetProvider`] from given url.
#[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) -> Result<R, Error>
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
.expect("Error occurred during request")
.json::<serde_json::Value>()
.await;

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

/// 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
}
}

#[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
2 changes: 1 addition & 1 deletion crates/sncast/src/helpers/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ impl RpcArgs {
}

#[must_use]
fn get_url(&self, config_url: &String) -> Option<String> {
pub fn get_url(&self, config_url: &String) -> Option<String> {
if let Some(network) = self.network {
let free_provider = FreeProvider::semi_random();
Some(network.url(&free_provider))
Expand Down
28 changes: 28 additions & 0 deletions crates/sncast/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use crate::helpers::account::{check_account_exists, get_account_from_devnet, is_devnet_account};
use crate::helpers::constants::{DEFAULT_STATE_FILE_SUFFIX, WAIT_RETRY_INTERVAL, WAIT_TIMEOUT};
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;
Expand Down Expand Up @@ -245,6 +247,32 @@ pub async fn get_nonce(
}

pub async fn get_account<'a>(
account: &str,
accounts_file: &Utf8PathBuf,
provider: &'a JsonRpcClient<HttpTransport>,
url: &str,
keystore: Option<&Utf8PathBuf>,
ui: &UI,
) -> Result<SingleOwnerAccount<&'a JsonRpcClient<HttpTransport>, LocalWallet>> {
let chain_id = get_chain_id(provider).await?;
let network_name = chain_id_to_network_name(chain_id);
let is_devnet_account = is_devnet_account(account);
let exists_in_accounts_file = check_account_exists(account, &network_name, accounts_file)?;

if is_devnet_account && exists_in_accounts_file {
ui.println(&WarningMessage::new(format!(
"Using account {account} from accounts file {accounts_file}. To use inbuilt devnet account, please change the name of your existing account {account}."
)));
ui.print_blank_line();
return get_account_from_accounts_file(account, accounts_file, provider, keystore).await;
} else if is_devnet_account && !exists_in_accounts_file {
return get_account_from_devnet(account, provider, url).await;
}

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<HttpTransport>,
Expand Down
15 changes: 15 additions & 0 deletions crates/sncast/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,14 +238,17 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()>
match cli.command {
Commands::Declare(declare) => {
let provider = declare.rpc.get_provider(&config, ui).await?;
let url = declare.rpc.get_url(&config.url).expect("Failed to get url");

let rpc = declare.rpc.clone();

let account = get_account(
&config.account,
&config.accounts_file,
&provider,
&url,
config.keystore.as_ref(),
ui,
)
.await?;
let manifest_path = assert_manifest_path_exists()?;
Expand Down Expand Up @@ -292,11 +295,17 @@ 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 url = declare_from
.rpc
.get_url(&config.url)
.expect("Failed to get url");
let account = get_account(
&config.account,
&config.accounts_file,
&provider,
&url,
config.keystore.as_ref(),
ui,
)
.await?;

Expand Down Expand Up @@ -339,12 +348,15 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()>
} = deploy;

let provider = rpc.get_provider(&config, ui).await?;
let url = rpc.get_url(&config.url).expect("Failed to get url");

let account = get_account(
&config.account,
&config.accounts_file,
&provider,
&url,
config.keystore.as_ref(),
ui,
)
.await?;

Expand Down Expand Up @@ -428,12 +440,15 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()>
} = invoke;

let provider = rpc.get_provider(&config, ui).await?;
let url = rpc.get_url(&config.url).expect("Failed to get url");

let account = get_account(
&config.account,
&config.accounts_file,
&provider,
&url,
config.keystore.as_ref(),
ui,
)
.await?;

Expand Down
3 changes: 3 additions & 0 deletions crates/sncast/src/starknet_commands/multicall/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,15 @@ pub async fn multicall(
}
starknet_commands::multicall::Commands::Run(run) => {
let provider = run.rpc.get_provider(&config, ui).await?;
let url = run.rpc.get_url(&config.url).expect("Failed to get url");

let account = get_account(
&config.account,
&config.accounts_file,
&provider,
&url,
config.keystore.as_ref(),
ui,
)
.await?;
let result =
Expand Down
2 changes: 2 additions & 0 deletions crates/sncast/src/starknet_commands/script/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,14 @@ pub fn run_script_command(
)))
};

let url = run.rpc.get_url(&config.url).expect("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,
Expand Down
3 changes: 3 additions & 0 deletions crates/sncast/src/starknet_commands/script/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ pub fn run(
package_metadata: &PackageMetadata,
artifacts: &mut HashMap<String, StarknetContractArtifacts>,
provider: &JsonRpcClient<HttpTransport>,
url: &str,
tokio_runtime: Runtime,
config: &CastConfig,
state_file_path: Option<Utf8PathBuf>,
Expand Down Expand Up @@ -368,7 +369,9 @@ pub fn run(
&config.account,
&config.accounts_file,
provider,
url,
config.keystore.as_ref(),
ui,
))?)
};
let state = StateManager::from(state_file_path)?;
Expand Down
5 changes: 5 additions & 0 deletions crates/sncast/tests/data/accounts/accounts.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@
"public_key": "0x36a64f9d432cce317c3e50bc467d48f2797463babe8f29c83c2e6125ddd1947",
"salt": "0x25e7144fce03d200",
"type": "open_zeppelin"
},
"devnet-1": {
"private_key": "0x0000000000000000000000000000000056c12e097e49ea382ca8eadec0839401",
"public_key": "0x048234b9bc6c1e749f4b908d310d8c53dae6564110b05ccf79016dca8ce7dfac",
"address": "0x06f4621e7ad43707b3f69f9df49425c3d94fdc5ab2e444bfa0e7e4edeff7992d"
}
}
}
Expand Down
Loading
Loading