Skip to content

Commit 16f5708

Browse files
authored
Configure --network providers in snfoundry.toml (#3874)
<!-- Reference any GitHub issues resolved by this PR --> Closes #3765 Stack: - #3874 ⬅ - #3875 ## Introduced changes <!-- A brief description of the changes --> - Add possibility to configure used providers via `--network` flag in `snfoundry.toml` ## 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`
1 parent 71f5224 commit 16f5708

File tree

6 files changed

+169
-17
lines changed

6 files changed

+169
-17
lines changed

crates/sncast/src/helpers/config.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ fn build_default_manifest() -> String {
5050
# accounts-file = "{default_accounts_file}"
5151
# account = ""
5252
# keystore = ""
53+
54+
# Configure custom network addresses
55+
# [sncast.default.networks]
56+
# mainnet = "https://mainnet.your-node.com"
57+
# sepolia = "https://sepolia.your-node.com"
58+
# devnet = "http://127.0.0.1:5050/rpc"
5359
"#,
5460
default_accounts_file = DEFAULT_ACCOUNTS_FILE,
5561
default_wait_timeout = default_wait_params.timeout,
@@ -79,6 +85,9 @@ macro_rules! clone_field {
7985
pub fn combine_cast_configs(global_config: &CastConfig, local_config: &CastConfig) -> CastConfig {
8086
let default_cast_config = CastConfig::default();
8187

88+
let mut networks = global_config.networks.clone();
89+
networks.override_with(&local_config.networks);
90+
8291
CastConfig {
8392
url: clone_field!(global_config, local_config, default_cast_config, url),
8493
account: clone_field!(global_config, local_config, default_cast_config, account),
@@ -107,5 +116,6 @@ pub fn combine_cast_configs(global_config: &CastConfig, local_config: &CastConfi
107116
default_cast_config,
108117
show_explorer_links
109118
),
119+
networks,
110120
}
111121
}

crates/sncast/src/helpers/configuration.rs

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::block_explorer;
2-
use crate::ValidatedWaitParams;
2+
use crate::{Network, ValidatedWaitParams};
33
use anyhow::Result;
44
use camino::Utf8PathBuf;
55
use configuration::Config;
@@ -10,6 +10,37 @@ pub const fn show_explorer_links_default() -> bool {
1010
true
1111
}
1212

13+
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Default)]
14+
#[serde(deny_unknown_fields)]
15+
pub struct NetworksConfig {
16+
pub mainnet: Option<String>,
17+
pub sepolia: Option<String>,
18+
pub devnet: Option<String>,
19+
}
20+
21+
impl NetworksConfig {
22+
#[must_use]
23+
pub fn get_url(&self, network: Network) -> Option<&String> {
24+
match network {
25+
Network::Mainnet => self.mainnet.as_ref(),
26+
Network::Sepolia => self.sepolia.as_ref(),
27+
Network::Devnet => self.devnet.as_ref(),
28+
}
29+
}
30+
31+
pub fn override_with(&mut self, other: &NetworksConfig) {
32+
if other.mainnet.is_some() {
33+
self.mainnet.clone_from(&other.mainnet);
34+
}
35+
if other.sepolia.is_some() {
36+
self.sepolia.clone_from(&other.sepolia);
37+
}
38+
if other.devnet.is_some() {
39+
self.devnet.clone_from(&other.devnet);
40+
}
41+
}
42+
}
43+
1344
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
1445
pub struct CastConfig {
1546
#[serde(default)]
@@ -46,6 +77,10 @@ pub struct CastConfig {
4677
)]
4778
/// Print links pointing to pages with transaction details in the chosen block explorer
4879
pub show_explorer_links: bool,
80+
81+
#[serde(default)]
82+
/// Configurable urls of predefined networks - mainnet, sepolia, and devnet are supported
83+
pub networks: NetworksConfig,
4984
}
5085

