Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cast default RPC providers #2855

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Cast

#### Added

- Default RPC providers under `--network` flag

#### Changed

- Renamed `--network` flag to `--network-name` in `sncast account delete` command

## [0.36.0] - 2025-01-15

### Forge
Expand Down
17 changes: 14 additions & 3 deletions crates/docs/src/snippet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,24 @@ impl SnippetType {
}
}

#[derive(Debug, Deserialize, Serialize, Default)]
#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct SnippetConfig {
#[serde(default)]
pub ignored: bool,
pub package_name: Option<String>,
#[serde(default)]
pub ignored_output: bool,
pub replace_network: bool,
}

impl Default for SnippetConfig {
fn default() -> Self {
Self {
ignored: false,
package_name: None,
ignored_output: false,
replace_network: true,
}
}
}

#[derive(Debug)]
Expand Down
124 changes: 116 additions & 8 deletions crates/sncast/src/helpers/rpc.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,50 @@
use crate::{get_provider, helpers::configuration::CastConfig};
use crate::helpers::configuration::CastConfig;
use crate::{get_provider, Network};
use anyhow::{bail, Context, Result};
use clap::Args;
use shared::consts::RPC_URL_VERSION;
use shared::verify_and_warn_if_incompatible_rpc_version;
use starknet::providers::{jsonrpc::HttpTransport, JsonRpcClient};
use std::env::current_exe;
use std::time::UNIX_EPOCH;

#[derive(Args, Clone, Debug, Default)]
#[group(required = false, multiple = false)]
pub struct RpcArgs {
/// RPC provider url address; overrides url from snfoundry.toml
#[clap(short, long)]
pub url: Option<String>,

/// Use predefined network with a public provider. Note that this option may result in rate limits or other unexpected behavior
#[clap(long)]
pub network: Option<Network>,
}

