Skip to content

Commit d2ca44a

Browse files
cptarturfranciszekjobkkawula
authored
Cast default RPC providers (#2855)
<!-- Reference any GitHub issues resolved by this PR --> Closes #2708 ## Introduced changes <!-- A brief description of the changes --> - Added default RPC providers to `sncast` under `--network` flag - Breaking: renamed `--network` flag to `--network-name` in `sncast account delete` and `sncast account import` due to conflict with the added flag ## Checklist <!-- Make sure all of these are complete --> - [x] Linked relevant issue - [x] Updated relevant documentation - [x] Added relevant tests - [x] Performed self-review of the code - [x] Added changes to `CHANGELOG.md` --------- Co-authored-by: Franciszek Job <[email protected]> Co-authored-by: kkawula <[email protected]>
1 parent 1b56757 commit d2ca44a

38 files changed

+566
-117
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313

1414
- coverage validation now supports comments in `Scarb.toml`
1515

16+
### Cast
17+
18+
#### Added
19+
20+
- Default RPC providers under `--network` flag
21+
22+
#### Changed
23+
24+
- Renamed `--network` flag to `--network-name` in `sncast account delete` command
25+
1626
## [0.36.0] - 2025-01-15
1727

1828
### Forge

crates/docs/src/snippet.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,24 @@ impl SnippetType {
5252
}
5353
}
5454

55-
#[derive(Debug, Deserialize, Serialize, Default)]
55+
#[derive(Debug, Deserialize, Serialize)]
56+
#[serde(default)]
5657
pub struct SnippetConfig {
57-
#[serde(default)]
5858
pub ignored: bool,
5959
pub package_name: Option<String>,
60-
#[serde(default)]
6160
pub ignored_output: bool,
61+
pub replace_network: bool,
62+
}
63+
64+
impl Default for SnippetConfig {
65+
fn default() -> Self {
66+
Self {
67+
ignored: false,
68+
package_name: None,
69+
ignored_output: false,
70+
replace_network: true,
71+
}
72+
}
6273
}
6374

6475
#[derive(Debug)]

crates/sncast/src/helpers/rpc.rs

Lines changed: 116 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,50 @@
1-
use crate::{get_provider, helpers::configuration::CastConfig};
1+
use crate::helpers::configuration::CastConfig;
2+
use crate::{get_provider, Network};
3+
use anyhow::{bail, Context, Result};
24
use clap::Args;
5+
use shared::consts::RPC_URL_VERSION;
36
use shared::verify_and_warn_if_incompatible_rpc_version;
47
use starknet::providers::{jsonrpc::HttpTransport, JsonRpcClient};
8+
use std::env::current_exe;
9+
use std::time::UNIX_EPOCH;
510

611
#[derive(Args, Clone, Debug, Default)]
12+
#[group(required = false, multiple = false)]
713
pub struct RpcArgs {
814
/// RPC provider url address; overrides url from snfoundry.toml
915
#[clap(short, long)]
1016
pub url: Option<String>,
17+
18+
/// Use predefined network with a public provider. Note that this option may result in rate limits or other unexpected behavior
19+
#[clap(long)]
20+
pub network: Option<Network>,
1121
}
1222