5186
impl Default for CastConfig {
@@ -58,6 +93,7 @@ impl Default for CastConfig {
5893
wait_params: ValidatedWaitParams::default(),
5994
block_explorer: Some(block_explorer::Service::default()),
6095
show_explorer_links: show_explorer_links_default(),
96+
networks: NetworksConfig::default(),
6197
}
6298
}
6399
}
@@ -71,3 +107,71 @@ impl Config for CastConfig {
71107
Ok(serde_json::from_value::<CastConfig>(config)?)
72108
}
73109
}
110+
111+
#[cfg(test)]
112+
mod tests {
113+
use super::*;
114+
115+
#[test]
116+
fn test_networks_config_get() {
117+
let networks = NetworksConfig {
118+
mainnet: Some("https://mainnet.example.com".to_string()),
119+
sepolia: Some("https://sepolia.example.com".to_string()),
120+
devnet: Some("https://devnet.example.com".to_string()),
121+
};
122+
123+
assert_eq!(
124+
networks.get_url(Network::Mainnet),
125+
Some(&"https://mainnet.example.com".to_string())
126+
);
127+
assert_eq!(
128+
networks.get_url(Network::Sepolia),
129+
Some(&"https://sepolia.example.com".to_string())
130+
);
131+
assert_eq!(
132+
networks.get_url(Network::Devnet),
133+
Some(&"https://devnet.example.com".to_string())
134+
);
135+
}
136+
137+
#[test]
138+
fn test_networks_config_override() {
139+
let mut global = NetworksConfig {
140+
mainnet: Some("https://global-mainnet.example.com".to_string()),
141+
sepolia: Some("https://global-sepolia.example.com".to_string()),
142+
devnet: None,
143+
};
144+
let local = NetworksConfig {
145+
mainnet: Some("https://local-mainnet.example.com".to_string()),
146+
sepolia: None,
147+
devnet: None,
148+
};
149+
150+
global.override_with(&local);
151+
152+
// Local mainnet should override global
153+
assert_eq!(
154+
global.mainnet,
155+
Some("https://local-mainnet.example.com".to_string())
156+
);
157+
// Global sepolia should remain
158+
assert_eq!(
159+
global.sepolia,
160+
Some("https://global-sepolia.example.com".to_string())
161+
);
162+
}
163+
164+
#[test]
165+
fn test_networks_config_rejects_unknown_fields_and_typos() {
166+
// Unknown fields should cause an error
167+
let toml_str = r#"
168+
mainnet = "https://mainnet.example.com"
169+
custom = "https://custom.example.com"
170+
wrong_key = "https://sepolia.example.com"
171+
"#;
172+
173+
let result: Result<NetworksConfig, _> = toml::from_str(toml_str);
174+
assert!(result.is_err());
175+
assert!(result.unwrap_err().to_string().contains("unknown field"));
176+
}
177+
}

crates/sncast/src/helpers/rpc.rs

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ impl RpcArgs {
3636
)
3737
}
3838

39-
let url = self.get_url(&config.url).await?;
39+
let url = self.get_url(config).await?;
4040

4141
assert!(!url.is_empty(), "url cannot be empty");
4242
let provider = get_provider(&url)?;
@@ -46,15 +46,20 @@ impl RpcArgs {
4646
Ok(provider)
4747
}
4848

