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