1use crate::opts::RpcCommonOpts;
4use alloy_primitives::{Address, B256, U256};
5use clap::Parser;
6use foundry_common::shell;
7use foundry_config::{
8 Chain, Config, FoundryHardfork,
9 figment::{
10 self, Metadata, Profile, Provider,
11 error::Kind::InvalidType,
12 value::{Dict, Map, Value},
13 },
14};
15use foundry_evm_networks::NetworkConfigs;
16use serde::Serialize;
17
18#[derive(Clone, Debug, Default, Serialize, Parser)]
41#[command(next_help_heading = "EVM options", about = None, long_about = None)] pub struct EvmArgs {
43 #[command(flatten)]
45 #[serde(flatten)]
46 pub rpc: RpcCommonOpts,
47
48 #[arg(long, requires = "rpc_url", value_name = "BLOCK")]
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub fork_block_number: Option<u64>,
54
55 #[arg(long, requires = "rpc_url", value_name = "RETRIES")]
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub fork_retries: Option<u32>,
61
62 #[arg(long, requires = "rpc_url", value_name = "BACKOFF")]
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub fork_retry_backoff: Option<u64>,
68
69 #[arg(long)]
77 #[serde(skip)]
78 pub no_storage_caching: bool,
79
80 #[arg(long, value_name = "BALANCE")]
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub initial_balance: Option<U256>,
84
85 #[arg(long, value_name = "ADDRESS")]
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub sender: Option<Address>,
89
90 #[arg(long)]
92 #[serde(skip)]
93 pub ffi: bool,
94
95 #[arg(long)]
97 #[serde(skip)]
98 pub live_logs: bool,
99
100 #[arg(long)]
102 #[serde(skip)]
103 pub always_use_create_2_factory: bool,
104
105 #[arg(long, value_name = "ADDRESS")]
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub create2_deployer: Option<Address>,
109
110 #[command(flatten)]
112 #[serde(flatten)]
113 pub env: EnvArgs,
114
115 #[arg(long)]
119 #[serde(skip)]
120 pub isolate: bool,
121
122 #[arg(long, conflicts_with = "isolate")]
124 #[serde(skip)]
125 pub no_isolate: bool,
126
127 #[arg(long, value_name = "HARDFORK")]
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub hardfork: Option<FoundryHardfork>,
133
134 #[command(flatten)]
136 #[serde(skip)]
137 pub networks: NetworkConfigs,
138}
139
140impl Provider for EvmArgs {
142 fn metadata(&self) -> Metadata {
143 Metadata::named("Evm Opts Provider")
144 }
145
146 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
147 let value = Value::serialize(self)?;
148 let error = InvalidType(value.to_actual(), "map".into());
149 let mut dict = value.into_dict().ok_or(error)?;
150
151 if shell::verbosity() > 0 {
152 let verbosity = shell::verbosity();
154 dict.insert("verbosity".to_string(), verbosity.into());
155 }
156
157 if self.ffi {
158 dict.insert("ffi".to_string(), self.ffi.into());
159 }
160
161 if self.live_logs {
162 dict.insert("live_logs".to_string(), self.live_logs.into());
163 }
164
165 if self.no_isolate {
166 dict.insert("isolate".to_string(), false.into());
167 } else if self.isolate {
168 dict.insert("isolate".to_string(), self.isolate.into());
169 }
170
171 if self.always_use_create_2_factory {
172 dict.insert(
173 "always_use_create_2_factory".to_string(),
174 self.always_use_create_2_factory.into(),
175 );
176 }
177
178 if self.no_storage_caching {
179 dict.insert("no_storage_caching".to_string(), self.no_storage_caching.into());
180 }
181
182 if self.rpc.no_rpc_rate_limit {
184 dict.insert("no_rpc_rate_limit".to_string(), true.into());
185 }
186 if self.rpc.accept_invalid_certs {
187 dict.insert("eth_rpc_accept_invalid_certs".to_string(), true.into());
188 }
189 if self.rpc.no_proxy {
190 dict.insert("eth_rpc_no_proxy".to_string(), true.into());
191 }
192
193 if let Some(network) = self.networks.resolved_network() {
196 dict.insert("network".to_string(), network.name().into());
197 }
198 if self.networks.is_celo() {
199 dict.insert("celo".to_string(), true.into());
200 }
201
202 Ok(Map::from([(Config::selected_profile(), dict)]))
203 }
204}
205
206#[derive(Clone, Debug, Default, Serialize, Parser)]
208#[command(next_help_heading = "Executor environment config")]
209pub struct EnvArgs {
210 #[arg(long, value_name = "CODE_SIZE")]
213 #[serde(skip_serializing_if = "Option::is_none")]
214 pub code_size_limit: Option<usize>,
215
216 #[arg(long, visible_alias = "chain-id", value_name = "CHAIN")]
218 #[serde(rename = "chain_id", skip_serializing_if = "Option::is_none", serialize_with = "id")]
219 pub chain: Option<Chain>,
220
221 #[arg(long, value_name = "GAS_PRICE")]
223 #[serde(skip_serializing_if = "Option::is_none")]
224 pub gas_price: Option<u64>,
225
226 #[arg(long, visible_alias = "base-fee", value_name = "FEE")]
228 #[serde(skip_serializing_if = "Option::is_none")]
229 pub block_base_fee_per_gas: Option<u64>,
230
231 #[arg(long, value_name = "ADDRESS")]
233 #[serde(skip_serializing_if = "Option::is_none")]
234 pub tx_origin: Option<Address>,
235
236 #[arg(long, value_name = "ADDRESS")]
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub block_coinbase: Option<Address>,
240
241 #[arg(long, value_name = "TIMESTAMP")]
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub block_timestamp: Option<u64>,
245
246 #[arg(long, value_name = "BLOCK")]
248 #[serde(skip_serializing_if = "Option::is_none")]
249 pub block_number: Option<u64>,
250
251 #[arg(long, value_name = "DIFFICULTY")]
253 #[serde(skip_serializing_if = "Option::is_none")]
254 pub block_difficulty: Option<u64>,
255
256 #[arg(long, value_name = "PREVRANDAO")]
258 #[serde(skip_serializing_if = "Option::is_none")]
259 pub block_prevrandao: Option<B256>,
260
261 #[arg(long, visible_alias = "gas-limit", value_name = "BLOCK_GAS_LIMIT")]
263 #[serde(skip_serializing_if = "Option::is_none")]
264 pub block_gas_limit: Option<u64>,
265
266 #[arg(long, value_name = "MEMORY_LIMIT")]
271 #[serde(skip_serializing_if = "Option::is_none")]
272 pub memory_limit: Option<u64>,
273
274 #[arg(long, visible_aliases = &["no-block-gas-limit", "no-gas-limit"])]
276 #[serde(skip_serializing_if = "std::ops::Not::not")]
277 pub disable_block_gas_limit: bool,
278
279 #[arg(long, visible_alias = "tx-gas-limit")]
281 #[serde(skip_serializing_if = "std::ops::Not::not")]
282 pub enable_tx_gas_limit: bool,
283}
284
285fn id<S: serde::Serializer>(chain: &Option<Chain>, s: S) -> Result<S::Ok, S::Error> {
288 if let Some(chain) = chain {
289 s.serialize_u64(chain.id())
290 } else {
291 unreachable!()
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use foundry_config::NamedChain;
300
301 #[test]
302 fn compute_units_per_second_skips_when_none() {
303 let args = EvmArgs::default();
304 let data = args.data().expect("provider data");
305 let dict = data.get(&Config::selected_profile()).expect("profile dict");
306 assert!(
307 !dict.contains_key("compute_units_per_second"),
308 "compute_units_per_second should be skipped when None"
309 );
310 }
311
312 #[test]
313 fn compute_units_per_second_present_when_some() {
314 let args = EvmArgs {
315 rpc: RpcCommonOpts { compute_units_per_second: Some(1000), ..Default::default() },
316 ..Default::default()
317 };
318 let data = args.data().expect("provider data");
319 let dict = data.get(&Config::selected_profile()).expect("profile dict");
320 let val = dict.get("compute_units_per_second").expect("cups present");
321 assert_eq!(val, &Value::from(1000u64));
322 }
323
324 #[test]
325 fn celo_network_is_included_in_provider_data() {
326 let args = EvmArgs { networks: NetworkConfigs::with_celo(), ..Default::default() };
327 let data = args.data().expect("provider data");
328 let dict = data.get(&Config::selected_profile()).expect("profile dict");
329
330 assert_eq!(dict.get("celo"), Some(&Value::from(true)));
331 assert!(!dict.contains_key("network"));
332 }
333
334 #[test]
335 fn explicit_ethereum_network_is_included_in_provider_data() {
336 let args = EvmArgs { networks: NetworkConfigs::with_ethereum(), ..Default::default() };
337 let data = args.data().expect("provider data");
338 let dict = data.get(&Config::selected_profile()).expect("profile dict");
339
340 assert_eq!(dict.get("network"), Some(&Value::from("ethereum")));
341 assert!(!dict.contains_key("celo"));
342 }
343
344 #[test]
345 fn rpc_url_arg_does_not_read_eth_rpc_url_env() {
346 use clap::CommandFactory;
347
348 let command = EvmArgs::command();
349 let rpc_url =
350 command.get_arguments().find(|arg| arg.get_id() == "rpc_url").expect("rpc_url arg");
351
352 assert!(rpc_url.get_env().is_none());
353 }
354
355 #[test]
356 fn can_parse_chain_id() {
357 let args = EvmArgs {
358 env: EnvArgs { chain: Some(NamedChain::Mainnet.into()), ..Default::default() },
359 ..Default::default()
360 };
361 let config = Config::from_provider(Config::figment().merge(args)).unwrap();
362 assert_eq!(config.chain, Some(NamedChain::Mainnet.into()));
363
364 let env = EnvArgs::parse_from(["foundry-cli", "--chain-id", "goerli"]);
365 assert_eq!(env.chain, Some(NamedChain::Goerli.into()));
366 }
367
368 #[cfg(feature = "base")]
369 #[test]
370 fn can_parse_namespaced_base_hardfork() {
371 let args = EvmArgs::parse_from(["foundry-cli", "--hardfork", "base:Beryl"]);
372 assert_eq!(args.hardfork.map(String::from).as_deref(), Some("base:Beryl"));
373
374 let config = Config::from_provider(Config::figment().merge(args)).unwrap();
375 assert!(config.networks.is_base());
376 assert_eq!(config.hardfork.map(String::from).as_deref(), Some("base:Beryl"));
377 }
378
379 #[test]
380 fn hardfork_arg_selects_network() {
381 let args = EvmArgs::parse_from(["foundry-cli", "--hardfork", "tempo:T5"]);
382 let hardfork = "tempo:T5".parse::<FoundryHardfork>().unwrap();
383 assert_eq!(args.hardfork, Some(hardfork));
384
385 let config = Config::from_provider(Config::figment().merge(args)).unwrap();
386 assert_eq!(config.hardfork, Some(hardfork));
387 assert!(config.networks.is_tempo());
388 }
389
390 #[test]
391 fn test_memory_limit() {
392 let args = EvmArgs {
393 env: EnvArgs { chain: Some(NamedChain::Mainnet.into()), ..Default::default() },
394 ..Default::default()
395 };
396 let config = Config::from_provider(Config::figment().merge(args)).unwrap();
397 assert_eq!(config.memory_limit, Config::default().memory_limit);
398
399 let env = EnvArgs::parse_from(["foundry-cli", "--memory-limit", "100"]);
400 assert_eq!(env.memory_limit, Some(100));
401 }
402
403 #[test]
404 fn test_chain_id() {
405 let env = EnvArgs::parse_from(["foundry-cli", "--chain-id", "1"]);
406 assert_eq!(env.chain, Some(Chain::mainnet()));
407
408 let env = EnvArgs::parse_from(["foundry-cli", "--chain-id", "mainnet"]);
409 assert_eq!(env.chain, Some(Chain::mainnet()));
410 let args = EvmArgs { env, ..Default::default() };
411 let config = Config::from_provider(Config::figment().merge(args)).unwrap();
412 assert_eq!(config.chain, Some(Chain::mainnet()));
413 }
414}