49-
pub async fn get_url(&self, config_url: &str) -> Result<String> {
50-
match (&self.network, &self.url, config_url) {
51-
(Some(network), None, _) => {
52-
let free_provider = FreeProvider::semi_random();
53-
network.url(&free_provider).await
54-
}
49+
pub async fn get_url(&self, config: &CastConfig) -> Result<String> {
50+
match (&self.network, &self.url, &config.url) {
51+
(Some(network), None, _) => self.resolve_network_url(network, config).await,
5552
(None, Some(url), _) => Ok(url.clone()),
56-
(None, None, config_url) if !config_url.is_empty() => Ok(config_url.to_string()),
57-
_ => bail!("Either `--network` or `--url` must be provided"),
53+
(None, None, url) if !url.is_empty() => Ok(url.clone()),
54+
_ => bail!("Either `--network` or `--url` must be provided."),
55+
}
56+
}
57+
58+
async fn resolve_network_url(&self, network: &Network, config: &CastConfig) -> Result<String> {
59+
if let Some(custom_url) = config.networks.get_url(*network) {
60+
Ok(custom_url.clone())
61+
} else {
62+
network.url(&FreeProvider::semi_random()).await
5863
}
5964
}
6065
}
@@ -147,4 +152,37 @@ mod tests {
147152
let spec_version = provider.spec_version().await.unwrap();
148153
assert!(is_expected_version(&Version::parse(&spec_version).unwrap()));
149154
}
155+
156+
#[tokio::test]
157+
async fn test_custom_network_url_from_config() {
158+
let mut config = CastConfig::default();
159+
config.networks.mainnet =
160+
Some("https://starknet-mainnet.infura.io/v3/custom-api-key".to_string());
161+
config.networks.sepolia =
162+
Some("https://starknet-sepolia.g.alchemy.com/v2/custom-api-key".to_string());
163+
164+
let rpc_args = RpcArgs {
165+
url: None,
166+
network: Some(Network::Mainnet),
167+
};
168+
169+
let url = rpc_args.get_url(&config).await.unwrap();
170+
assert_eq!(
171+
url,
172+
"https://starknet-mainnet.infura.io/v3/custom-api-key".to_string()
173+
);
174+
}
175+
176+
#[tokio::test]
177+
async fn test_fallback_to_default_network_url() {
178+
let config = CastConfig::default();
179+
180+
let rpc_args = RpcArgs {
181+
url: None,
182+
network: Some(Network::Mainnet),
183+
};
184+
185+
let url = rpc_args.get_url(&config).await.unwrap();
186+
assert_eq!(url, Network::free_mainnet_rpc(&FreeProvider::Alchemy));
187+
}
150188
}

crates/sncast/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ pub async fn get_account<'a>(
287287
}
288288
(true, false) => {
289289
let url = rpc_args
290-
.get_url(&config.url)
290+
.get_url(config)
291291
.await
292292
.context("Failed to get url")?;
293293
return get_account_from_devnet(account, provider, &url).await;

crates/sncast/src/main.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ async fn run_async_command(cli: Cli, config: CastConfig, ui: &UI) -> Result<()>
335335
serde_json::from_str(&contract_artifacts.sierra)
336336
.context("Failed to parse sierra artifact")?;
337337
let network_flag = generate_network_flag(
338-
rpc.get_url(&config.url).await.ok().as_deref(),
338+
rpc.get_url(&config).await.ok().as_deref(),
339339
rpc.network.as_ref(),
340340
);
341341
Some(DeployCommandMessage::new(
@@ -742,11 +742,11 @@ fn get_cast_config(cli: &Cli, ui: &UI) -> Result<CastConfig> {
742742

743743
let global_config =
744744
load_config::<CastConfig>(Some(&global_config_path.clone()), cli.profile.as_deref())
745-
.unwrap_or_else(|_| {
746-
load_config::<CastConfig>(Some(&global_config_path), None).unwrap()
747-
});
745+
.or_else(|_| load_config::<CastConfig>(Some(&global_config_path), None))
746+
.map_err(|err| anyhow::anyhow!(format!("Failed to load config: {err}")))?;
748747

749-
let local_config = load_config::<CastConfig>(None, cli.profile.as_deref())?;
748+
let local_config = load_config::<CastConfig>(None, cli.profile.as_deref())
749+
.map_err(|err| anyhow::anyhow!(format!("Failed to load config: {err}")))?;
750750

751751
let mut combined_config = combine_cast_configs(&global_config, &local_config);
752752

crates/sncast/src/starknet_commands/script/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ pub fn run_script_command(
7979
)))
8080
};
8181
let url = runtime
82-
.block_on(run.rpc.get_url(&config.url))
82+
.block_on(run.rpc.get_url(&config))
8383
.context("Failed to get url")?;
8484
let result = starknet_commands::script::run::run(
8585
&run.script_name,

0 commit comments

Comments
 (0)