1#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg))]
7
8#[macro_use]
9extern crate foundry_common;
10
11#[macro_use]
12extern crate tracing;
13
14use crate::runner::ScriptRunner;
15use alloy_json_abi::{Function, JsonAbi};
16use alloy_primitives::{
17 Address, Bytes, Log, TxKind, U256, hex,
18 map::{AddressHashMap, HashMap},
19};
20use alloy_signer::Signer;
21use broadcast::next_nonce;
22use build::PreprocessedState;
23use clap::{Parser, ValueHint};
24use dialoguer::Confirm;
25use eyre::{ContextCompat, Result};
26use forge_script_sequence::{AdditionalContract, NestedValue};
27use forge_verify::{RetryArgs, VerifierArgs};
28use foundry_block_explorers::EtherscanApiVersion;
29use foundry_cli::{
30 opts::{BuildOpts, GlobalArgs},
31 utils::{self, LoadConfig},
32};
33use foundry_common::{
34 CONTRACT_MAX_SIZE, ContractsByArtifact, SELECTOR_LEN,
35 abi::{encode_function_args, get_func},
36 evm::{Breakpoints, EvmArgs},
37 shell,
38};
39use foundry_compilers::ArtifactId;
40use foundry_config::{
41 Config, figment,
42 figment::{
43 Metadata, Profile, Provider,
44 value::{Dict, Map},
45 },
46};
47use foundry_evm::{
48 backend::Backend,
49 executors::ExecutorBuilder,
50 inspectors::{
51 CheatsConfig,
52 cheatcodes::{BroadcastableTransactions, Wallets},
53 },
54 opts::EvmOpts,
55 traces::{TraceMode, Traces},
56};
57use foundry_wallets::MultiWalletOpts;
58use serde::Serialize;
59use std::path::PathBuf;
60
61mod broadcast;
62mod build;
63mod execute;
64mod multi_sequence;
65mod progress;
66mod providers;
67mod receipts;
68mod runner;
69mod sequence;
70mod simulate;
71mod transaction;
72mod verify;
73
74foundry_config::merge_impl_figment_convert!(ScriptArgs, build, evm);
76
77#[derive(Clone, Debug, Default, Parser)]
79pub struct ScriptArgs {
80 #[command(flatten)]
82 pub global: GlobalArgs,
83
84 #[arg(value_hint = ValueHint::FilePath)]
89 pub path: String,
90
91 pub args: Vec<String>,
93
94 #[arg(long, visible_alias = "tc", value_name = "CONTRACT_NAME")]
96 pub target_contract: Option<String>,
97
98 #[arg(long, short, default_value = "run()")]
100 pub sig: String,
101
102 #[arg(
104 long,
105 env = "ETH_PRIORITY_GAS_PRICE",
106 value_parser = foundry_cli::utils::parse_ether_value,
107 value_name = "PRICE"
108 )]
109 pub priority_gas_price: Option<U256>,
110
111 #[arg(long)]
115 pub legacy: bool,
116
117 #[arg(long)]
119 pub broadcast: bool,
120
121 #[arg(long, default_value = "100")]
125 pub batch_size: usize,
126
127 #[arg(long)]
129 pub skip_simulation: bool,
130
131 #[arg(long, short, default_value = "130")]
133 pub gas_estimate_multiplier: u64,
134
135 #[arg(
137 long,
138 conflicts_with_all = &["private_key", "private_keys", "froms", "ledger", "trezor", "aws"],
139 )]
140 pub unlocked: bool,
141
142 #[arg(long)]
149 pub resume: bool,
150
151 #[arg(long)]
153 pub multi: bool,
154
155 #[arg(long)]
159 pub debug: bool,
160
161 #[arg(
163 long,
164 requires = "debug",
165 value_hint = ValueHint::FilePath,
166 value_name = "PATH"
167 )]
168 pub dump: Option<PathBuf>,
169
170 #[arg(long)]
173 pub slow: bool,
174
175 #[arg(long)]
179 pub non_interactive: bool,
180
181 #[arg(long)]
183 pub disable_code_size_limit: bool,
184
185 #[arg(long, env = "ETHERSCAN_API_KEY", value_name = "KEY")]
187 pub etherscan_api_key: Option<String>,
188
189 #[arg(long, env = "ETHERSCAN_API_VERSION", value_name = "VERSION")]
191 pub etherscan_api_version: Option<EtherscanApiVersion>,
192
193 #[arg(long)]
195 pub verify: bool,
196
197 #[arg(
202 long,
203 env = "ETH_GAS_PRICE",
204 value_parser = foundry_cli::utils::parse_ether_value,
205 value_name = "PRICE",
206 )]
207 pub with_gas_price: Option<U256>,
208
209 #[arg(long, env = "ETH_TIMEOUT")]
211 pub timeout: Option<u64>,
212
213 #[command(flatten)]
214 pub build: BuildOpts,
215
216 #[command(flatten)]
217 pub wallets: MultiWalletOpts,
218
219 #[command(flatten)]
220 pub evm: EvmArgs,
221
222 #[command(flatten)]
223 pub verifier: VerifierArgs,
224
225 #[command(flatten)]
226 pub retry: RetryArgs,
227}
228
229impl ScriptArgs {
230 pub async fn preprocess(self) -> Result<PreprocessedState> {
231 let script_wallets = Wallets::new(self.wallets.get_multi_wallet().await?, self.evm.sender);
232
233 let (config, mut evm_opts) = self.load_config_and_evm_opts()?;
234
235 if let Some(sender) = self.maybe_load_private_key()? {
236 evm_opts.sender = sender;
237 }
238
239 let script_config = ScriptConfig::new(config, evm_opts).await?;
240
241 Ok(PreprocessedState { args: self, script_config, script_wallets })
242 }
243
244 pub async fn run_script(self) -> Result<()> {
246 trace!(target: "script", "executing script command");
247
248 let state = self.preprocess().await?;
249 let create2_deployer = state.script_config.evm_opts.create2_deployer;
250 let compiled = state.compile()?;
251
252 let bundled = if compiled.args.resume || (compiled.args.verify && !compiled.args.broadcast)
255 {
256 compiled.resume().await?
257 } else {
258 let pre_simulation = compiled
260 .link()
261 .await?
262 .prepare_execution()
263 .await?
264 .execute()
265 .await?
266 .prepare_simulation()
267 .await?;
268
269 if pre_simulation.args.debug {
270 return match pre_simulation.args.dump.clone() {
271 Some(path) => pre_simulation.dump_debugger(&path),
272 None => pre_simulation.run_debugger(),
273 };
274 }
275
276 if shell::is_json() {
277 pre_simulation.show_json().await?;
278 } else {
279 pre_simulation.show_traces().await?;
280 }
281
282 if pre_simulation
285 .execution_result
286 .transactions
287 .as_ref()
288 .is_none_or(|txs| txs.is_empty())
289 {
290 if pre_simulation.args.broadcast {
291 sh_warn!("No transactions to broadcast.")?;
292 }
293
294 return Ok(());
295 }
296
297 if pre_simulation.execution_artifacts.rpc_data.missing_rpc {
299 if !shell::is_json() {
300 sh_println!("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?;
301 }
302
303 return Ok(());
304 }
305
306 pre_simulation.args.check_contract_sizes(
307 &pre_simulation.execution_result,
308 &pre_simulation.build_data.known_contracts,
309 create2_deployer,
310 )?;
311
312 pre_simulation.fill_metadata().await?.bundle().await?
313 };
314
315 if !bundled.args.should_broadcast() {
317 if !shell::is_json() {
318 if shell::verbosity() >= 4 {
319 sh_println!("\n=== Transactions that will be broadcast ===\n")?;
320 bundled.sequence.show_transactions()?;
321 }
322
323 sh_println!(
324 "\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more."
325 )?;
326 }
327 return Ok(());
328 }
329
330 if bundled.args.verify {
332 bundled.verify_preflight_check()?;
333 }
334
335 let broadcasted = bundled.wait_for_pending().await?.broadcast().await?;
337
338 if broadcasted.args.verify {
339 broadcasted.verify().await?;
340 }
341
342 Ok(())
343 }
344
345 fn maybe_load_private_key(&self) -> Result<Option<Address>> {
348 let maybe_sender = self
349 .wallets
350 .private_keys()?
351 .filter(|pks| pks.len() == 1)
352 .map(|pks| pks.first().unwrap().address());
353 Ok(maybe_sender)
354 }
355
356 fn get_method_and_calldata(&self, abi: &JsonAbi) -> Result<(Function, Bytes)> {
364 if let Ok(decoded) = hex::decode(&self.sig) {
365 let selector = &decoded[..SELECTOR_LEN];
366 let func =
367 abi.functions().find(|func| selector == &func.selector()[..]).ok_or_else(|| {
368 eyre::eyre!(
369 "Function selector `{}` not found in the ABI",
370 hex::encode(selector)
371 )
372 })?;
373 return Ok((func.clone(), decoded.into()));
374 }
375
376 let func = if self.sig.contains('(') {
377 let func = get_func(&self.sig)?;
378 abi.functions()
379 .find(|&abi_func| abi_func.selector() == func.selector())
380 .wrap_err(format!("Function `{}` is not implemented in your script.", self.sig))?
381 } else {
382 let matching_functions =
383 abi.functions().filter(|func| func.name == self.sig).collect::<Vec<_>>();
384 match matching_functions.len() {
385 0 => eyre::bail!("Function `{}` not found in the ABI", self.sig),
386 1 => matching_functions[0],
387 2.. => eyre::bail!(
388 "Multiple functions with the same name `{}` found in the ABI",
389 self.sig
390 ),
391 }
392 };
393 let data = encode_function_args(func, &self.args)?;
394
395 Ok((func.clone(), data.into()))
396 }
397
398 fn check_contract_sizes(
404 &self,
405 result: &ScriptResult,
406 known_contracts: &ContractsByArtifact,
407 create2_deployer: Address,
408 ) -> Result<()> {
409 if self.disable_code_size_limit {
411 return Ok(());
412 }
413
414 let mut bytecodes: Vec<(String, &[u8], &[u8])> = vec![];
416
417 for (artifact, contract) in known_contracts.iter() {
419 let Some(bytecode) = contract.bytecode() else { continue };
420 let Some(deployed_bytecode) = contract.deployed_bytecode() else { continue };
421 bytecodes.push((artifact.name.clone(), bytecode, deployed_bytecode));
422 }
423
424 let create_nodes = result.traces.iter().flat_map(|(_, traces)| {
426 traces.nodes().iter().filter(|node| node.trace.kind.is_any_create())
427 });
428 let mut unknown_c = 0usize;
429 for node in create_nodes {
430 let init_code = &node.trace.data;
431 let deployed_code = &node.trace.output;
432 if !bytecodes.iter().any(|(_, b, _)| *b == init_code.as_ref()) {
433 bytecodes.push((format!("Unknown{unknown_c}"), init_code, deployed_code));
434 unknown_c += 1;
435 }
436 continue;
437 }
438
439 let mut prompt_user = false;
440 let max_size = match self.evm.env.code_size_limit {
441 Some(size) => size,
442 None => CONTRACT_MAX_SIZE,
443 };
444
445 for (data, to) in result.transactions.iter().flat_map(|txes| {
446 txes.iter().filter_map(|tx| {
447 tx.transaction
448 .input()
449 .filter(|data| data.len() > max_size)
450 .map(|data| (data, tx.transaction.to()))
451 })
452 }) {
453 let mut offset = 0;
454
455 if let Some(TxKind::Call(to)) = to {
457 if to == create2_deployer {
458 offset = 32;
460 } else {
461 continue;
462 }
463 } else if let Some(TxKind::Create) = to {
464 }
466
467 if let Some((name, _, deployed_code)) =
469 bytecodes.iter().find(|(_, init_code, _)| *init_code == &data[offset..])
470 {
471 let deployment_size = deployed_code.len();
472
473 if deployment_size > max_size {
474 prompt_user = self.should_broadcast();
475 sh_err!(
476 "`{name}` is above the contract size limit ({deployment_size} > {max_size})."
477 )?;
478 }
479 }
480 }
481
482 if prompt_user
484 && !self.non_interactive
485 && !Confirm::new().with_prompt("Do you wish to continue?".to_string()).interact()?
486 {
487 eyre::bail!("User canceled the script.");
488 }
489
490 Ok(())
491 }
492
493 fn should_broadcast(&self) -> bool {
495 self.broadcast || self.resume
496 }
497}
498
499impl Provider for ScriptArgs {
500 fn metadata(&self) -> Metadata {
501 Metadata::named("Script Args Provider")
502 }
503
504 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
505 let mut dict = Dict::default();
506 if let Some(ref etherscan_api_key) =
507 self.etherscan_api_key.as_ref().filter(|s| !s.trim().is_empty())
508 {
509 dict.insert(
510 "etherscan_api_key".to_string(),
511 figment::value::Value::from(etherscan_api_key.to_string()),
512 );
513 }
514 if let Some(api_version) = &self.etherscan_api_version {
515 dict.insert("etherscan_api_version".to_string(), api_version.to_string().into());
516 }
517 if let Some(timeout) = self.timeout {
518 dict.insert("transaction_timeout".to_string(), timeout.into());
519 }
520 Ok(Map::from([(Config::selected_profile(), dict)]))
521 }
522}
523
524#[derive(Default, Serialize, Clone)]
525pub struct ScriptResult {
526 pub success: bool,
527 #[serde(rename = "raw_logs")]
528 pub logs: Vec<Log>,
529 pub traces: Traces,
530 pub gas_used: u64,
531 pub labeled_addresses: AddressHashMap<String>,
532 #[serde(skip)]
533 pub transactions: Option<BroadcastableTransactions>,
534 pub returned: Bytes,
535 pub address: Option<Address>,
536 #[serde(skip)]
537 pub breakpoints: Breakpoints,
538}
539
540impl ScriptResult {
541 pub fn get_created_contracts(&self) -> Vec<AdditionalContract> {
542 self.traces
543 .iter()
544 .flat_map(|(_, traces)| {
545 traces.nodes().iter().filter_map(|node| {
546 if node.trace.kind.is_any_create() {
547 return Some(AdditionalContract {
548 opcode: node.trace.kind,
549 address: node.trace.address,
550 init_code: node.trace.data.clone(),
551 });
552 }
553 None
554 })
555 })
556 .collect()
557 }
558}
559
560#[derive(Serialize)]
561struct JsonResult<'a> {
562 logs: Vec<String>,
563 returns: &'a HashMap<String, NestedValue>,
564 #[serde(flatten)]
565 result: &'a ScriptResult,
566}
567
568#[derive(Clone, Debug)]
569pub struct ScriptConfig {
570 pub config: Config,
571 pub evm_opts: EvmOpts,
572 pub sender_nonce: u64,
573 pub backends: HashMap<String, Backend>,
575}
576
577impl ScriptConfig {
578 pub async fn new(config: Config, evm_opts: EvmOpts) -> Result<Self> {
579 let sender_nonce = if let Some(fork_url) = evm_opts.fork_url.as_ref() {
580 next_nonce(evm_opts.sender, fork_url, evm_opts.fork_block_number).await?
581 } else {
582 1
584 };
585
586 Ok(Self { config, evm_opts, sender_nonce, backends: HashMap::default() })
587 }
588
589 pub async fn update_sender(&mut self, sender: Address) -> Result<()> {
590 self.sender_nonce = if let Some(fork_url) = self.evm_opts.fork_url.as_ref() {
591 next_nonce(sender, fork_url, None).await?
592 } else {
593 1
595 };
596 self.evm_opts.sender = sender;
597 Ok(())
598 }
599
600 async fn get_runner(&mut self) -> Result<ScriptRunner> {
601 self._get_runner(None, false).await
602 }
603
604 async fn get_runner_with_cheatcodes(
605 &mut self,
606 known_contracts: ContractsByArtifact,
607 script_wallets: Wallets,
608 debug: bool,
609 target: ArtifactId,
610 ) -> Result<ScriptRunner> {
611 self._get_runner(Some((known_contracts, script_wallets, target)), debug).await
612 }
613
614 async fn _get_runner(
615 &mut self,
616 cheats_data: Option<(ContractsByArtifact, Wallets, ArtifactId)>,
617 debug: bool,
618 ) -> Result<ScriptRunner> {
619 trace!("preparing script runner");
620 let env = self.evm_opts.evm_env().await?;
621 let strategy = utils::get_executor_strategy(&self.config);
622
623 let db = if let Some(fork_url) = self.evm_opts.fork_url.as_ref() {
624 match self.backends.get(fork_url) {
625 Some(db) => db.clone(),
626 None => {
627 let fork = self.evm_opts.get_fork(&self.config, env.clone());
628 let backend = Backend::spawn(
629 fork,
630 strategy.runner.new_backend_strategy(strategy.context.as_ref()),
631 )?;
632 self.backends.insert(fork_url.clone(), backend.clone());
633 backend
634 }
635 }
636 } else {
637 Backend::spawn(None, strategy.runner.new_backend_strategy(strategy.context.as_ref()))?
641 };
642
643 let mut builder = ExecutorBuilder::new()
645 .inspectors(|stack| {
646 stack
647 .trace_mode(if debug { TraceMode::Debug } else { TraceMode::Call })
648 .odyssey(self.evm_opts.odyssey)
649 .create2_deployer(self.evm_opts.create2_deployer)
650 })
651 .spec_id(self.config.evm_spec_id())
652 .gas_limit(self.evm_opts.gas_limit())
653 .legacy_assertions(self.config.legacy_assertions);
654
655 if let Some((known_contracts, script_wallets, target)) = cheats_data {
656 builder = builder.inspectors(|stack| {
657 stack
658 .cheatcodes(
659 CheatsConfig::new(
660 strategy.runner.new_cheatcodes_strategy(strategy.context.as_ref()),
661 &self.config,
662 self.evm_opts.clone(),
663 Some(known_contracts),
664 Some(target),
665 )
666 .into(),
667 )
668 .wallets(script_wallets)
669 .enable_isolation(self.evm_opts.isolate)
670 });
671 }
672
673 Ok(ScriptRunner::new(builder.build(env, db, strategy), self.evm_opts.clone()))
674 }
675}
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680 use foundry_config::{NamedChain, UnresolvedEnvVarError};
681 use std::fs;
682 use tempfile::tempdir;
683
684 #[test]
685 fn can_parse_sig() {
686 let sig = "0x522bb704000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfFFb92266";
687 let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--sig", sig]);
688 assert_eq!(args.sig, sig);
689 }
690
691 #[test]
692 fn can_parse_unlocked() {
693 let args = ScriptArgs::parse_from([
694 "foundry-cli",
695 "Contract.sol",
696 "--sender",
697 "0x4e59b44847b379578588920ca78fbf26c0b4956c",
698 "--unlocked",
699 ]);
700 assert!(args.unlocked);
701
702 let key = U256::ZERO;
703 let args = ScriptArgs::try_parse_from([
704 "foundry-cli",
705 "Contract.sol",
706 "--sender",
707 "0x4e59b44847b379578588920ca78fbf26c0b4956c",
708 "--unlocked",
709 "--private-key",
710 key.to_string().as_str(),
711 ]);
712 assert!(args.is_err());
713 }
714
715 #[test]
716 fn can_merge_script_config() {
717 let args = ScriptArgs::parse_from([
718 "foundry-cli",
719 "Contract.sol",
720 "--etherscan-api-key",
721 "goerli",
722 ]);
723 let config = args.load_config().unwrap();
724 assert_eq!(config.etherscan_api_key, Some("goerli".to_string()));
725 }
726
727 #[test]
728 fn can_disable_code_size_limit() {
729 let args =
730 ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--disable-code-size-limit"]);
731 assert!(args.disable_code_size_limit);
732
733 let result = ScriptResult::default();
734 let contracts = ContractsByArtifact::default();
735 let create = Address::ZERO;
736 assert!(args.check_contract_sizes(&result, &contracts, create).is_ok());
737 }
738
739 #[test]
740 fn can_parse_verifier_url() {
741 let args = ScriptArgs::parse_from([
742 "foundry-cli",
743 "script",
744 "script/Test.s.sol:TestScript",
745 "--fork-url",
746 "http://localhost:8545",
747 "--verifier-url",
748 "http://localhost:3000/api/verify",
749 "--etherscan-api-key",
750 "blacksmith",
751 "--broadcast",
752 "--verify",
753 "-vvvvv",
754 ]);
755 assert_eq!(
756 args.verifier.verifier_url,
757 Some("http://localhost:3000/api/verify".to_string())
758 );
759 }
760
761 #[test]
762 fn can_extract_code_size_limit() {
763 let args = ScriptArgs::parse_from([
764 "foundry-cli",
765 "script",
766 "script/Test.s.sol:TestScript",
767 "--fork-url",
768 "http://localhost:8545",
769 "--broadcast",
770 "--code-size-limit",
771 "50000",
772 ]);
773 assert_eq!(args.evm.env.code_size_limit, Some(50000));
774 }
775
776 #[test]
777 fn can_extract_script_etherscan_key() {
778 let temp = tempdir().unwrap();
779 let root = temp.path();
780
781 let config = r#"
782 [profile.default]
783 etherscan_api_key = "amoy"
784
785 [etherscan]
786 amoy = { key = "https://etherscan-amoy.com/" }
787 "#;
788
789 let toml_file = root.join(Config::FILE_NAME);
790 fs::write(toml_file, config).unwrap();
791 let args = ScriptArgs::parse_from([
792 "foundry-cli",
793 "Contract.sol",
794 "--etherscan-api-key",
795 "amoy",
796 "--root",
797 root.as_os_str().to_str().unwrap(),
798 ]);
799
800 let config = args.load_config().unwrap();
801 let amoy = config.get_etherscan_api_key(Some(NamedChain::PolygonAmoy.into()));
802 assert_eq!(amoy, Some("https://etherscan-amoy.com/".to_string()));
803 }
804
805 #[test]
806 fn can_extract_script_rpc_alias() {
807 let temp = tempdir().unwrap();
808 let root = temp.path();
809
810 let config = r#"
811 [profile.default]
812
813 [rpc_endpoints]
814 polygonAmoy = "https://polygon-amoy.g.alchemy.com/v2/${_CAN_EXTRACT_RPC_ALIAS}"
815 "#;
816
817 let toml_file = root.join(Config::FILE_NAME);
818 fs::write(toml_file, config).unwrap();
819 let args = ScriptArgs::parse_from([
820 "foundry-cli",
821 "DeployV1",
822 "--rpc-url",
823 "polygonAmoy",
824 "--root",
825 root.as_os_str().to_str().unwrap(),
826 ]);
827
828 let err = args.load_config_and_evm_opts().unwrap_err();
829
830 assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
831
832 unsafe {
833 std::env::set_var("_CAN_EXTRACT_RPC_ALIAS", "123456");
834 }
835 let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
836 assert_eq!(config.eth_rpc_url, Some("polygonAmoy".to_string()));
837 assert_eq!(
838 evm_opts.fork_url,
839 Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
840 );
841 }
842
843 #[test]
844 fn can_extract_script_rpc_and_etherscan_alias() {
845 let temp = tempdir().unwrap();
846 let root = temp.path();
847
848 let config = r#"
849 [profile.default]
850
851 [rpc_endpoints]
852 amoy = "https://polygon-amoy.g.alchemy.com/v2/${_EXTRACT_RPC_ALIAS}"
853
854 [etherscan]
855 amoy = { key = "${_ETHERSCAN_API_KEY}", chain = 80002, url = "https://amoy.polygonscan.com/" }
856 "#;
857
858 let toml_file = root.join(Config::FILE_NAME);
859 fs::write(toml_file, config).unwrap();
860 let args = ScriptArgs::parse_from([
861 "foundry-cli",
862 "DeployV1",
863 "--rpc-url",
864 "amoy",
865 "--etherscan-api-key",
866 "amoy",
867 "--root",
868 root.as_os_str().to_str().unwrap(),
869 ]);
870 let err = args.load_config_and_evm_opts().unwrap_err();
871
872 assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
873
874 unsafe {
875 std::env::set_var("_EXTRACT_RPC_ALIAS", "123456");
876 }
877 unsafe {
878 std::env::set_var("_ETHERSCAN_API_KEY", "etherscan_api_key");
879 }
880 let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
881 assert_eq!(config.eth_rpc_url, Some("amoy".to_string()));
882 assert_eq!(
883 evm_opts.fork_url,
884 Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
885 );
886 let etherscan = config.get_etherscan_api_key(Some(80002u64.into()));
887 assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
888 let etherscan = config.get_etherscan_api_key(None);
889 assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
890 }
891
892 #[test]
893 fn can_extract_script_rpc_and_sole_etherscan_alias() {
894 let temp = tempdir().unwrap();
895 let root = temp.path();
896
897 let config = r#"
898 [profile.default]
899
900 [rpc_endpoints]
901 amoy = "https://polygon-amoy.g.alchemy.com/v2/${_SOLE_EXTRACT_RPC_ALIAS}"
902
903 [etherscan]
904 amoy = { key = "${_SOLE_ETHERSCAN_API_KEY}" }
905 "#;
906
907 let toml_file = root.join(Config::FILE_NAME);
908 fs::write(toml_file, config).unwrap();
909 let args = ScriptArgs::parse_from([
910 "foundry-cli",
911 "DeployV1",
912 "--rpc-url",
913 "amoy",
914 "--root",
915 root.as_os_str().to_str().unwrap(),
916 ]);
917 let err = args.load_config_and_evm_opts().unwrap_err();
918
919 assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
920
921 unsafe {
922 std::env::set_var("_SOLE_EXTRACT_RPC_ALIAS", "123456");
923 }
924 unsafe {
925 std::env::set_var("_SOLE_ETHERSCAN_API_KEY", "etherscan_api_key");
926 }
927 let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
928 assert_eq!(
929 evm_opts.fork_url,
930 Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
931 );
932 let etherscan = config.get_etherscan_api_key(Some(80002u64.into()));
933 assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
934 let etherscan = config.get_etherscan_api_key(None);
935 assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
936 }
937
938 #[test]
940 fn test_5923() {
941 let args =
942 ScriptArgs::parse_from(["foundry-cli", "DeployV1", "--priority-gas-price", "100"]);
943 assert!(args.priority_gas_price.is_some());
944 }
945
946 #[test]
948 fn test_5910() {
949 let args = ScriptArgs::parse_from([
950 "foundry-cli",
951 "--broadcast",
952 "--with-gas-price",
953 "0",
954 "SolveTutorial",
955 ]);
956 assert!(args.with_gas_price.unwrap().is_zero());
957 }
958}