Skip to main content

forge_script/
lib.rs

1//! # foundry-script
2//!
3//! Smart contract scripting.
4
5#![recursion_limit = "256"]
6#![cfg_attr(not(test), warn(unused_crate_dependencies))]
7#![cfg_attr(docsrs, feature(doc_cfg))]
8
9#[macro_use]
10extern crate foundry_common;
11
12#[macro_use]
13extern crate tracing;
14
15use crate::{broadcast::BundledState, runner::ScriptRunner};
16use alloy_json_abi::{Function, JsonAbi};
17use alloy_network::Network;
18use alloy_primitives::{
19    Address, Bytes, Log, U256, hex,
20    map::{AddressHashMap, HashMap},
21};
22use alloy_signer::Signer;
23use broadcast::next_nonce;
24use build::PreprocessedState;
25use clap::{Parser, ValueHint, builder::RangedU64ValueParser};
26use dialoguer::Confirm;
27use eyre::{ContextCompat, Result};
28use forge_script_sequence::{AdditionalContract, NestedValue};
29use forge_verify::{RetryArgs, VerifierArgs};
30use foundry_cli::{
31    opts::{BuildOpts, EvmArgs, GlobalArgs, TempoOpts},
32    utils::LoadConfig,
33};
34use foundry_common::{
35    ContractsByArtifact, SELECTOR_LEN,
36    abi::{encode_function_args, get_func},
37    compile::ContractSizeLimits,
38    shell,
39};
40use foundry_compilers::ArtifactId;
41use foundry_config::{
42    Config, Eip1559FeeEstimatePreset, figment,
43    figment::{
44        Metadata, Profile, Provider,
45        value::{Dict, Map},
46    },
47};
48use foundry_debugger::DebuggerLayout;
49#[cfg(feature = "optimism")]
50use foundry_evm::core::evm::OpEvmNetwork;
51use foundry_evm::{
52    backend::Backend,
53    core::{
54        Breakpoints, FoundryTransaction,
55        evm::{EthEvmNetwork, FoundryEvmNetwork, TempoEvmNetwork, TxEnvFor},
56    },
57    executors::ExecutorBuilder,
58    inspectors::{
59        CheatsConfig,
60        cheatcodes::{BroadcastableTransactions, Wallets},
61    },
62    opts::EvmOpts,
63    revm::interpreter::InstructionResult,
64    traces::{TraceRequirements, Traces},
65};
66use foundry_evm_networks::NetworkConfigs;
67use foundry_wallets::MultiWalletOpts;
68use serde::Serialize;
69use std::path::PathBuf;
70
71mod broadcast;
72mod build;
73mod execute;
74mod multi_sequence;
75mod progress;
76mod providers;
77mod receipts;
78mod runner;
79mod sequence;
80mod session;
81mod simulate;
82mod transaction;
83mod verify;
84mod wallet_session;
85
86pub use wallet_session::ScriptWalletSessionArgs;
87
88// Loads project's figment and merges the build cli arguments into it
89foundry_config::merge_impl_figment_convert!(ScriptArgs, build, evm);
90
91/// CLI arguments for `forge script`.
92#[derive(Clone, Debug, Default, Parser)]
93pub struct ScriptArgs {
94    // Include global options for users of this struct.
95    #[command(flatten)]
96    pub global: GlobalArgs,
97
98    /// The contract you want to run. Either the file path or contract name.
99    ///
100    /// If multiple contracts exist in the same file you must specify the target contract with
101    /// --target-contract.
102    #[arg(value_hint = ValueHint::FilePath)]
103    pub path: String,
104
105    /// Arguments to pass to the script function.
106    pub args: Vec<String>,
107
108    /// The name of the contract you want to run.
109    #[arg(long, visible_alias = "tc", value_name = "CONTRACT_NAME")]
110    pub target_contract: Option<String>,
111
112    /// The signature of the function you want to call in the contract, or raw calldata.
113    #[arg(long, short, default_value = "run")]
114    pub sig: String,
115
116    /// Max priority fee per gas for EIP1559 transactions.
117    #[arg(
118        long,
119        env = "ETH_PRIORITY_GAS_PRICE",
120        value_parser = foundry_cli::utils::parse_ether_value,
121        value_name = "PRICE"
122    )]
123    pub priority_gas_price: Option<U256>,
124
125    /// Use legacy transactions instead of EIP1559 ones.
126    ///
127    /// This is auto-enabled for common networks without EIP1559.
128    #[arg(long)]
129    pub legacy: bool,
130
131    /// How to estimate EIP-1559 fees: `low`, `market` (default), or `aggressive`.
132    ///
133    /// The preset sets the priority-fee percentile and the `maxFeePerGas` buffer
134    /// (`low`: `base_fee * 1.5`, others: `* 2`); `low`'s tighter buffer is more
135    /// likely to stall if the base fee rises. `--with-gas-price` and
136    /// `--priority-gas-price` override only `maxFeePerGas` and
137    /// `maxPriorityFeePerGas` respectively. Ignored for `--legacy`.
138    #[arg(long = "estimate", value_name = "PRESET")]
139    pub eip1559_fee_estimate: Option<Eip1559FeeEstimatePreset>,
140
141    /// Broadcasts the transactions.
142    #[arg(long)]
143    pub broadcast: bool,
144
145    /// Batch all broadcast transactions into a single Tempo batch transaction.
146    ///
147    /// When enabled, all vm.broadcast() calls are collected and sent as a single
148    /// atomic type 0x76 transaction instead of individual transactions.
149    /// This provides atomicity (all-or-nothing execution) and gas savings.
150    #[arg(long)]
151    pub batch: bool,
152
153    /// Tempo transaction options.
154    #[command(flatten)]
155    pub tempo: TempoOpts,
156
157    /// Create a temporary Tempo wallet session, run this script with it, then revoke it.
158    #[command(flatten)]
159    pub wallet_session: ScriptWalletSessionArgs,
160
161    /// Skips on-chain simulation.
162    #[arg(long)]
163    pub skip_simulation: bool,
164
165    /// Relative percentage to multiply gas estimates by.
166    #[arg(long, short, default_value = "130")]
167    pub gas_estimate_multiplier: u64,
168
169    /// Override the sender's initial nonce for script execution and transaction generation.
170    #[arg(
171        long,
172        value_name = "NONCE",
173        value_parser = RangedU64ValueParser::<u64>::new().range(..u64::MAX),
174    )]
175    pub sender_nonce: Option<u64>,
176
177    /// Send via `eth_sendTransaction` using the `--sender` argument as sender.
178    #[arg(
179        long,
180        conflicts_with_all = &["private_key", "private_keys", "ledger", "trezor", "aws", "browser"],
181    )]
182    pub unlocked: bool,
183
184    /// Resumes submitting transactions that failed or timed-out previously.
185    ///
186    /// It DOES NOT simulate the script again and it expects nonces to have remained the same.
187    ///
188    /// Example: If transaction N has a nonce of 22, then the account should have a nonce of 22,
189    /// otherwise it fails.
190    #[arg(long)]
191    pub resume: bool,
192
193    /// If present, --resume or --verify will be assumed to be a multi chain deployment.
194    #[arg(long)]
195    pub multi: bool,
196
197    /// Open the script in the debugger.
198    ///
199    /// Takes precedence over broadcast.
200    #[arg(long)]
201    pub debug: bool,
202
203    /// Debugger layout to use.
204    #[arg(long = "debug-layout", requires = "debug", value_enum)]
205    pub debug_layout: Option<DebuggerLayout>,
206
207    /// Dumps all debugger steps to file.
208    #[arg(
209        long,
210        requires = "debug",
211        value_hint = ValueHint::FilePath,
212        value_name = "PATH"
213    )]
214    pub dump: Option<PathBuf>,
215
216    /// Makes sure a transaction is sent,
217    /// only after its previous one has been confirmed and succeeded.
218    #[arg(long)]
219    pub slow: bool,
220
221    /// Disables interactive prompts that might appear when deploying big contracts.
222    ///
223    /// For more info on the contract size limit, see EIP-170: <https://eips.ethereum.org/EIPS/eip-170>
224    #[arg(long)]
225    pub non_interactive: bool,
226
227    /// Disables the contract size limit during script execution.
228    #[arg(long)]
229    pub disable_code_size_limit: bool,
230
231    /// Disables the labels in the traces.
232    #[arg(long)]
233    pub disable_labels: bool,
234
235    /// The Etherscan (or equivalent) API key
236    #[arg(long, env = "ETHERSCAN_API_KEY", value_name = "KEY")]
237    pub etherscan_api_key: Option<String>,
238
239    /// Verifies all the contracts found in the receipts of a script, if any.
240    #[arg(long, requires = "broadcast")]
241    pub verify: bool,
242
243    /// Gas price for legacy transactions, or max fee per gas for EIP1559 transactions, either
244    /// specified in wei, or as a string with a unit type.
245    ///
246    /// Examples: 1ether, 10gwei, 0.01ether
247    #[arg(
248        long,
249        env = "ETH_GAS_PRICE",
250        value_parser = foundry_cli::utils::parse_ether_value,
251        value_name = "PRICE",
252    )]
253    pub with_gas_price: Option<U256>,
254
255    /// Timeout to use for broadcasting transactions.
256    #[arg(long, env = "ETH_TIMEOUT")]
257    pub timeout: Option<u64>,
258
259    #[command(flatten)]
260    pub build: BuildOpts,
261
262    #[command(flatten)]
263    pub wallets: MultiWalletOpts,
264
265    #[command(flatten)]
266    pub evm: EvmArgs,
267
268    #[command(flatten)]
269    pub verifier: VerifierArgs,
270
271    #[command(flatten)]
272    pub retry: RetryArgs,
273}
274
275impl ScriptArgs {
276    fn has_tempo_session(&self) -> Result<bool> {
277        Ok(self.tempo.session_id()?.is_some())
278    }
279
280    /// Loads config, resolves evm_opts (including network inference from fork), and returns them.
281    async fn resolved_evm_opts(&self) -> Result<(Config, EvmOpts)> {
282        let (config, mut evm_opts) = self.load_config_and_evm_opts()?;
283
284        if self.tempo.is_tempo() || self.has_tempo_session()? {
285            // If Tempo tx options or a session are set, select the Tempo network.
286            evm_opts.networks = NetworkConfigs::with_tempo();
287        } else {
288            // Auto-detect network from fork chain ID when not explicitly configured.
289            evm_opts.infer_network_from_fork().await;
290        }
291
292        Ok((config, evm_opts))
293    }
294
295    async fn preprocess<FEN: FoundryEvmNetwork>(
296        self,
297        config: Config,
298        mut evm_opts: EvmOpts,
299    ) -> Result<PreprocessedState<FEN>> {
300        let args = self;
301        let mut tempo = args.tempo.clone();
302
303        let session_sender = if args.resume {
304            None
305        } else {
306            // Initial scripts may only reveal multi-chain transactions during execution. Use the
307            // session root as the script sender here and validate chain scope during broadcast.
308            tempo.session_sender_for_multi_wallet(&args.wallets, args.evm.sender)?
309        };
310
311        let script_wallets = Wallets::new(args.wallets.get_multi_wallet().await?, args.evm.sender);
312        let browser_wallet = args.wallets.browser_signer::<FEN::Network>().await?;
313
314        if let Some(sender) = session_sender {
315            evm_opts.sender = sender;
316        } else if let Some(sender) = args.maybe_load_private_key()? {
317            evm_opts.sender = sender;
318        } else if args.evm.sender.is_none() {
319            // If no sender was explicitly set via --sender, auto-detect it from available signers:
320            // use the sole signer's address if there's exactly one, or fall back to the browser
321            // wallet address if present.
322            let addresses = script_wallets.addresses();
323            if addresses.len() == 1 {
324                evm_opts.sender = addresses[0];
325            } else if let Some(signer) = browser_wallet.as_ref().map(|b| b.address()) {
326                evm_opts.sender = signer
327            }
328        }
329
330        tempo.resolve_expires();
331
332        let script_config =
333            ScriptConfig::new(config, evm_opts, args.batch, tempo, args.sender_nonce).await?;
334        Ok(PreprocessedState { args, script_config, script_wallets, browser_wallet })
335    }
336
337    /// Executes the script
338    #[allow(clippy::large_stack_frames)]
339    pub async fn run_script(self) -> Result<()> {
340        trace!(target: "script", "executing script command");
341
342        if self.wallet_session.enabled {
343            return self.run_wallet_session_wrapper();
344        }
345
346        let (config, evm_opts) = self.resolved_evm_opts().await?;
347
348        let is_tempo = evm_opts.networks.is_tempo();
349
350        if self.batch && !is_tempo {
351            eyre::bail!("--batch mode is only supported on Tempo networks");
352        }
353
354        if self.unlocked && self.has_tempo_session()? {
355            eyre::bail!("--tempo.session/TEMPO_SESSION_ID cannot be combined with --unlocked");
356        }
357
358        // Box each branch's future to keep its large async state off `run_script`'s future;
359        // otherwise `run_command` trips `clippy::large_stack_frames` by a small margin.
360        if is_tempo {
361            let batch = self.batch;
362            return Box::pin(async move {
363                let bundled =
364                    match self.prepare_bundled::<TempoEvmNetwork>(config, evm_opts).await? {
365                        Some(bundled) => bundled,
366                        None => return Ok(()),
367                    };
368                // batch mode owns its own pending recovery inside broadcast_batch(); running the
369                // generic wait_for_pending() first would race with that and could double-process
370                // an already-confirmed batch hash.
371                let bundled = if batch { bundled } else { bundled.wait_for_pending().await? };
372                let broadcasted = if batch {
373                    bundled.broadcast_batch().await?
374                } else {
375                    bundled.broadcast().await?
376                };
377                if broadcasted.args.verify {
378                    broadcasted.verify().await?;
379                }
380                Ok(())
381            })
382            .await;
383        }
384
385        #[cfg(feature = "optimism")]
386        if evm_opts.networks.is_optimism() {
387            return Box::pin(self.run_generic_script::<OpEvmNetwork>(config, evm_opts)).await;
388        }
389
390        Box::pin(self.run_generic_script::<EthEvmNetwork>(config, evm_opts)).await
391    }
392
393    /// Prepares the bundled state (compile, simulate, bundle) and returns it
394    /// for broadcasting, or returns `None` if there's nothing to broadcast
395    /// (e.g., debug mode, no transactions, missing RPCs).
396    #[allow(clippy::large_stack_frames)]
397    async fn prepare_bundled<FEN: FoundryEvmNetwork>(
398        self,
399        config: Config,
400        evm_opts: EvmOpts,
401    ) -> Result<Option<BundledState<FEN>>> {
402        let state = self.preprocess::<FEN>(config, evm_opts).await?;
403        let create2_deployer = state.script_config.evm_opts.create2_deployer;
404        let compiled = state.compile()?;
405
406        // Move from `CompiledState` to `BundledState` either by resuming or executing and
407        // simulating script.
408        let bundled = if compiled.args.resume {
409            compiled.resume().await?
410        } else {
411            // Drive state machine to point at which we have everything needed for simulation.
412            let pre_simulation = compiled
413                .link()
414                .await?
415                .prepare_execution()
416                .await?
417                .execute()
418                .await?
419                .prepare_simulation()
420                .await?;
421
422            if pre_simulation.args.debug {
423                return match pre_simulation.args.dump.clone() {
424                    Some(path) => pre_simulation.dump_debugger(&path).map(|_| None),
425                    None => pre_simulation.run_debugger().map(|_| None),
426                };
427            }
428
429            if shell::is_json() {
430                pre_simulation.show_json().await?;
431            } else {
432                pre_simulation.show_traces().await?;
433            }
434
435            // Ensure that we have transactions to simulate/broadcast, otherwise exit early to avoid
436            // hard error.
437            if pre_simulation
438                .execution_result
439                .transactions
440                .as_ref()
441                .is_none_or(|txs| txs.is_empty())
442            {
443                if pre_simulation.args.broadcast {
444                    sh_warn!("No transactions to broadcast.")?;
445                }
446
447                return Ok(None);
448            }
449
450            // Check if there are any missing RPCs and exit early to avoid hard error.
451            if pre_simulation.execution_artifacts.rpc_data.missing_rpc {
452                if !shell::is_json() {
453                    sh_println!("\nIf you wish to simulate on-chain transactions pass a RPC URL.")?;
454                }
455
456                return Ok(None);
457            }
458
459            let size_limits = pre_simulation
460                .script_config
461                .evm_opts
462                .env
463                .code_size_limit
464                .or(pre_simulation.script_config.config.code_size_limit)
465                .map(ContractSizeLimits::with_runtime_limit)
466                .unwrap_or_default();
467            pre_simulation.args.check_contract_sizes(
468                size_limits,
469                &pre_simulation.execution_result,
470                &pre_simulation.build_data.known_contracts,
471                create2_deployer,
472            )?;
473
474            pre_simulation.fill_metadata().await?.bundle().await?
475        };
476
477        // Exit early in case user didn't provide any broadcast/verify related flags.
478        if !bundled.args.should_broadcast() {
479            if !shell::is_json() {
480                if shell::verbosity() >= 4 {
481                    sh_println!("\n=== Transactions that will be broadcast ===\n")?;
482                    bundled.sequence.show_transactions()?;
483                }
484
485                sh_println!(
486                    "\nSIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more."
487                )?;
488            }
489            return Ok(None);
490        }
491
492        // Exit early if something is wrong with verification options.
493        if bundled.args.verify {
494            bundled.verify_preflight_check().await?;
495        }
496
497        Ok(Some(bundled))
498    }
499
500    async fn run_generic_script<FEN: FoundryEvmNetwork>(
501        self,
502        config: Config,
503        evm_opts: EvmOpts,
504    ) -> Result<()> {
505        let bundled = match self.prepare_bundled::<FEN>(config, evm_opts).await? {
506            Some(bundled) => bundled,
507            None => return Ok(()),
508        };
509
510        // Wait for pending txes and broadcast others.
511        let broadcasted = bundled.wait_for_pending().await?.broadcast().await?;
512
513        if broadcasted.args.verify {
514            broadcasted.verify().await?;
515        }
516
517        Ok(())
518    }
519
520    /// In case the user has loaded *only* one private-key or a single remote signer (e.g.,
521    /// Turnkey), we can assume that they're using it as the `--sender`.
522    fn maybe_load_private_key(&self) -> Result<Option<Address>> {
523        if let Some(turnkey_address) = self.wallets.turnkey_address() {
524            return Ok(Some(turnkey_address));
525        }
526
527        let maybe_sender = self
528            .wallets
529            .private_keys()?
530            .filter(|pks| pks.len() == 1)
531            .map(|pks| pks.first().unwrap().address());
532        Ok(maybe_sender)
533    }
534
535    /// Returns the Function and calldata based on the signature
536    ///
537    /// If the `sig` is a valid human-readable function we find the corresponding function in the
538    /// `abi` If the `sig` is valid hex, we assume it's calldata and try to find the
539    /// corresponding function by matching the selector, first 4 bytes in the calldata.
540    ///
541    /// Note: We assume that the `sig` is already stripped of its prefix, See [`ScriptArgs`]
542    fn get_method_and_calldata(&self, abi: &JsonAbi) -> Result<(Function, Bytes)> {
543        if let Ok(decoded) = hex::decode(&self.sig) {
544            let selector = &decoded[..SELECTOR_LEN];
545            let func =
546                abi.functions().find(|func| selector == &func.selector()[..]).ok_or_else(|| {
547                    eyre::eyre!(
548                        "Function selector `{}` not found in the ABI",
549                        hex::encode(selector)
550                    )
551                })?;
552            return Ok((func.clone(), decoded.into()));
553        }
554
555        let func = if self.sig.contains('(') {
556            let func = get_func(&self.sig)?;
557            abi.functions()
558                .find(|&abi_func| abi_func.selector() == func.selector())
559                .wrap_err(format!("Function `{}` is not implemented in your script.", self.sig))?
560        } else {
561            let matching_functions =
562                abi.functions().filter(|func| func.name == self.sig).collect::<Vec<_>>();
563            match matching_functions.len() {
564                0 => {
565                    eyre::bail!("Function `{}` not found in the ABI", self.sig);
566                }
567                1 => matching_functions[0],
568                2.. => {
569                    eyre::bail!(
570                        "Multiple functions with the same name `{}` found in the ABI",
571                        self.sig
572                    );
573                }
574            }
575        };
576        let data = encode_function_args(func, &self.args)?;
577
578        Ok((func.clone(), data.into()))
579    }
580
581    /// Checks if the transaction is a deployment with either a size above the default contract size
582    /// limit or specified `code_size_limit`.
583    ///
584    /// If `self.broadcast` is enabled, it asks confirmation of the user. Otherwise, it just warns
585    /// the user.
586    fn check_contract_sizes<N: Network>(
587        &self,
588        size_limits: ContractSizeLimits,
589        result: &ScriptResult<N>,
590        known_contracts: &ContractsByArtifact,
591        create2_deployer: Address,
592    ) -> Result<()> {
593        // If disable-code-size-limit flag is enabled then skip the size check
594        if self.disable_code_size_limit {
595            return Ok(());
596        }
597
598        // (name, &init, &deployed)[]
599        let mut bytecodes: Vec<(String, &[u8], &[u8])> = vec![];
600
601        // From artifacts
602        for (artifact, contract) in known_contracts.iter() {
603            let Some(bytecode) = contract.bytecode() else { continue };
604            let Some(deployed_bytecode) = contract.deployed_bytecode() else { continue };
605            bytecodes.push((artifact.name.clone(), bytecode, deployed_bytecode));
606        }
607
608        // From traces
609        let create_nodes = result.traces.iter().flat_map(|(_, traces)| {
610            traces.nodes().iter().filter(|node| node.trace.kind.is_any_create())
611        });
612        let mut unknown_c = 0usize;
613        for node in create_nodes {
614            let init_code = &node.trace.data;
615            let deployed_code = &node.trace.output;
616            if !bytecodes.iter().any(|(_, b, _)| *b == init_code.as_ref()) {
617                bytecodes.push((format!("Unknown{unknown_c}"), init_code, deployed_code));
618                unknown_c += 1;
619            }
620        }
621
622        let mut prompt_user = false;
623        let max_size = size_limits.runtime;
624
625        for (data, to) in result.transactions.iter().flat_map(|txes| {
626            txes.iter().filter_map(|tx| {
627                tx.transaction
628                    .input()
629                    .filter(|data| data.len() > max_size)
630                    .map(|data| (data, tx.transaction.to()))
631            })
632        }) {
633            let mut offset = 0;
634
635            // Find if it's a CREATE or CREATE2. Otherwise, skip transaction.
636            if let Some(to) = to {
637                if to == create2_deployer {
638                    // Size of the salt prefix.
639                    offset = 32;
640                } else {
641                    continue;
642                }
643            }
644
645            // Find artifact with a deployment code same as the data.
646            if let Some((name, _, deployed_code)) =
647                bytecodes.iter().find(|(_, init_code, _)| *init_code == &data[offset..])
648            {
649                let deployment_size = deployed_code.len();
650
651                if deployment_size > max_size {
652                    prompt_user = self.should_broadcast();
653                    sh_err!(
654                        "`{name}` is above the contract size limit ({deployment_size} > {max_size})."
655                    )?;
656                }
657            }
658        }
659
660        // Only prompt if we're broadcasting and we've not disabled interactivity.
661        if prompt_user
662            && !self.non_interactive
663            && !Confirm::new().with_prompt("Do you wish to continue?".to_string()).interact()?
664        {
665            eyre::bail!("User canceled the script.");
666        }
667
668        Ok(())
669    }
670
671    /// We only broadcast transactions if --broadcast, --resume, or --verify was passed.
672    const fn should_broadcast(&self) -> bool {
673        self.broadcast || self.resume || self.verify
674    }
675}
676
677impl Provider for ScriptArgs {
678    fn metadata(&self) -> Metadata {
679        Metadata::named("Script Args Provider")
680    }
681
682    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
683        let mut dict = Dict::default();
684
685        if let Some(etherscan_api_key) =
686            self.etherscan_api_key.as_ref().filter(|s| !s.trim().is_empty())
687        {
688            dict.insert(
689                "etherscan_api_key".to_string(),
690                figment::value::Value::from(etherscan_api_key.clone()),
691            );
692        }
693
694        if let Some(timeout) = self.timeout {
695            dict.insert("transaction_timeout".to_string(), timeout.into());
696        }
697
698        if let Some(preset) = self.eip1559_fee_estimate {
699            dict.insert(
700                "eip1559_fee_estimate".to_string(),
701                figment::value::Value::from(preset.to_string()),
702            );
703        }
704
705        Ok(Map::from([(Config::selected_profile(), dict)]))
706    }
707}
708
709#[derive(Serialize, Clone)]
710#[serde(bound = "")]
711pub struct ScriptResult<N: Network> {
712    pub success: bool,
713    #[serde(rename = "raw_logs")]
714    pub logs: Vec<Log>,
715    pub traces: Traces,
716    pub gas_used: u64,
717    pub labeled_addresses: AddressHashMap<String>,
718    #[serde(skip)]
719    pub debug_bytecodes: AddressHashMap<Bytes>,
720    #[serde(skip)]
721    pub transactions: Option<BroadcastableTransactions<N>>,
722    pub returned: Bytes,
723    #[serde(skip)]
724    pub exit_reason: Option<InstructionResult>,
725    pub address: Option<Address>,
726    #[serde(skip)]
727    pub breakpoints: Breakpoints,
728}
729
730impl<N: Network> Default for ScriptResult<N> {
731    fn default() -> Self {
732        Self {
733            success: Default::default(),
734            logs: Default::default(),
735            traces: Default::default(),
736            gas_used: Default::default(),
737            labeled_addresses: Default::default(),
738            debug_bytecodes: Default::default(),
739            transactions: Default::default(),
740            returned: Default::default(),
741            exit_reason: Default::default(),
742            address: Default::default(),
743            breakpoints: Default::default(),
744        }
745    }
746}
747
748impl<N: Network> ScriptResult<N> {
749    pub fn get_created_contracts(
750        &self,
751        known_contracts: &ContractsByArtifact,
752    ) -> Vec<AdditionalContract> {
753        self.traces
754            .iter()
755            .flat_map(|(_, traces)| {
756                traces.nodes().iter().filter_map(|node| {
757                    if node.trace.kind.is_any_create() {
758                        let init_code = node.trace.data.clone();
759                        let contract_name = known_contracts
760                            .find_by_creation_code(init_code.as_ref())
761                            .map(|artifact| artifact.0.name.clone());
762                        return Some(AdditionalContract {
763                            call_kind: node.trace.kind,
764                            address: node.trace.address,
765                            contract_name,
766                            init_code,
767                        });
768                    }
769                    None
770                })
771            })
772            .collect()
773    }
774}
775
776#[derive(Serialize)]
777#[serde(bound = "")]
778struct JsonResult<'a, N: Network> {
779    logs: Vec<String>,
780    returns: &'a HashMap<String, NestedValue>,
781    #[serde(flatten)]
782    result: &'a ScriptResult<N>,
783}
784
785#[derive(Clone, Debug)]
786pub struct ScriptConfig<FEN: FoundryEvmNetwork> {
787    pub config: Config,
788    pub evm_opts: EvmOpts,
789    pub sender_nonce: u64,
790    sender_nonce_override: Option<u64>,
791    /// Maps a rpc url to a backend
792    pub backends: HashMap<String, Backend<FEN>>,
793    /// Whether to batch all broadcast transactions into a single Tempo batch transaction.
794    pub batch: bool,
795    /// Tempo transaction options applied to broadcast transactions.
796    pub tempo: TempoOpts,
797}
798
799impl<FEN: FoundryEvmNetwork> ScriptConfig<FEN> {
800    pub async fn new(
801        config: Config,
802        evm_opts: EvmOpts,
803        batch: bool,
804        tempo: TempoOpts,
805        sender_nonce_override: Option<u64>,
806    ) -> Result<Self> {
807        let sender_nonce = if let Some(sender_nonce) = sender_nonce_override {
808            sender_nonce
809        } else if let Some(fork_url) = evm_opts.fork_url.as_ref() {
810            next_nonce(evm_opts.sender, fork_url, evm_opts.fork_block_number).await?
811        } else {
812            // dapptools compatibility
813            1
814        };
815
816        Ok(Self {
817            config,
818            evm_opts,
819            sender_nonce,
820            sender_nonce_override,
821            backends: HashMap::default(),
822            batch,
823            tempo,
824        })
825    }
826
827    pub async fn update_sender(&mut self, sender: Address) -> Result<()> {
828        self.sender_nonce = if let Some(sender_nonce) = self.sender_nonce_override {
829            sender_nonce
830        } else if let Some(fork_url) = self.evm_opts.fork_url.as_ref() {
831            next_nonce(sender, fork_url, None).await?
832        } else {
833            // dapptools compatibility
834            1
835        };
836        self.evm_opts.sender = sender;
837        Ok(())
838    }
839
840    pub(crate) async fn update_tempo_session_sender(
841        &mut self,
842        wallets: &MultiWalletOpts,
843        expected_sender: Option<Address>,
844    ) -> Result<()> {
845        if let Some(sender) =
846            self.tempo.session_sender_for_multi_wallet(wallets, expected_sender)?
847        {
848            self.update_sender(sender).await?;
849        }
850        Ok(())
851    }
852
853    async fn get_runner(&mut self) -> Result<ScriptRunner<FEN>> {
854        self._get_runner(None, false).await
855    }
856
857    async fn get_runner_with_cheatcodes(
858        &mut self,
859        known_contracts: ContractsByArtifact,
860        script_wallets: Wallets,
861        debug: bool,
862        target: ArtifactId,
863    ) -> Result<ScriptRunner<FEN>> {
864        self._get_runner(Some((known_contracts, script_wallets, target)), debug).await
865    }
866
867    async fn _get_runner(
868        &mut self,
869        cheats_data: Option<(ContractsByArtifact, Wallets, ArtifactId)>,
870        debug: bool,
871    ) -> Result<ScriptRunner<FEN>> {
872        trace!("preparing script runner");
873        let (evm_env, mut tx_env, fork_block) = self.evm_opts.env::<_, _, TxEnvFor<FEN>>().await?;
874
875        let db = if let Some(fork_url) = self.evm_opts.fork_url.as_ref() {
876            match self.backends.get(fork_url) {
877                Some(db) => db.clone(),
878                None => {
879                    let fork =
880                        self.evm_opts.get_fork(&self.config, evm_env.cfg_env.chain_id, fork_block);
881                    let backend = Backend::spawn(fork)?;
882                    self.backends.insert(fork_url.clone(), backend.clone());
883                    backend
884                }
885            }
886        } else {
887            // It's only really `None`, when we don't pass any `--fork-url`. And if so, there is
888            // no need to cache it, since there won't be any onchain simulation that we'd need
889            // to cache the backend for.
890            Backend::spawn(None)?
891        };
892
893        // We need to enable tracing to decode contract names: local or external.
894        let mut builder = ExecutorBuilder::default()
895            .inspectors(|stack| {
896                stack
897                    .logs(self.config.live_logs)
898                    .trace_requirements(
899                        TraceRequirements::none()
900                            .with_calls(true)
901                            .with_debug(debug)
902                            .with_verbosity(self.evm_opts.verbosity),
903                    )
904                    .networks(self.evm_opts.networks)
905                    .create2_deployer(self.evm_opts.create2_deployer)
906            })
907            .spec_id(self.config.evm_spec_id())
908            .gas_limit(self.evm_opts.gas_limit())
909            .legacy_assertions(self.config.legacy_assertions);
910
911        if let Some((known_contracts, script_wallets, target)) = cheats_data {
912            builder = builder.inspectors(|stack| {
913                stack
914                    .cheatcodes(
915                        CheatsConfig::new(
916                            &self.config,
917                            self.evm_opts.clone(),
918                            Some(known_contracts),
919                            Some(target),
920                            self.tempo.fee_token,
921                            self.batch,
922                        )
923                        .into(),
924                    )
925                    .wallets(script_wallets)
926                    .enable_isolation(self.evm_opts.isolate)
927            });
928        }
929
930        // Propagate fee token to the transaction environment so that internal EVM calls
931        // (e.g. script deployment, setUp) use the correct fee token for Tempo networks.
932        tx_env.set_fee_token(self.tempo.fee_token);
933
934        let mut runner =
935            ScriptRunner::new(builder.build(evm_env, tx_env, db), self.evm_opts.clone())
936                .with_debug_bytecodes(debug);
937
938        if self.sender_nonce_override.is_some() {
939            runner.executor.set_nonce(self.evm_opts.sender, self.sender_nonce)?;
940        }
941
942        Ok(runner)
943    }
944}
945
946#[cfg(test)]
947mod tests {
948    use super::*;
949    use alloy_chains::NamedChain;
950    use alloy_network::Ethereum;
951    use alloy_primitives::{B256, address};
952    use foundry_cli::opts::TEMPO_SESSION_ID_ENV;
953    use foundry_common::tempo::{
954        KeyType, SessionEntry, SessionKeyMaterial, SessionStatus, TEMPO_HOME_ENV,
955        upsert_session_entry,
956    };
957    use foundry_config::UnresolvedEnvVarError;
958    use std::{fs, sync::LazyLock};
959    use tempfile::tempdir;
960    use tokio::sync::{Mutex, MutexGuard};
961
962    const SESSION_PRIVATE_KEY: &str =
963        "0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0";
964    const SESSION_ID_HEX: &str =
965        "0x1111111111111111111111111111111111111111111111111111111111111111";
966    const SESSION_ROOT_ADDRESS: &str = "0x1111111111111111111111111111111111111111";
967    static TEMPO_HOME_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
968
969    fn active_session_entry(
970        session_id: B256,
971        root_account: Address,
972        chain_id: u64,
973    ) -> SessionEntry {
974        let key = foundry_wallets::utils::create_private_key_signer(SESSION_PRIVATE_KEY).unwrap();
975        SessionEntry {
976            session_id,
977            root_account,
978            chain_id,
979            key_address: key.address(),
980            expiry: u64::MAX,
981            scope: None,
982            limits: None,
983            status: SessionStatus::Active,
984            key: Some(SessionKeyMaterial {
985                key_type: KeyType::Secp256k1,
986                key: SESSION_PRIVATE_KEY.to_string(),
987                key_authorization: None,
988            }),
989        }
990    }
991
992    struct TempoHomeGuard {
993        _guard: MutexGuard<'static, ()>,
994    }
995
996    impl TempoHomeGuard {
997        async fn set(path: &std::path::Path) -> Self {
998            let guard = TEMPO_HOME_LOCK.lock().await;
999            // SAFETY: test-only environment override for Tempo local state.
1000            unsafe {
1001                std::env::remove_var(TEMPO_SESSION_ID_ENV);
1002                std::env::set_var(TEMPO_HOME_ENV, path);
1003            }
1004            Self { _guard: guard }
1005        }
1006    }
1007
1008    impl Drop for TempoHomeGuard {
1009        fn drop(&mut self) {
1010            // SAFETY: restore process environment after the critical section.
1011            unsafe {
1012                std::env::remove_var(TEMPO_HOME_ENV);
1013                std::env::remove_var(TEMPO_SESSION_ID_ENV);
1014            }
1015        }
1016    }
1017
1018    fn session_root() -> Address {
1019        SESSION_ROOT_ADDRESS.parse().unwrap()
1020    }
1021
1022    #[test]
1023    fn can_parse_sig() {
1024        let sig = "0x522bb704000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfFFb92266";
1025        let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--sig", sig]);
1026        assert_eq!(args.sig, sig);
1027    }
1028
1029    #[test]
1030    fn rejects_max_sender_nonce() {
1031        let max_valid_nonce = (u64::MAX - 1).to_string();
1032        let args = ScriptArgs::try_parse_from([
1033            "foundry-cli",
1034            "Contract.sol",
1035            "--sender-nonce",
1036            &max_valid_nonce,
1037        ])
1038        .unwrap();
1039        assert_eq!(args.sender_nonce, Some(u64::MAX - 1));
1040
1041        let max_nonce = u64::MAX.to_string();
1042        let err = ScriptArgs::try_parse_from([
1043            "foundry-cli",
1044            "Contract.sol",
1045            "--sender-nonce",
1046            &max_nonce,
1047        ])
1048        .unwrap_err();
1049        assert_eq!(err.kind(), clap::error::ErrorKind::ValueValidation);
1050    }
1051
1052    #[test]
1053    fn can_parse_shared_tempo_opts() {
1054        let args = ScriptArgs::parse_from([
1055            "foundry-cli",
1056            "Contract.sol",
1057            "--tempo.fee-token",
1058            "1",
1059            "--tempo.expires",
1060            "10",
1061        ]);
1062
1063        assert_eq!(
1064            args.tempo.fee_token,
1065            Some(address!("0x20C0000000000000000000000000000000000001"))
1066        );
1067        assert_eq!(args.tempo.expires, Some(10));
1068    }
1069
1070    #[test]
1071    fn can_parse_sponsor_tempo_opts() {
1072        let args = ScriptArgs::parse_from([
1073            "foundry-cli",
1074            "Contract.sol",
1075            "--tempo.sponsor",
1076            SESSION_ROOT_ADDRESS,
1077            "--tempo.sponsor-signer",
1078            "env://TEMPO_SPONSOR_PK",
1079        ]);
1080
1081        assert_eq!(args.tempo.sponsor, Some(session_root()));
1082        assert_eq!(args.tempo.sponsor_signer.as_deref(), Some("env://TEMPO_SPONSOR_PK"));
1083    }
1084
1085    #[test]
1086    fn can_parse_full_tempo_opts() {
1087        let args =
1088            ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--tempo.nonce-key", "1"]);
1089
1090        assert_eq!(args.tempo.nonce_key, Some(U256::from(1)));
1091    }
1092
1093    #[test]
1094    fn can_parse_tempo_session_opt() {
1095        let args = ScriptArgs::parse_from([
1096            "foundry-cli",
1097            "Contract.sol",
1098            "--tempo.session",
1099            SESSION_ID_HEX,
1100        ]);
1101
1102        assert_eq!(args.tempo.session, Some(B256::from([0x11; 32])),);
1103    }
1104
1105    #[tokio::test]
1106    async fn tempo_session_sets_script_sender_to_root_account() {
1107        let temp = tempdir().unwrap();
1108        let session_id = B256::from([0x22; 32]);
1109        let root = session_root();
1110        let chain_id = foundry_common::DEV_CHAIN_ID;
1111
1112        let _guard = TempoHomeGuard::set(temp.path()).await;
1113        upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
1114
1115        let args = ScriptArgs::parse_from([
1116            "foundry-cli",
1117            "Contract.sol",
1118            "--tempo.session",
1119            &format!("{session_id:?}"),
1120        ]);
1121        let evm_opts = EvmOpts {
1122            networks: NetworkConfigs::with_tempo(),
1123            env: foundry_evm::opts::Env { chain_id: Some(chain_id), ..Default::default() },
1124            ..Default::default()
1125        };
1126
1127        let state = args.preprocess::<TempoEvmNetwork>(Config::default(), evm_opts).await.unwrap();
1128        assert_eq!(state.script_config.evm_opts.sender, root);
1129    }
1130
1131    #[tokio::test]
1132    async fn tempo_session_resume_multi_defers_session_sender_until_reexecution() {
1133        let temp = tempdir().unwrap();
1134        let session_id = B256::from([0x55; 32]);
1135        let root = session_root();
1136        let chain_id = 4217;
1137
1138        let _guard = TempoHomeGuard::set(temp.path()).await;
1139        upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
1140
1141        let args = ScriptArgs::parse_from([
1142            "foundry-cli",
1143            "Contract.sol",
1144            "--resume",
1145            "--multi",
1146            "--tempo.session",
1147            &format!("{session_id:?}"),
1148        ]);
1149        let evm_opts = EvmOpts { networks: NetworkConfigs::with_tempo(), ..Default::default() };
1150
1151        let state = args.preprocess::<TempoEvmNetwork>(Config::default(), evm_opts).await.unwrap();
1152        assert_ne!(state.script_config.evm_opts.sender, root);
1153    }
1154
1155    #[tokio::test]
1156    async fn tempo_session_resume_defers_session_sender_until_reexecution() {
1157        let temp = tempdir().unwrap();
1158        let session_id = B256::from([0x77; 32]);
1159        let root = session_root();
1160        let chain_id = 4217;
1161
1162        let _guard = TempoHomeGuard::set(temp.path()).await;
1163        upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
1164
1165        let args = ScriptArgs::parse_from([
1166            "foundry-cli",
1167            "Contract.sol",
1168            "--resume",
1169            "--tempo.session",
1170            &format!("{session_id:?}"),
1171        ]);
1172        let evm_opts = EvmOpts { networks: NetworkConfigs::with_tempo(), ..Default::default() };
1173
1174        let state = args.preprocess::<TempoEvmNetwork>(Config::default(), evm_opts).await.unwrap();
1175        assert_ne!(state.script_config.evm_opts.sender, root);
1176    }
1177
1178    #[tokio::test]
1179    async fn tempo_session_non_resume_multi_sets_sender_without_chain_validation() {
1180        let temp = tempdir().unwrap();
1181        let session_id = B256::from([0x66; 32]);
1182        let root = session_root();
1183        let chain_id = 4217;
1184
1185        let _guard = TempoHomeGuard::set(temp.path()).await;
1186        upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
1187
1188        let args = ScriptArgs::parse_from([
1189            "foundry-cli",
1190            "Contract.sol",
1191            "--multi",
1192            "--tempo.session",
1193            &format!("{session_id:?}"),
1194        ]);
1195        let evm_opts = EvmOpts { networks: NetworkConfigs::with_tempo(), ..Default::default() };
1196
1197        let state = args.preprocess::<TempoEvmNetwork>(Config::default(), evm_opts).await.unwrap();
1198        assert_eq!(state.script_config.evm_opts.sender, root);
1199    }
1200
1201    #[tokio::test]
1202    async fn tempo_session_initial_broadcast_sets_sender_without_chain_validation() {
1203        let temp = tempdir().unwrap();
1204        let session_id = B256::from([0x88; 32]);
1205        let root = session_root();
1206        let chain_id = 4217;
1207
1208        let _guard = TempoHomeGuard::set(temp.path()).await;
1209        upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
1210
1211        let args = ScriptArgs::parse_from([
1212            "foundry-cli",
1213            "Contract.sol",
1214            "--broadcast",
1215            "--tempo.session",
1216            &format!("{session_id:?}"),
1217        ]);
1218        let evm_opts = EvmOpts { networks: NetworkConfigs::with_tempo(), ..Default::default() };
1219
1220        let state = args.preprocess::<TempoEvmNetwork>(Config::default(), evm_opts).await.unwrap();
1221        assert_eq!(state.script_config.evm_opts.sender, root);
1222    }
1223
1224    #[tokio::test]
1225    async fn tempo_session_env_selects_tempo_network() {
1226        let temp = tempdir().unwrap();
1227        let _guard = TempoHomeGuard::set(temp.path()).await;
1228        let session_id = B256::from([0x44; 32]);
1229        // SAFETY: serialized by TempoHomeGuard.
1230        unsafe { std::env::set_var(TEMPO_SESSION_ID_ENV, format!("{session_id:?}")) };
1231
1232        let args = ScriptArgs::parse_from(["foundry-cli", "Contract.sol"]);
1233        let (_, evm_opts) = args.resolved_evm_opts().await.unwrap();
1234
1235        assert!(evm_opts.networks.is_tempo());
1236    }
1237
1238    #[tokio::test]
1239    async fn tempo_session_rejects_explicit_script_wallet_signer() {
1240        let temp = tempdir().unwrap();
1241        let session_id = B256::from([0x33; 32]);
1242        let root = session_root();
1243        let chain_id = foundry_common::DEV_CHAIN_ID;
1244
1245        let _guard = TempoHomeGuard::set(temp.path()).await;
1246        upsert_session_entry(active_session_entry(session_id, root, chain_id)).unwrap();
1247
1248        let args = ScriptArgs::parse_from([
1249            "foundry-cli",
1250            "Contract.sol",
1251            "--tempo.session",
1252            &format!("{session_id:?}"),
1253            "--private-key",
1254            SESSION_PRIVATE_KEY,
1255        ]);
1256        let evm_opts = EvmOpts {
1257            networks: NetworkConfigs::with_tempo(),
1258            env: foundry_evm::opts::Env { chain_id: Some(chain_id), ..Default::default() },
1259            ..Default::default()
1260        };
1261
1262        let err = match args.preprocess::<TempoEvmNetwork>(Config::default(), evm_opts).await {
1263            Ok(_) => panic!("expected --tempo.session with --private-key to fail"),
1264            Err(err) => err,
1265        };
1266        assert!(err.to_string().contains("explicit wallet signer"), "{err}");
1267    }
1268
1269    #[test]
1270    fn can_parse_unlocked() {
1271        let args = ScriptArgs::parse_from([
1272            "foundry-cli",
1273            "Contract.sol",
1274            "--sender",
1275            "0x4e59b44847b379578588920ca78fbf26c0b4956c",
1276            "--unlocked",
1277        ]);
1278        assert!(args.unlocked);
1279
1280        let key = U256::ZERO;
1281        let args = ScriptArgs::try_parse_from([
1282            "foundry-cli",
1283            "Contract.sol",
1284            "--sender",
1285            "0x4e59b44847b379578588920ca78fbf26c0b4956c",
1286            "--unlocked",
1287            "--private-key",
1288            &key.to_string(),
1289        ]);
1290        assert!(args.is_err());
1291    }
1292
1293    #[test]
1294    fn can_merge_script_config() {
1295        let args = ScriptArgs::parse_from([
1296            "foundry-cli",
1297            "Contract.sol",
1298            "--etherscan-api-key",
1299            "goerli",
1300        ]);
1301        let config = args.load_config().unwrap();
1302        assert_eq!(config.etherscan_api_key, Some("goerli".to_string()));
1303    }
1304
1305    #[test]
1306    fn can_disable_code_size_limit() {
1307        let args =
1308            ScriptArgs::parse_from(["foundry-cli", "Contract.sol", "--disable-code-size-limit"]);
1309        assert!(args.disable_code_size_limit);
1310
1311        let result = ScriptResult::<Ethereum>::default();
1312        let contracts = ContractsByArtifact::default();
1313        let create = Address::ZERO;
1314        assert!(
1315            args.check_contract_sizes(ContractSizeLimits::default(), &result, &contracts, create)
1316                .is_ok()
1317        );
1318    }
1319
1320    #[test]
1321    fn can_parse_verifier_url() {
1322        let args = ScriptArgs::parse_from([
1323            "foundry-cli",
1324            "script",
1325            "script/Test.s.sol:TestScript",
1326            "--fork-url",
1327            "http://localhost:8545",
1328            "--verifier-url",
1329            "http://localhost:3000/api/verify",
1330            "--etherscan-api-key",
1331            "blacksmith",
1332            "--broadcast",
1333            "--verify",
1334            "-vvvvv",
1335        ]);
1336        assert_eq!(
1337            args.verifier.verifier_url,
1338            Some("http://localhost:3000/api/verify".to_string())
1339        );
1340    }
1341
1342    #[test]
1343    fn can_extract_code_size_limit() {
1344        let args = ScriptArgs::parse_from([
1345            "foundry-cli",
1346            "script",
1347            "script/Test.s.sol:TestScript",
1348            "--fork-url",
1349            "http://localhost:8545",
1350            "--broadcast",
1351            "--code-size-limit",
1352            "50000",
1353        ]);
1354        assert_eq!(args.evm.env.code_size_limit, Some(50000));
1355    }
1356
1357    /// `--code-size-limit` on the CLI should be used by `check_contract_sizes`, not silently
1358    /// ignored in favour of the foundry.toml value (which defaults to None → EIP-170's 24576).
1359    #[test]
1360    fn cli_code_size_limit_is_honoured_by_check() {
1361        let args = ScriptArgs::parse_from([
1362            "foundry-cli",
1363            "script",
1364            "script/Test.s.sol:TestScript",
1365            "--code-size-limit",
1366            "2147483647",
1367        ]);
1368        // The CLI flag must land in evm_opts so that the size_limits computation in run() picks
1369        // it up via `.evm_opts.env.code_size_limit.or(config.code_size_limit)`.
1370        assert_eq!(args.evm.env.code_size_limit, Some(2147483647));
1371    }
1372
1373    #[test]
1374    fn can_extract_script_etherscan_key() {
1375        let temp = tempdir().unwrap();
1376        let root = temp.path();
1377
1378        let config = r#"
1379                [profile.default]
1380                etherscan_api_key = "amoy"
1381
1382                [etherscan]
1383                amoy = { key = "https://etherscan-amoy.com/" }
1384            "#;
1385
1386        let toml_file = root.join(Config::FILE_NAME);
1387        fs::write(toml_file, config).unwrap();
1388        let args = ScriptArgs::parse_from([
1389            "foundry-cli",
1390            "Contract.sol",
1391            "--etherscan-api-key",
1392            "amoy",
1393            "--root",
1394            root.as_os_str().to_str().unwrap(),
1395        ]);
1396
1397        let config = args.load_config().unwrap();
1398        let amoy = config.get_etherscan_api_key(Some(NamedChain::PolygonAmoy.into()));
1399        assert_eq!(amoy, Some("https://etherscan-amoy.com/".to_string()));
1400    }
1401
1402    #[test]
1403    fn can_extract_script_rpc_alias() {
1404        let temp = tempdir().unwrap();
1405        let root = temp.path();
1406
1407        let config = r#"
1408                [profile.default]
1409
1410                [rpc_endpoints]
1411                polygonAmoy = "https://polygon-amoy.g.alchemy.com/v2/${_CAN_EXTRACT_RPC_ALIAS}"
1412            "#;
1413
1414        let toml_file = root.join(Config::FILE_NAME);
1415        fs::write(toml_file, config).unwrap();
1416        let args = ScriptArgs::parse_from([
1417            "foundry-cli",
1418            "DeployV1",
1419            "--rpc-url",
1420            "polygonAmoy",
1421            "--root",
1422            root.as_os_str().to_str().unwrap(),
1423        ]);
1424
1425        let err = args.load_config_and_evm_opts().unwrap_err();
1426
1427        assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
1428
1429        unsafe {
1430            std::env::set_var("_CAN_EXTRACT_RPC_ALIAS", "123456");
1431        }
1432        let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
1433        assert_eq!(config.eth_rpc_url, Some("polygonAmoy".to_string()));
1434        assert_eq!(
1435            evm_opts.fork_url,
1436            Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
1437        );
1438    }
1439
1440    #[test]
1441    fn can_extract_script_rpc_and_etherscan_alias() {
1442        let temp = tempdir().unwrap();
1443        let root = temp.path();
1444
1445        let config = r#"
1446            [profile.default]
1447
1448            [rpc_endpoints]
1449            amoy = "https://polygon-amoy.g.alchemy.com/v2/${_EXTRACT_RPC_ALIAS}"
1450
1451            [etherscan]
1452            amoy = { key = "${_ETHERSCAN_API_KEY}", chain = 80002, url = "https://amoy.polygonscan.com/" }
1453        "#;
1454
1455        let toml_file = root.join(Config::FILE_NAME);
1456        fs::write(toml_file, config).unwrap();
1457        let args = ScriptArgs::parse_from([
1458            "foundry-cli",
1459            "DeployV1",
1460            "--rpc-url",
1461            "amoy",
1462            "--etherscan-api-key",
1463            "amoy",
1464            "--root",
1465            root.as_os_str().to_str().unwrap(),
1466        ]);
1467        let err = args.load_config_and_evm_opts().unwrap_err();
1468
1469        assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
1470
1471        unsafe {
1472            std::env::set_var("_EXTRACT_RPC_ALIAS", "123456");
1473        }
1474        unsafe {
1475            std::env::set_var("_ETHERSCAN_API_KEY", "etherscan_api_key");
1476        }
1477        let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
1478        assert_eq!(config.eth_rpc_url, Some("amoy".to_string()));
1479        assert_eq!(
1480            evm_opts.fork_url,
1481            Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
1482        );
1483        let etherscan = config.get_etherscan_api_key(Some(80002u64.into()));
1484        assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
1485        let etherscan = config.get_etherscan_api_key(None);
1486        assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
1487    }
1488
1489    #[test]
1490    fn can_extract_script_rpc_and_sole_etherscan_alias() {
1491        let temp = tempdir().unwrap();
1492        let root = temp.path();
1493
1494        let config = r#"
1495                [profile.default]
1496
1497               [rpc_endpoints]
1498                amoy = "https://polygon-amoy.g.alchemy.com/v2/${_SOLE_EXTRACT_RPC_ALIAS}"
1499
1500                [etherscan]
1501                amoy = { key = "${_SOLE_ETHERSCAN_API_KEY}" }
1502            "#;
1503
1504        let toml_file = root.join(Config::FILE_NAME);
1505        fs::write(toml_file, config).unwrap();
1506        let args = ScriptArgs::parse_from([
1507            "foundry-cli",
1508            "DeployV1",
1509            "--rpc-url",
1510            "amoy",
1511            "--root",
1512            root.as_os_str().to_str().unwrap(),
1513        ]);
1514        let err = args.load_config_and_evm_opts().unwrap_err();
1515
1516        assert!(err.downcast::<UnresolvedEnvVarError>().is_ok());
1517
1518        unsafe {
1519            std::env::set_var("_SOLE_EXTRACT_RPC_ALIAS", "123456");
1520        }
1521        unsafe {
1522            std::env::set_var("_SOLE_ETHERSCAN_API_KEY", "etherscan_api_key");
1523        }
1524        let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
1525        assert_eq!(
1526            evm_opts.fork_url,
1527            Some("https://polygon-amoy.g.alchemy.com/v2/123456".to_string())
1528        );
1529        let etherscan = config.get_etherscan_api_key(Some(80002u64.into()));
1530        assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
1531        let etherscan = config.get_etherscan_api_key(None);
1532        assert_eq!(etherscan, Some("etherscan_api_key".to_string()));
1533    }
1534
1535    // <https://github.com/foundry-rs/foundry/issues/5923>
1536    #[test]
1537    fn test_5923() {
1538        let args =
1539            ScriptArgs::parse_from(["foundry-cli", "DeployV1", "--priority-gas-price", "100"]);
1540        assert!(args.priority_gas_price.is_some());
1541    }
1542
1543    #[test]
1544    fn test_eip1559_fee_estimate() {
1545        // Defaults to unset (config provides `market`).
1546        let args = ScriptArgs::parse_from(["foundry-cli", "DeployV1"]);
1547        assert!(args.eip1559_fee_estimate.is_none());
1548
1549        let args = ScriptArgs::parse_from(["foundry-cli", "DeployV1", "--estimate", "aggressive"]);
1550        assert_eq!(args.eip1559_fee_estimate, Some(Eip1559FeeEstimatePreset::Aggressive));
1551    }
1552
1553    // <https://github.com/foundry-rs/foundry/issues/5910>
1554    #[test]
1555    fn test_5910() {
1556        let args = ScriptArgs::parse_from([
1557            "foundry-cli",
1558            "--broadcast",
1559            "--with-gas-price",
1560            "0",
1561            "SolveTutorial",
1562        ]);
1563        assert!(args.with_gas_price.unwrap().is_zero());
1564    }
1565
1566    #[test]
1567    fn test_priority_gas_price_cannot_exceed_gas_price() {
1568        let args = ScriptArgs::parse_from([
1569            "foundry-cli",
1570            "--broadcast",
1571            "--with-gas-price",
1572            "100",
1573            "--priority-gas-price",
1574            "200",
1575            "Script",
1576        ]);
1577        // priority (200) > max_fee (100) — broadcast should reject this at runtime
1578        assert!(args.priority_gas_price.unwrap() > args.with_gas_price.unwrap());
1579    }
1580}