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 11 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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ 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`
with provider command
cptartur marked this conversation as resolved.
Show resolved Hide resolved

## [0.36.0] - 2025-01-15

### Forge
Expand Down
2 changes: 2 additions & 0 deletions crates/docs/src/snippet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ pub struct SnippetConfig {
pub package_name: Option<String>,
#[serde(default)]
pub ignored_output: bool,
#[serde(default)]
pub not_replace_network: bool,
franciszekjob marked this conversation as resolved.
Show resolved Hide resolved
}

#[derive(Debug)]
Expand Down
126 changes: 118 additions & 8 deletions crates/sncast/src/helpers/rpc.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,49 @@
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::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 using public provider. When using this option you may experience rate limits and other unexpected behavior
#[clap(long)]
cptartur marked this conversation as resolved.
Show resolved Hide resolved
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 +53,88 @@ 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 => {
"https://starknet-mainnet.public.blastapi.io/rpc/v0_7".to_string()
}
franciszekjob marked this conversation as resolved.
Show resolved Hide resolved
FreeProvider::Voyager => "https://free-rpc.nethermind.io/mainnet-juno".to_string(),
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Probably version should be added here as well

Copy link
Member Author

@cptartur cptartur Jan 17, 2025

Choose a reason for hiding this comment

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

}

fn free_sepolia_rpc(provider: &FreeProvider) -> String {
match provider {
FreeProvider::Blast => {
"https://starknet-sepolia.public.blastapi.io/rpc/v0_7".to_string()
}
cptartur marked this conversation as resolved.
Show resolved Hide resolved
FreeProvider::Voyager => "https://free-rpc.nethermind.io/sepolia-juno".to_string(),
}
cptartur marked this conversation as resolved.
Show resolved Hide resolved
}
}

#[cfg(test)]
mod tests {
use super::*;
use reqwest::Response;
use test_case::test_case;

async fn call_provider(url: &str) -> Result<Response> {
let client = reqwest::Client::new();
client
.get(url)
.send()
.await
.context("Failed to send request")
}

#[test_case(FreeProvider::Voyager)]
#[test_case(FreeProvider::Blast)]
#[tokio::test]
async fn test_mainnet_url_works(free_provider: FreeProvider) {
assert!(call_provider(&Network::free_mainnet_rpc(&free_provider))
cptartur marked this conversation as resolved.
Show resolved Hide resolved
.await
.is_ok());
}

#[test_case(FreeProvider::Voyager)]
#[test_case(FreeProvider::Blast)]
#[tokio::test]
async fn test_sepolia_url_works(free_provider: FreeProvider) {
assert!(call_provider(&Network::free_sepolia_rpc(&free_provider))
cptartur marked this conversation as resolved.
Show resolved Hide resolved
.await
.is_ok());
}
}
13 changes: 12 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,16 @@ pub enum Network {
Sepolia,
}

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

cptartur marked this conversation as resolved.
Show resolved Hide resolved
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}",)
cptartur marked this conversation as resolved.
Show resolved Hide resolved
} else {
unreachable!()
cptartur marked this conversation as resolved.
Show resolved Hide resolved
}
}

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"
)
}
25 changes: 11 additions & 14 deletions crates/sncast/src/starknet_commands/account/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,24 @@ use sncast::{chain_id_to_network_name, get_chain_id};
#[derive(Args, Debug)]
#[command(about = "Delete account information from the accounts file")]
#[command(group(ArgGroup::new("networks")
.args(&["url", "network"])
.args(&["url", "network", "network_name"])
.required(true)
.multiple(false)))]
pub struct Delete {
/// Name of the account to be deleted
#[clap(short, long)]
pub name: String,

/// Network where the account exists; defaults to network of rpc node
#[clap(long)]
pub network: Option<String>,

/// Assume "yes" as answer to confirmation prompt and run non-interactively
#[clap(long, default_value = "false")]
pub yes: bool,

#[clap(flatten)]
pub rpc: Option<RpcArgs>,
pub rpc: RpcArgs,

/// Literal name of the network used in accounts file
#[clap(long)]
pub network_name: Option<String>,
}

#[allow(clippy::too_many_arguments)]
Expand Down Expand Up @@ -79,13 +79,10 @@ pub fn delete(
}

pub(crate) async fn get_network_name(delete: &Delete, config: &CastConfig) -> Result<String> {
match (&delete.rpc, &delete.network) {
(Some(rpc), None) => {
let provider = rpc.get_provider(config).await?;
let network_name = chain_id_to_network_name(get_chain_id(&provider).await?);
Ok(network_name)
}
(None, Some(network)) => Ok(network.clone()),
_ => unreachable!("Checked on clap level"),
if let Some(network_name) = &delete.network_name {
return Ok(network_name.clone());
}

let provider = delete.rpc.get_provider(config).await?;
Ok(chain_id_to_network_name(get_chain_id(&provider).await?))
}
Loading
Loading