impl RpcArgs {
pub async fn get_provider(
&self,
config: &CastConfig,
) -> anyhow::Result<JsonRpcClient<HttpTransport>> {
let url = self.url.as_ref().unwrap_or(&config.url);
let provider = get_provider(url)?;
pub async fn get_provider(&self, config: &CastConfig) -> Result<JsonRpcClient<HttpTransport>> {
if self.network.is_some() && !config.url.is_empty() {
bail!("The argument '--network' cannot be used when `url` is defined in `snfoundry.toml` for the active profile")
}

let url = if let Some(network) = self.network {
let free_provider = FreeProvider::semi_random();
network.url(&free_provider)
} else {
let url = self.url.clone().or_else(|| {
if config.url.is_empty() {
None
} else {
Some(config.url.clone())
}
});

url.context("Either `--network` or `--url` must be provided")?
};

cptartur marked this conversation as resolved.
Show resolved Hide resolved
assert!(!url.is_empty(), "url cannot be empty");
let provider = get_provider(&url)?;

verify_and_warn_if_incompatible_rpc_version(&provider, &url).await?;
verify_and_warn_if_incompatible_rpc_version(&provider, url).await?;

Ok(provider)
}
Expand All @@ -28,3 +54,85 @@ impl RpcArgs {
self.url.clone().unwrap_or_else(|| config.url.clone())
}
}

fn installation_constant_seed() -> Result<u64> {
let executable_path = current_exe()?;
let metadata = executable_path.metadata()?;
let modified_time = metadata.modified()?;
let duration = modified_time.duration_since(UNIX_EPOCH)?;

Ok(duration.as_secs())
}

enum FreeProvider {
Blast,
Voyager,
}

impl FreeProvider {
fn semi_random() -> Self {
let seed = installation_constant_seed().unwrap_or(2);
if seed % 2 == 0 {
return Self::Blast;
}
Self::Voyager
}
}

impl Network {
fn url(self, provider: &FreeProvider) -> String {
match self {
Network::Mainnet => Self::free_mainnet_rpc(provider),
Network::Sepolia => Self::free_sepolia_rpc(provider),
}
}

fn free_mainnet_rpc(provider: &FreeProvider) -> String {
match provider {
FreeProvider::Blast => {
format!("https://starknet-mainnet.public.blastapi.io/rpc/{RPC_URL_VERSION}")
}
FreeProvider::Voyager => {
format!("https://free-rpc.nethermind.io/mainnet-juno/{RPC_URL_VERSION}")
}
}
}

fn free_sepolia_rpc(provider: &FreeProvider) -> String {
match provider {
FreeProvider::Blast => {
format!("https://starknet-sepolia.public.blastapi.io/rpc/{RPC_URL_VERSION}")
}
FreeProvider::Voyager => {
format!("https://free-rpc.nethermind.io/sepolia-juno/{RPC_URL_VERSION}")
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use semver::Version;
use shared::rpc::is_expected_version;
use starknet::providers::Provider;
use test_case::test_case;

#[test_case(FreeProvider::Voyager)]
#[test_case(FreeProvider::Blast)]
#[tokio::test]
async fn test_mainnet_url_happy_case(free_provider: FreeProvider) {
let provider = get_provider(&Network::free_sepolia_rpc(&free_provider)).unwrap();
let spec_version = provider.spec_version().await.unwrap();
assert!(is_expected_version(&Version::parse(&spec_version).unwrap()));
}

#[test_case(FreeProvider::Voyager)]
#[test_case(FreeProvider::Blast)]
#[tokio::test]
async fn test_sepolia_url_happy_case(free_provider: FreeProvider) {
let provider = get_provider(&Network::free_sepolia_rpc(&free_provider)).unwrap();
let spec_version = provider.spec_version().await.unwrap();
assert!(is_expected_version(&Version::parse(&spec_version).unwrap()));
}
}
12 changes: 11 additions & 1 deletion crates/sncast/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ use starknet::{
signers::{LocalWallet, SigningKey},
};
use starknet_types_core::felt::Felt;
use std::collections::HashMap;
use std::fmt::Display;
use std::str::FromStr;
use std::thread::sleep;
use std::time::Duration;
use std::{collections::HashMap, fmt::Display};
use std::{env, fs};
use thiserror::Error;

Expand Down Expand Up @@ -84,6 +85,15 @@ pub enum Network {
Sepolia,
}

impl Display for Network {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Network::Mainnet => write!(f, "mainnet"),
Network::Sepolia => write!(f, "sepolia"),
}
}
}

impl TryFrom<Felt> for Network {
type Error = anyhow::Error;

Expand Down
1 change: 0 additions & 1 deletion crates/sncast/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,6 @@ async fn run_async_command(
config.account.clone()
};
let result = starknet_commands::account::create::create(
create.rpc.get_url(&config),
&account,
&config.accounts_file,
config.keystore,
Expand Down
67 changes: 50 additions & 17 deletions crates/sncast/src/starknet_commands/account/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use sncast::helpers::rpc::RpcArgs;
use sncast::response::structs::AccountCreateResponse;
use sncast::{
check_class_hash_exists, check_if_legacy_contract, extract_or_generate_salt, get_chain_id,
get_keystore_password, handle_account_factory_error,
get_keystore_password, handle_account_factory_error, Network,
};
use starknet::accounts::{
AccountDeploymentV1, AccountFactory, ArgentAccountFactory, OpenZeppelinAccountFactory,
Expand All @@ -44,7 +44,7 @@ pub struct Create {
pub salt: Option<Felt>,

/// If passed, a profile with provided name and corresponding data will be created in snfoundry.toml
#[clap(long)]
#[clap(long, conflicts_with = "network")]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding profile should be possible if the account is created using network alias

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The profile stores the provider url in the Scarb.toml. If we allow this, we'll be adding URL of the free providers to users' Scarb.toml files which I though is not ideal.

pub add_profile: Option<String>,

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

#[allow(clippy::too_many_arguments)]
pub async fn create(
rpc_url: String,
account: &str,
accounts_file: &Utf8PathBuf,
keystore: Option<Utf8PathBuf>,
Expand Down Expand Up @@ -112,24 +111,38 @@ pub async fn create(
legacy,
)?;

let deploy_command = generate_deploy_command_with_keystore(account, &keystore, &rpc_url);
let deploy_command = generate_deploy_command_with_keystore(
account,
&keystore,
create.rpc.url.as_deref(),
create.rpc.network.as_ref(),
);
message.push_str(&deploy_command);
} else {
write_account_to_accounts_file(account, accounts_file, chain_id, account_json.clone())?;

let deploy_command = generate_deploy_command(accounts_file, &rpc_url, account);
let deploy_command = generate_deploy_command(
accounts_file,
create.rpc.url.as_deref(),
create.rpc.network.as_ref(),
account,
);
message.push_str(&deploy_command);
}

if add_profile.is_some() {
let config = CastConfig {
url: rpc_url,
account: account.into(),
accounts_file: accounts_file.into(),
keystore,
..Default::default()
};
add_created_profile_to_configuration(create.add_profile.as_deref(), &config, None)?;
if let Some(url) = &create.rpc.url {
let config = CastConfig {
url: url.clone(),
account: account.into(),
accounts_file: accounts_file.into(),
keystore,
..Default::default()
};
add_created_profile_to_configuration(create.add_profile.as_deref(), &config, None)?;
} else {
unreachable!("Conflicting arguments should be handled in clap");
}
}

Ok(AccountCreateResponse {
Expand Down Expand Up @@ -328,7 +341,22 @@ fn write_account_to_file(
Ok(())
}

fn generate_deploy_command(accounts_file: &Utf8PathBuf, rpc_url: &str, account: &str) -> String {
fn generate_network_flag(rpc_url: Option<&str>, network: Option<&Network>) -> String {
if let Some(rpc_url) = rpc_url {
format!("--url {rpc_url}")
} else if let Some(network) = network {
format!("--network {network}")
} else {
unreachable!("Either `--rpc_url` or `--network` must be provided.")
}
}

fn generate_deploy_command(
accounts_file: &Utf8PathBuf,
rpc_url: Option<&str>,
network: Option<&Network>,
account: &str,
) -> String {
let accounts_flag = if accounts_file
.to_string()
.contains("starknet_accounts/starknet_open_zeppelin_accounts.json")
Expand All @@ -338,19 +366,24 @@ fn generate_deploy_command(accounts_file: &Utf8PathBuf, rpc_url: &str, account:
format!(" --accounts-file {accounts_file}")
};

let network_flag = generate_network_flag(rpc_url, network);

format!(
"\n\nAfter prefunding the address, run:\n\
sncast{accounts_flag} account deploy --url {rpc_url} --name {account} --fee-token strk"
sncast{accounts_flag} account deploy {network_flag} --name {account} --fee-token strk"
)
}

fn generate_deploy_command_with_keystore(
account: &str,
keystore: &Utf8PathBuf,
rpc_url: &str,
rpc_url: Option<&str>,
network: Option<&Network>,
) -> String {
let network_flag = generate_network_flag(rpc_url, network);

format!(
"\n\nAfter prefunding the address, run:\n\
sncast --account {account} --keystore {keystore} account deploy --url {rpc_url} --fee-token strk"
sncast --account {account} --keystore {keystore} account deploy {network_flag} --fee-token strk"
)
}
Loading
Loading