1323
impl RpcArgs {
14-
pub async fn get_provider(
15-
&self,
16-
config: &CastConfig,
17-
) -> anyhow::Result<JsonRpcClient<HttpTransport>> {
18-
let url = self.url.as_ref().unwrap_or(&config.url);
19-
let provider = get_provider(url)?;
24+
pub async fn get_provider(&self, config: &CastConfig) -> Result<JsonRpcClient<HttpTransport>> {
25+
if self.network.is_some() && !config.url.is_empty() {
26+
bail!("The argument '--network' cannot be used when `url` is defined in `snfoundry.toml` for the active profile")
27+
}
28+
29+
let url = if let Some(network) = self.network {
30+
let free_provider = FreeProvider::semi_random();
31+
network.url(&free_provider)
32+
} else {
33+
let url = self.url.clone().or_else(|| {
34+
if config.url.is_empty() {
35+
None
36+
} else {
37+
Some(config.url.clone())
38+
}
39+
});
40+
41+
url.context("Either `--network` or `--url` must be provided")?
42+
};
43+
44+
assert!(!url.is_empty(), "url cannot be empty");
45+
let provider = get_provider(&url)?;
2046

21-
verify_and_warn_if_incompatible_rpc_version(&provider, &url).await?;
47+
verify_and_warn_if_incompatible_rpc_version(&provider, url).await?;
2248

2349
Ok(provider)
2450
}
@@ -28,3 +54,85 @@ impl RpcArgs {
2854
self.url.clone().unwrap_or_else(|| config.url.clone())
2955
}
3056
}
57+
58+
fn installation_constant_seed() -> Result<u64> {
59+
let executable_path = current_exe()?;
60+
let metadata = executable_path.metadata()?;
61+
let modified_time = metadata.modified()?;
62+
let duration = modified_time.duration_since(UNIX_EPOCH)?;
63+
64+
Ok(duration.as_secs())
65+
}
66+
67+
enum FreeProvider {
68+
Blast,
69+
Voyager,
70+
}
71+
72+
impl FreeProvider {
73+
fn semi_random() -> Self {
74+
let seed = installation_constant_seed().unwrap_or(2);
75+
if seed % 2 == 0 {
76+
return Self::Blast;
77+
}
78+
Self::Voyager
79+
}
80+
}
81+
82+
impl Network {
83+
fn url(self, provider: &FreeProvider) -> String {
84+
match self {
85+
Network::Mainnet => Self::free_mainnet_rpc(provider),
86+
Network::Sepolia => Self::free_sepolia_rpc(provider),
87+
}
88+
}
89+
90+
fn free_mainnet_rpc(provider: &FreeProvider) -> String {
91+
match provider {
92+
FreeProvider::Blast => {
93+
format!("https://starknet-mainnet.public.blastapi.io/rpc/{RPC_URL_VERSION}")
94+
}
95+
FreeProvider::Voyager => {
96+
format!("https://free-rpc.nethermind.io/mainnet-juno/{RPC_URL_VERSION}")
97+
}
98+
}
99+
}
100+
101+
fn free_sepolia_rpc(provider: &FreeProvider) -> String {
102+
match provider {
103+
FreeProvider::Blast => {
104+
format!("https://starknet-sepolia.public.blastapi.io/rpc/{RPC_URL_VERSION}")
105+
}
106+
FreeProvider::Voyager => {
107+
format!("https://free-rpc.nethermind.io/sepolia-juno/{RPC_URL_VERSION}")
108+
}
109+
}
110+
}
111+
}
112+
113+
#[cfg(test)]
114+
mod tests {
115+
use super::*;
116+
use semver::Version;
117+
use shared::rpc::is_expected_version;
118+
use starknet::providers::Provider;
119+
use test_case::test_case;
120+
121+
#[test_case(FreeProvider::Voyager)]
122+
#[test_case(FreeProvider::Blast)]
123+
#[tokio::test]
124+
async fn test_mainnet_url_happy_case(free_provider: FreeProvider) {
125+
let provider = get_provider(&Network::free_sepolia_rpc(&free_provider)).unwrap();
126+
let spec_version = provider.spec_version().await.unwrap();
127+
assert!(is_expected_version(&Version::parse(&spec_version).unwrap()));
128+
}
129+
130+
#[test_case(FreeProvider::Voyager)]
131+
#[test_case(FreeProvider::Blast)]
132+
#[tokio::test]
133+
async fn test_sepolia_url_happy_case(free_provider: FreeProvider) {
134+
let provider = get_provider(&Network::free_sepolia_rpc(&free_provider)).unwrap();
135+
let spec_version = provider.spec_version().await.unwrap();
136+
assert!(is_expected_version(&Version::parse(&spec_version).unwrap()));
137+
}
138+
}

crates/sncast/src/lib.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ use starknet::{
3131
signers::{LocalWallet, SigningKey},
3232
};
3333
use starknet_types_core::felt::Felt;
34+
use std::collections::HashMap;
35+
use std::fmt::Display;
3436
use std::str::FromStr;
3537
use std::thread::sleep;
3638
use std::time::Duration;
37-
use std::{collections::HashMap, fmt::Display};
3839
use std::{env, fs};
3940
use thiserror::Error;
4041

@@ -84,6 +85,15 @@ pub enum Network {
8485
Sepolia,
8586
}
8687

88+
impl Display for Network {
89+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90+
match self {
91+
Network::Mainnet => write!(f, "mainnet"),
92+
Network::Sepolia => write!(f, "sepolia"),
93+
}
94+
}
95+
}
96+
8797
impl TryFrom<Felt> for Network {
8898
type Error = anyhow::Error;
8999

crates/sncast/src/main.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,11 @@ async fn run_async_command(
492492
)
493493
.await;
494494

495-
if !import.silent && result.is_ok() && io::stdout().is_terminal() {
495+
if !import.silent
496+
&& result.is_ok()
497+
&& io::stdout().is_terminal()
498+
&& import.rpc.network.is_none()
499+
{
496500
if let Some(account_name) =
497501
result.as_ref().ok().and_then(|r| r.account_name.clone())
498502
{
@@ -519,7 +523,6 @@ async fn run_async_command(
519523
config.account.clone()
520524
};
521525
let result = starknet_commands::account::create::create(
522-
create.rpc.get_url(&config),
523526
&account,
524527
&config.accounts_file,
525528
config.keystore,
@@ -529,7 +532,11 @@ async fn run_async_command(
529532
)
530533
.await;
531534

532-
if !create.silent && result.is_ok() && io::stdout().is_terminal() {
535+
if !create.silent
536+
&& result.is_ok()
537+
&& io::stdout().is_terminal()
538+
&& create.rpc.network.is_none()
539+
{
533540
if let Err(err) = prompt_to_add_account_as_default(&account) {
534541
eprintln!("Error: Failed to launch interactive prompt: {err}");
535542
}

crates/sncast/src/starknet_commands/account/create.rs

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use sncast::helpers::rpc::RpcArgs;
1717
use sncast::response::structs::AccountCreateResponse;
1818
use sncast::{
1919
check_class_hash_exists, check_if_legacy_contract, extract_or_generate_salt, get_chain_id,
20-
get_keystore_password, handle_account_factory_error,
20+
get_keystore_password, handle_account_factory_error, Network,
2121
};
2222
use starknet::accounts::{
2323
AccountDeploymentV1, AccountFactory, ArgentAccountFactory, OpenZeppelinAccountFactory,
@@ -44,7 +44,7 @@ pub struct Create {
4444
pub salt: Option<Felt>,
4545

4646
/// If passed, a profile with provided name and corresponding data will be created in snfoundry.toml
47-
#[clap(long)]
47+
#[clap(long, conflicts_with = "network")]
4848
pub add_profile: Option<String>,
4949

5050
/// Custom contract class hash of declared contract
@@ -61,7 +61,6 @@ pub struct Create {
6161

6262
#[allow(clippy::too_many_arguments)]
6363
pub async fn create(
64-
rpc_url: String,
6564
account: &str,
6665
accounts_file: &Utf8PathBuf,
6766
keystore: Option<Utf8PathBuf>,
@@ -112,24 +111,38 @@ pub async fn create(
112111
legacy,
113112
)?;
114113

115-
let deploy_command = generate_deploy_command_with_keystore(account, &keystore, &rpc_url);
114+
let deploy_command = generate_deploy_command_with_keystore(
115+
account,
116+
&keystore,
117+
create.rpc.url.as_deref(),
118+
create.rpc.network.as_ref(),
119+
);
116120
message.push_str(&deploy_command);
117121
} else {
118122
write_account_to_accounts_file(account, accounts_file, chain_id, account_json.clone())?;
119123

120-
let deploy_command = generate_deploy_command(accounts_file, &rpc_url, account);
124+
let deploy_command = generate_deploy_command(
125+
accounts_file,
126+
create.rpc.url.as_deref(),
127+
create.rpc.network.as_ref(),
128+
account,
129+
);
121130
message.push_str(&deploy_command);
122131
}
123132

124133
if add_profile.is_some() {
125-
let config = CastConfig {
126-
url: rpc_url,
127-
account: account.into(),
128-
accounts_file: accounts_file.into(),
129-
keystore,
130-
..Default::default()
131-
};
132-
add_created_profile_to_configuration(create.add_profile.as_deref(), &config, None)?;
134+
if let Some(url) = &create.rpc.url {
135+
let config = CastConfig {
136+
url: url.clone(),
137+
account: account.into(),
138+
accounts_file: accounts_file.into(),
139+
keystore,
140+
..Default::default()
141+
};
142+
add_created_profile_to_configuration(create.add_profile.as_deref(), &config, None)?;
143+
} else {
144+
unreachable!("Conflicting arguments should be handled in clap");
145+
}
133146
}
134147

135148
Ok(AccountCreateResponse {
@@ -328,7 +341,22 @@ fn write_account_to_file(
328341
Ok(())
329342
}
330343

331-
fn generate_deploy_command(accounts_file: &Utf8PathBuf, rpc_url: &str, account: &str) -> String {
344+
fn generate_network_flag(rpc_url: Option<&str>, network: Option<&Network>) -> String {
345+
if let Some(rpc_url) = rpc_url {
346+
format!("--url {rpc_url}")
347+
} else if let Some(network) = network {
348+
format!("--network {network}")
349+
} else {
350+
unreachable!("Either `--rpc_url` or `--network` must be provided.")
351+
}
352+
}
353+
354+
fn generate_deploy_command(
355+
accounts_file: &Utf8PathBuf,
356+
rpc_url: Option<&str>,
357+
network: Option<&Network>,
358+
account: &str,
359+
) -> String {
332360
let accounts_flag = if accounts_file
333361
.to_string()
334362
.contains("starknet_accounts/starknet_open_zeppelin_accounts.json")
@@ -338,19 +366,24 @@ fn generate_deploy_command(accounts_file: &Utf8PathBuf, rpc_url: &str, account:
338366
format!(" --accounts-file {accounts_file}")
339367
};
340368

369+
let network_flag = generate_network_flag(rpc_url, network);
370+
341371
format!(
342372
"\n\nAfter prefunding the address, run:\n\
343-
sncast{accounts_flag} account deploy --url {rpc_url} --name {account} --fee-token strk"
373+
sncast{accounts_flag} account deploy {network_flag} --name {account} --fee-token strk"
344374
)
345375
}
346376

347377
fn generate_deploy_command_with_keystore(
348378
account: &str,
349379
keystore: &Utf8PathBuf,
350-
rpc_url: &str,
380+
rpc_url: Option<&str>,
381+
network: Option<&Network>,
351382
) -> String {
383+
let network_flag = generate_network_flag(rpc_url, network);
384+
352385
format!(
353386
"\n\nAfter prefunding the address, run:\n\
354-
sncast --account {account} --keystore {keystore} account deploy --url {rpc_url} --fee-token strk"
387+
sncast --account {account} --keystore {keystore} account deploy {network_flag} --fee-token strk"
355388
)
356389
}

0 commit comments

Comments
 (0)