Skip to main content

forge_script/
simulate.rs

1use super::{
2    multi_sequence::MultiChainSequence, providers::ProvidersManager, runner::ScriptRunner,
3    sequence::ScriptSequenceKind, transaction::ScriptTransactionBuilder,
4};
5use crate::{
6    ScriptArgs, ScriptConfig, ScriptResult,
7    broadcast::{BundledState, estimate_gas},
8    build::LinkedBuildData,
9    execute::{ExecutionArtifacts, ExecutionData, build_trace_decoder_for_context},
10    sequence::get_commit_hash,
11};
12use alloy_chains::{Chain, NamedChain};
13use alloy_evm::revm::context::Block;
14use alloy_network::TransactionBuilder;
15use alloy_primitives::{Address, U256, map::HashMap, utils::format_units};
16use alloy_provider::Provider;
17use dialoguer::Confirm;
18use eyre::{Context, Result};
19use forge_script_sequence::{ScriptSequence, TransactionWithMetadata};
20use foundry_cheatcodes::Wallets;
21use foundry_cli::utils::{has_different_gas_calc, now};
22use foundry_common::{
23    ContractData, ContractsByArtifact, provider::fee::resolve_broadcast_eip1559_fees, shell,
24    tempo::known_fee_token_symbol,
25};
26use foundry_evm::{
27    core::{FoundryBlock, evm::FoundryEvmNetwork},
28    traces::{
29        CallTraceDecoder, debug::ContractSources, decode_trace_arena, prune_trace_depth,
30        render_trace_arena_inner,
31    },
32};
33use foundry_wallets::wallet_browser::signer::BrowserSigner;
34use futures::future::join_all;
35use parking_lot::RwLock;
36use std::{
37    collections::{BTreeMap, VecDeque},
38    mem,
39    sync::Arc,
40};
41
42/// Same as [ExecutedState](crate::execute::ExecutedState), but also contains [ExecutionArtifacts]
43/// which are obtained from [ScriptResult].
44///
45/// Can be either converted directly to [BundledState] or driven to it through
46/// [FilledTransactionsState].
47pub struct PreSimulationState<FEN: FoundryEvmNetwork> {
48    pub args: ScriptArgs,
49    pub script_config: ScriptConfig<FEN>,
50    pub script_wallets: Wallets,
51    pub browser_wallet: Option<BrowserSigner<FEN::Network>>,
52    pub build_data: LinkedBuildData,
53    pub execution_data: ExecutionData,
54    pub execution_result: ScriptResult<FEN::Network>,
55    pub execution_artifacts: ExecutionArtifacts,
56}
57
58struct RpcSimulationContext<R> {
59    runner: RwLock<R>,
60    decoder: CallTraceDecoder,
61}
62
63enum RpcContexts<R> {
64    Simulation(Arc<HashMap<String, RpcSimulationContext<R>>>),
65    Decoding(HashMap<String, CallTraceDecoder>),
66}
67
68impl<R> RpcContexts<R> {
69    fn decoder(&self, rpc: &str) -> &CallTraceDecoder {
70        match self {
71            Self::Simulation(contexts) => &context_for_rpc(contexts, rpc).decoder,
72            Self::Decoding(decoders) => decoders.get(rpc).expect("invalid rpc url"),
73        }
74    }
75}
76
77fn context_for_rpc<'a, R>(
78    contexts: &'a HashMap<String, RpcSimulationContext<R>>,
79    rpc: &str,
80) -> &'a RpcSimulationContext<R> {
81    contexts.get(rpc).expect("invalid rpc url")
82}
83
84async fn build_rpc_simulation_context<FEN: FoundryEvmNetwork>(
85    rpc: String,
86    args: &ScriptArgs,
87    script_config: &ScriptConfig<FEN>,
88    known_contracts: &ContractsByArtifact,
89    sources: &ContractSources,
90    execution_result: &ScriptResult<FEN::Network>,
91) -> Result<(String, RpcSimulationContext<ScriptRunner<FEN>>)> {
92    let mut script_config = script_config.clone();
93    script_config.set_fork_url(rpc.clone());
94    let mut runner = script_config._get_runner(None, false, false).await?;
95    runner.executor.enable_block_context_progression()?;
96    let decoder = build_trace_decoder_for_context(
97        args,
98        &script_config,
99        known_contracts,
100        sources,
101        execution_result,
102        script_config.source_chain_id.map(Chain::from),
103    )?;
104    Ok((rpc, RpcSimulationContext { runner: RwLock::new(runner), decoder }))
105}
106
107async fn build_rpc_decoder<FEN: FoundryEvmNetwork>(
108    rpc: String,
109    args: &ScriptArgs,
110    script_config: &ScriptConfig<FEN>,
111    known_contracts: &ContractsByArtifact,
112    sources: &ContractSources,
113    execution_result: &ScriptResult<FEN::Network>,
114) -> Result<(String, CallTraceDecoder)> {
115    let mut script_config = script_config.clone();
116    script_config.set_fork_url(rpc.clone());
117    let _ = script_config.resolve_execution_env().await?;
118    let decoder = build_trace_decoder_for_context(
119        args,
120        &script_config,
121        known_contracts,
122        sources,
123        execution_result,
124        script_config.source_chain_id.map(Chain::from),
125    )?;
126    Ok((rpc, decoder))
127}
128
129impl<FEN: FoundryEvmNetwork> PreSimulationState<FEN> {
130    /// If simulation is enabled, simulates transactions against the fork and fills gas estimation
131    /// and execution metadata. Otherwise, fills metadata that can be derived without transaction
132    /// simulation using each RPC's resolved execution context.
133    ///
134    /// Both modes will panic if any of the transactions have None for the `rpc` field.
135    pub async fn fill_metadata(self) -> Result<FilledTransactionsState<FEN>> {
136        let address_to_abi = self.build_address_to_abi_map();
137        let contexts = if self.args.skip_simulation {
138            RpcContexts::Decoding(self.build_rpc_decoders().await?)
139        } else {
140            RpcContexts::Simulation(Arc::new(
141                self.build_runners().await?.into_iter().collect::<HashMap<_, _>>(),
142            ))
143        };
144
145        let mut transactions = self
146            .execution_result
147            .transactions
148            .clone()
149            .unwrap_or_default()
150            .into_iter()
151            .map(|tx| {
152                let rpc = tx.rpc.expect("missing broadcastable tx rpc url");
153                let sender = tx.transaction.from().expect("all transactions should have a sender");
154                let nonce = tx.transaction.nonce().expect("all transactions should have a nonce");
155                let to = tx.transaction.to();
156                let decoder = contexts.decoder(&rpc);
157
158                let mut builder = ScriptTransactionBuilder::new(tx.transaction, rpc);
159
160                if to.is_some() {
161                    builder.set_call(
162                        &address_to_abi,
163                        decoder,
164                        self.script_config.evm_opts.create2_deployer,
165                    )?;
166                } else {
167                    builder.set_create(false, sender.create(nonce), &address_to_abi)?;
168                }
169
170                Ok(builder.build())
171            })
172            .collect::<Result<VecDeque<_>>>()?;
173
174        match contexts {
175            RpcContexts::Simulation(contexts) => {
176                transactions = self.simulate_and_fill_with_contexts(transactions, contexts).await?;
177            }
178            RpcContexts::Decoding(_) => sh_println!("\nSKIPPING ON CHAIN SIMULATION.")?,
179        }
180
181        Ok(FilledTransactionsState {
182            args: self.args,
183            script_config: self.script_config,
184            script_wallets: self.script_wallets,
185            browser_wallet: self.browser_wallet,
186            build_data: self.build_data,
187            execution_artifacts: self.execution_artifacts,
188            transactions,
189        })
190    }
191
192    /// Executes every transaction in its RPC-specific simulation context and collects gas usage
193    /// and metadata.
194    async fn simulate_and_fill_with_contexts(
195        &self,
196        transactions: VecDeque<TransactionWithMetadata<FEN::Network>>,
197        contexts: Arc<HashMap<String, RpcSimulationContext<ScriptRunner<FEN>>>>,
198    ) -> Result<VecDeque<TransactionWithMetadata<FEN::Network>>> {
199        trace!(target: "script", "executing onchain simulation");
200
201        let mut final_txs = VecDeque::new();
202
203        // Executes all transactions from the different forks concurrently.
204        let futs = transactions
205            .into_iter()
206            .map(|mut transaction| async {
207                let rpc = transaction.rpc.clone();
208                let context = context_for_rpc(&contexts, &rpc);
209                let mut runner = context.runner.write();
210                let tx = transaction.tx_mut();
211
212                let to = tx.to();
213                let result = runner
214                    .simulate(
215                        tx.from()
216                            .expect("transaction doesn't have a `from` address at execution time"),
217                        to,
218                        tx.input().cloned(),
219                        tx.value(),
220                        tx.authorization_list(),
221                    )
222                    .wrap_err("Internal EVM error during simulation")?;
223
224                if !result.success {
225                    return Ok((rpc, None, false, result.traces));
226                }
227
228                // Simulate mining the transaction if the user passes `--slow`.
229                if self.args.slow {
230                    runner.executor.advance_block_context();
231                    let block_number = runner.executor.evm_env().block_env.number() + U256::from(1);
232                    runner.executor.evm_env_mut().block_env.set_number(block_number);
233                }
234
235                let is_noop_tx = if let Some(to) = to {
236                    runner.executor.is_empty_code(to)? && tx.value().unwrap_or_default().is_zero()
237                } else {
238                    false
239                };
240
241                let transaction = ScriptTransactionBuilder::from(transaction)
242                    .with_execution_result(
243                        &result,
244                        self.args.gas_estimate_multiplier,
245                        &self.build_data,
246                    )
247                    .build();
248
249                eyre::Ok((rpc, Some(transaction), is_noop_tx, result.traces))
250            })
251            .collect::<Vec<_>>();
252
253        let tracing = &self.script_config.config.tracing;
254        if !shell::is_json() && tracing.verbosity > 3 {
255            sh_println!("==========================")?;
256            sh_println!("Simulated On-chain Traces:\n")?;
257        }
258
259        let mut abort = false;
260        for res in join_all(futs).await {
261            let (rpc, tx, is_noop_tx, mut traces) = res?;
262
263            // Transaction will be `None`, if execution didn't pass.
264            if !shell::is_json() && (tx.is_none() || tracing.verbosity > 3) {
265                let decoder = &context_for_rpc(&contexts, &rpc).decoder;
266                for (_, trace) in &mut traces {
267                    decode_trace_arena(trace, decoder).await;
268                    if let Some(trace_depth) = tracing.trace_depth {
269                        prune_trace_depth(trace, trace_depth);
270                    }
271                    sh_println!(
272                        "{}",
273                        render_trace_arena_inner(trace, false, tracing.verbosity > 4)
274                    )?;
275                }
276            }
277
278            if let Some(tx) = tx {
279                if is_noop_tx {
280                    let to = tx.contract_address.unwrap();
281                    sh_warn!(
282                        "Script contains a transaction to {to} which does not contain any code."
283                    )?;
284
285                    // Only prompt if we're broadcasting and we've not disabled interactivity.
286                    if self.args.should_broadcast()
287                        && !self.args.non_interactive
288                        && !Confirm::new()
289                            .with_prompt("Do you wish to continue?".to_string())
290                            .interact()?
291                    {
292                        eyre::bail!("User canceled the script.");
293                    }
294                }
295
296                final_txs.push_back(tx);
297            } else {
298                abort = true;
299            }
300        }
301
302        if abort {
303            eyre::bail!("Simulated execution failed.");
304        }
305
306        Ok(final_txs)
307    }
308
309    /// Build mapping from contract address to its ABI, code and contract name.
310    fn build_address_to_abi_map(&self) -> BTreeMap<Address, &ContractData> {
311        self.execution_artifacts
312            .decoder
313            .contracts
314            .iter()
315            .filter_map(move |(addr, contract_id)| {
316                if let Ok(Some((_, data))) =
317                    self.build_data.known_contracts.find_by_name_or_identifier(contract_id)
318                {
319                    return Some((*addr, data));
320                }
321                None
322            })
323            .collect()
324    }
325
326    /// Build [ScriptRunner] forking given RPC for each RPC used in the script.
327    async fn build_runners(
328        &self,
329    ) -> Result<Vec<(String, RpcSimulationContext<ScriptRunner<FEN>>)>> {
330        let rpcs = &self.execution_artifacts.rpc_data.total_rpcs;
331
332        if !shell::is_json() {
333            let n = rpcs.len();
334            let s = if n == 1 { "" } else { "s" };
335            sh_println!("\n## Setting up {n} EVM{s}.")?;
336        }
337
338        // Context construction performs several identity and block probes per endpoint. Resolve
339        // endpoints serially so setup does not create an unbounded cross-endpoint request burst.
340        let mut contexts = Vec::with_capacity(rpcs.len());
341        for rpc in rpcs.iter().cloned() {
342            contexts.push(
343                build_rpc_simulation_context(
344                    rpc,
345                    &self.args,
346                    &self.script_config,
347                    &self.build_data.known_contracts,
348                    &self.build_data.sources,
349                    &self.execution_result,
350                )
351                .await?,
352            );
353        }
354        Ok(contexts)
355    }
356
357    /// Builds one trace decoder for every RPC without constructing simulation runners.
358    async fn build_rpc_decoders(&self) -> Result<HashMap<String, CallTraceDecoder>> {
359        let rpcs = &self.execution_artifacts.rpc_data.total_rpcs;
360        // Decoder construction resolves the same endpoint context as a simulation runner.
361        let mut decoders = HashMap::default();
362        for rpc in rpcs.iter().cloned() {
363            let (rpc, decoder) = build_rpc_decoder(
364                rpc,
365                &self.args,
366                &self.script_config,
367                &self.build_data.known_contracts,
368                &self.build_data.sources,
369                &self.execution_result,
370            )
371            .await?;
372            decoders.insert(rpc, decoder);
373        }
374        Ok(decoders)
375    }
376}
377
378#[cfg(all(test, feature = "monad"))]
379mod tests {
380    use super::*;
381    use alloy_primitives::address;
382    use anvil::{NodeConfig, spawn};
383    use foundry_cli::opts::TempoOpts;
384    use foundry_config::Config;
385    use foundry_evm::{
386        core::{evm::MonadEvmNetwork, opts::EvmOpts},
387        hardforks::MonadHardfork,
388    };
389    use foundry_evm_networks::NetworkConfigs;
390
391    const RESERVE_BALANCE_ADDRESS: Address = address!("0000000000000000000000000000000000001001");
392
393    #[tokio::test(flavor = "multi_thread")]
394    async fn multi_rpc_fork_selects_trace_decoder_for_source_hardfork() {
395        let (monad_eight_api, monad_eight) = spawn(
396            NodeConfig::test_monad()
397                .with_chain_id(Some(NamedChain::Monad as u64))
398                .with_hardfork(Some(MonadHardfork::MonadEight.into())),
399        )
400        .await;
401        let (monad_nine_api, monad_nine) = spawn(
402            NodeConfig::test_monad()
403                .with_chain_id(Some(NamedChain::Monad as u64))
404                .with_hardfork(Some(MonadHardfork::MonadNine.into())),
405        )
406        .await;
407        monad_eight_api.mine_one().await.unwrap();
408        monad_nine_api.mine_one().await.unwrap();
409        let monad_eight_rpc = monad_eight.http_endpoint();
410        let monad_nine_rpc = monad_nine.http_endpoint();
411
412        let mut evm_opts = EvmOpts {
413            fork_url: Some(monad_eight_rpc.clone()),
414            fork_block_number: Some(0),
415            networks: NetworkConfigs::with_monad(),
416            ..Default::default()
417        };
418        evm_opts.env.chain_id = Some(42);
419        let script_config = ScriptConfig::<MonadEvmNetwork>::new(
420            Config::default(),
421            evm_opts,
422            false,
423            TempoOpts::default(),
424            Some(0),
425        )
426        .await
427        .unwrap();
428        let args = ScriptArgs::default();
429        let known_contracts = ContractsByArtifact::default();
430        let sources = ContractSources::default();
431        let execution_result = ScriptResult::default();
432        let contexts = [
433            build_rpc_simulation_context(
434                monad_eight_rpc.clone(),
435                &args,
436                &script_config,
437                &known_contracts,
438                &sources,
439                &execution_result,
440            )
441            .await
442            .unwrap(),
443            build_rpc_simulation_context(
444                monad_nine_rpc.clone(),
445                &args,
446                &script_config,
447                &known_contracts,
448                &sources,
449                &execution_result,
450            )
451            .await
452            .unwrap(),
453        ]
454        .into_iter()
455        .collect::<HashMap<_, _>>();
456
457        let monad_eight = context_for_rpc(&contexts, &monad_eight_rpc);
458        let monad_eight_runner = monad_eight.runner.read();
459        assert_eq!(monad_eight_runner.executor.evm_env().cfg_env.chain_id, 42);
460        assert_eq!(monad_eight_runner.executor.evm_env().block_env.number(), U256::ZERO);
461        assert_eq!(monad_eight_runner.evm_opts.fork_block_number, Some(0));
462        assert!(!monad_eight_runner.evm_opts.fork_block_number_is_inferred);
463        assert_eq!(monad_eight_runner.evm_opts.networks, NetworkConfigs::with_monad());
464        assert!(!monad_eight_runner.evm_opts.fork_network_is_inferred);
465        assert_eq!(monad_eight.decoder.chain_id, Some(NamedChain::Monad as u64));
466        assert_eq!(monad_eight.decoder.monad_hardfork(), Some(MonadHardfork::MonadEight));
467        assert!(!monad_eight.decoder.precompile_labels().contains_key(&RESERVE_BALANCE_ADDRESS));
468
469        let monad_nine = context_for_rpc(&contexts, &monad_nine_rpc);
470        let monad_nine_runner = monad_nine.runner.read();
471        assert_eq!(monad_nine_runner.executor.evm_env().cfg_env.chain_id, 42);
472        assert_eq!(monad_nine_runner.executor.evm_env().block_env.number(), U256::ZERO);
473        assert_eq!(monad_nine_runner.evm_opts.fork_block_number, Some(0));
474        assert!(!monad_nine_runner.evm_opts.fork_block_number_is_inferred);
475        assert_eq!(monad_nine_runner.evm_opts.networks, NetworkConfigs::with_monad());
476        assert!(!monad_nine_runner.evm_opts.fork_network_is_inferred);
477        assert_eq!(monad_nine.decoder.chain_id, Some(NamedChain::Monad as u64));
478        assert_eq!(monad_nine.decoder.monad_hardfork(), Some(MonadHardfork::MonadNine));
479        assert_eq!(
480            monad_nine.decoder.precompile_labels().get(&RESERVE_BALANCE_ADDRESS),
481            Some(&"ReserveBalance".to_string())
482        );
483    }
484
485    #[tokio::test(flavor = "multi_thread")]
486    async fn skip_simulation_fork_builds_per_rpc_trace_decoders() {
487        let (monad_eight_api, monad_eight) = spawn(
488            NodeConfig::test_monad()
489                .with_chain_id(Some(NamedChain::Monad as u64))
490                .with_hardfork(Some(MonadHardfork::MonadEight.into())),
491        )
492        .await;
493        let (monad_nine_api, monad_nine) = spawn(
494            NodeConfig::test_monad()
495                .with_chain_id(Some(NamedChain::Monad as u64))
496                .with_hardfork(Some(MonadHardfork::MonadNine.into())),
497        )
498        .await;
499        monad_eight_api.mine_one().await.unwrap();
500        monad_nine_api.mine_one().await.unwrap();
501        let monad_eight_rpc = monad_eight.http_endpoint();
502        let monad_nine_rpc = monad_nine.http_endpoint();
503
504        let script_config = ScriptConfig::<MonadEvmNetwork>::new(
505            Config::default(),
506            EvmOpts {
507                fork_url: Some(monad_eight_rpc.clone()),
508                fork_block_number: Some(0),
509                networks: NetworkConfigs::with_monad(),
510                ..Default::default()
511            },
512            false,
513            TempoOpts::default(),
514            Some(0),
515        )
516        .await
517        .unwrap();
518        let args = ScriptArgs { skip_simulation: true, ..Default::default() };
519        let known_contracts = ContractsByArtifact::default();
520        let sources = ContractSources::default();
521        let execution_result = ScriptResult::default();
522        let decoders = [
523            build_rpc_decoder(
524                monad_eight_rpc.clone(),
525                &args,
526                &script_config,
527                &known_contracts,
528                &sources,
529                &execution_result,
530            )
531            .await
532            .unwrap(),
533            build_rpc_decoder(
534                monad_nine_rpc.clone(),
535                &args,
536                &script_config,
537                &known_contracts,
538                &sources,
539                &execution_result,
540            )
541            .await
542            .unwrap(),
543        ]
544        .into_iter()
545        .collect::<HashMap<_, _>>();
546
547        let monad_eight = decoders.get(&monad_eight_rpc).unwrap();
548        assert_eq!(monad_eight.monad_hardfork(), Some(MonadHardfork::MonadEight));
549        assert!(!monad_eight.precompile_labels().contains_key(&RESERVE_BALANCE_ADDRESS));
550
551        let monad_nine = decoders.get(&monad_nine_rpc).unwrap();
552        assert_eq!(monad_nine.monad_hardfork(), Some(MonadHardfork::MonadNine));
553        assert_eq!(
554            monad_nine.precompile_labels().get(&RESERVE_BALANCE_ADDRESS),
555            Some(&"ReserveBalance".to_string())
556        );
557    }
558
559    #[tokio::test(flavor = "multi_thread")]
560    async fn multi_rpc_fork_rejects_inferred_network_change() {
561        let (_monad_api, monad) = spawn(NodeConfig::test_monad()).await;
562        let (_ethereum_api, ethereum) = spawn(NodeConfig::test()).await;
563        let ethereum_rpc = ethereum.http_endpoint();
564        let script_config = ScriptConfig::<MonadEvmNetwork>::new(
565            Config::default(),
566            EvmOpts { fork_url: Some(monad.http_endpoint()), ..Default::default() },
567            false,
568            TempoOpts::default(),
569            Some(0),
570        )
571        .await
572        .unwrap();
573        assert!(script_config.evm_opts.fork_network_is_inferred);
574
575        let result = build_rpc_simulation_context(
576            ethereum_rpc.clone(),
577            &ScriptArgs::default(),
578            &script_config,
579            &ContractsByArtifact::default(),
580            &ContractSources::default(),
581            &ScriptResult::default(),
582        )
583        .await;
584        let Err(error) = result else { panic!("inferred cross-network fork should be rejected") };
585        assert!(
586            error
587                .to_string()
588                .contains("fork network `ethereum` is incompatible with the active EVM"),
589            "{error}"
590        );
591
592        let result = build_rpc_decoder(
593            ethereum_rpc,
594            &ScriptArgs { skip_simulation: true, ..Default::default() },
595            &script_config,
596            &ContractsByArtifact::default(),
597            &ContractSources::default(),
598            &ScriptResult::default(),
599        )
600        .await;
601        let Err(error) = result else {
602            panic!("skip-simulation decoder should reject an inferred cross-network fork")
603        };
604        assert!(
605            error
606                .to_string()
607                .contains("fork network `ethereum` is incompatible with the active EVM"),
608            "{error}"
609        );
610    }
611}
612
613/// At this point we have converted transactions collected during script execution to
614/// [TransactionWithMetadata] objects which contain additional metadata needed for broadcasting and
615/// verification.
616pub struct FilledTransactionsState<FEN: FoundryEvmNetwork> {
617    pub args: ScriptArgs,
618    pub script_config: ScriptConfig<FEN>,
619    pub script_wallets: Wallets,
620    pub browser_wallet: Option<BrowserSigner<FEN::Network>>,
621    pub build_data: LinkedBuildData,
622    pub execution_artifacts: ExecutionArtifacts,
623    pub transactions: VecDeque<TransactionWithMetadata<FEN::Network>>,
624}
625
626impl<FEN: FoundryEvmNetwork> FilledTransactionsState<FEN> {
627    /// Bundles all transactions of the [`TransactionWithMetadata`] type in a list of
628    /// [`ScriptSequence`]. List length will be higher than 1, if we're dealing with a multi
629    /// chain deployment.
630    ///
631    /// Each transaction will be added with the correct transaction type and gas estimation.
632    pub async fn bundle(mut self) -> Result<BundledState<FEN>> {
633        let is_multi_deployment = self.execution_artifacts.rpc_data.total_rpcs.len() > 1;
634
635        if is_multi_deployment && !self.build_data.libraries.is_empty() {
636            eyre::bail!("Multi-chain deployment is not supported with libraries.");
637        }
638
639        let mut total_gas_per_rpc: HashMap<String, u128> = HashMap::default();
640
641        // Batches sequence of transactions from different rpcs.
642        let mut new_sequence = VecDeque::new();
643        let mut manager = ProvidersManager::<FEN::Network>::default();
644        let mut sequences = vec![];
645
646        // Peeking is used to check if the next rpc url is different. If so, it creates a
647        // [`ScriptSequence`] from all the collected transactions up to this point.
648        let mut txes_iter = mem::take(&mut self.transactions).into_iter().peekable();
649
650        while let Some(mut tx) = txes_iter.next() {
651            let tx_rpc = tx.rpc.clone();
652            let provider_info = manager
653                .get_or_init_provider(
654                    &tx.rpc,
655                    self.execution_artifacts.rpc_data.chain_ids.get(&tx.rpc).copied(),
656                    self.args.legacy,
657                    self.script_config.config.eip1559_fee_estimate,
658                )
659                .await?;
660
661            if let Some(tx) = tx.tx_mut().as_unsigned_mut() {
662                // Handles chain specific requirements for unsigned transactions.
663                tx.set_chain_id(provider_info.chain);
664            }
665
666            if !self.args.skip_simulation {
667                let tx = tx.tx_mut();
668
669                if has_different_gas_calc(provider_info.chain) {
670                    // only estimate gas for unsigned transactions
671                    if let Some(tx) = tx.as_unsigned_mut() {
672                        trace!("estimating with different gas calculation");
673                        let gas = tx.gas_limit().expect("gas is set by simulation.");
674
675                        // We are trying to show the user an estimation of the total gas usage.
676                        //
677                        // However, some transactions might depend on previous ones. For
678                        // example, tx1 might deploy a contract that tx2 uses. That
679                        // will result in the following `estimate_gas` call to fail,
680                        // since tx1 hasn't been broadcasted yet.
681                        //
682                        // Not exiting here will not be a problem when actually broadcasting,
683                        // because for chains where `has_different_gas_calc`
684                        // returns true, we await each transaction before
685                        // broadcasting the next one.
686                        if let Err(err) = estimate_gas(
687                            tx,
688                            &provider_info.provider,
689                            self.args.gas_estimate_multiplier,
690                            false,
691                        )
692                        .await
693                        {
694                            trace!("gas estimation failed: {err}");
695
696                            // Restore gas value, since `estimate_gas` will remove it.
697                            tx.set_gas_limit(gas);
698                        }
699                    }
700                }
701
702                let total_gas = total_gas_per_rpc.entry(tx_rpc.clone()).or_insert(0);
703                *total_gas += tx.gas().expect("gas is set");
704            }
705
706            new_sequence.push_back(tx);
707            // We only create a [`ScriptSequence`] object when we collect all the rpc related
708            // transactions.
709            if let Some(next_tx) = txes_iter.peek()
710                && next_tx.rpc == tx_rpc
711            {
712                continue;
713            }
714
715            let sequence =
716                self.create_sequence(is_multi_deployment, provider_info.chain, new_sequence)?;
717
718            sequences.push(sequence);
719
720            new_sequence = VecDeque::new();
721        }
722
723        if !self.args.skip_simulation {
724            // Present gas information on a per RPC basis.
725            for (rpc, total_gas) in total_gas_per_rpc {
726                let provider_info = manager.get(&rpc).expect("provider is set.");
727
728                let token_symbol = if self.script_config.evm_opts.networks.is_tempo() {
729                    self.args.tempo.fee_token.map_or_else(
730                        || "TIP-20".to_string(),
731                        |fee_token| {
732                            known_fee_token_symbol(fee_token)
733                                .map(str::to_string)
734                                .unwrap_or_else(|| fee_token.to_string())
735                        },
736                    )
737                } else {
738                    NamedChain::try_from(provider_info.chain)
739                        .unwrap_or_default()
740                        .native_currency_symbol()
741                        .unwrap_or("ETH")
742                        .to_string()
743                };
744
745                // We don't store it in the transactions, since we want the most updated value.
746                // Right before broadcasting.
747                //
748                // Resolve the fees with the same overrides as the broadcast path so the
749                // displayed values match what is sent. Skipped when `--with-gas-price` pins
750                // the max fee directly.
751                let resolved_eip1559_fees = if self.args.with_gas_price.is_none() {
752                    if let Some(fees) = provider_info.eip1559_fees().copied() {
753                        // `--batch` broadcasts via `broadcast_batch`, which applies no
754                        // browser tip, so skip it here too. Best-effort.
755                        let browser_suggested_tip =
756                            if !self.args.batch && self.browser_wallet.is_some() {
757                                provider_info.provider.get_max_priority_fee_per_gas().await.ok()
758                            } else {
759                                None
760                            };
761                        Some(resolve_broadcast_eip1559_fees(
762                            fees,
763                            None,
764                            self.args.priority_gas_price.map(|p| p.to()),
765                            browser_suggested_tip,
766                        )?)
767                    } else {
768                        None
769                    }
770                } else {
771                    None
772                };
773
774                // `per_gas` is the legacy gas price or, for EIP-1559, the `maxFeePerGas`
775                // (a base-fee buffer plus the priority fee), which is what the transaction
776                // can pay at most -- not the spot base fee shown by block explorers.
777                let per_gas = if let Some(gas_price) = self.args.with_gas_price {
778                    gas_price.to()
779                } else if let Some(fees) = &resolved_eip1559_fees {
780                    fees.max_fee_per_gas
781                } else {
782                    provider_info.gas_price()?
783                };
784
785                // Format a wei value as a trimmed gwei string.
786                let fmt_gwei = |wei: u128| {
787                    let raw = format_units(wei, 9)
788                        .unwrap_or_else(|_| "[Could not calculate]".to_string());
789                    raw.trim_end_matches('0').trim_end_matches('.').to_string()
790                };
791
792                let estimated_gas_price = fmt_gwei(per_gas);
793
794                // (base fee, max priority fee) for the EIP-1559 breakdown.
795                let fee_breakdown = resolved_eip1559_fees.as_ref().map(|fees| {
796                    (fmt_gwei(fees.base_fee_per_gas), fmt_gwei(fees.max_priority_fee_per_gas))
797                });
798
799                let estimated_amount_raw = format_units(total_gas.saturating_mul(per_gas), 18)
800                    .unwrap_or_else(|_| "[Could not calculate]".to_string());
801                let estimated_amount = estimated_amount_raw.trim_end_matches('0');
802
803                if shell::is_json() {
804                    let mut json = serde_json::json!({
805                        "chain": provider_info.chain,
806                        "estimated_gas_price": estimated_gas_price,
807                        "estimated_total_gas_used": total_gas,
808                        "estimated_amount_required": estimated_amount,
809                        "token_symbol": token_symbol,
810                    });
811                    if let Some((base_fee, priority_fee)) = &fee_breakdown {
812                        json["estimated_max_fee_per_gas"] =
813                            serde_json::Value::from(estimated_gas_price.clone());
814                        json["estimated_base_fee_per_gas"] =
815                            serde_json::Value::from(base_fee.clone());
816                        json["estimated_max_priority_fee_per_gas"] =
817                            serde_json::Value::from(priority_fee.clone());
818                    }
819                    sh_println!("{}", json)?;
820                } else {
821                    sh_println!("\n==========================")?;
822                    sh_println!("\nChain {}", provider_info.chain)?;
823
824                    if let Some((base_fee, priority_fee)) = &fee_breakdown {
825                        sh_println!("\nEstimated max fee per gas: {estimated_gas_price} gwei")?;
826                        sh_println!("Estimated base fee per gas: {base_fee} gwei")?;
827                        sh_println!("Estimated max priority fee per gas: {priority_fee} gwei")?;
828                    } else {
829                        sh_println!("\nEstimated gas price: {estimated_gas_price} gwei")?;
830                    }
831                    sh_println!("\nEstimated total gas used for script: {total_gas}")?;
832                    sh_println!("\nEstimated amount required: {estimated_amount} {token_symbol}")?;
833                    sh_println!("\n==========================")?;
834                }
835            }
836        }
837
838        let sequence = if sequences.len() == 1 {
839            ScriptSequenceKind::Single(sequences.pop().expect("empty sequences"))
840        } else {
841            ScriptSequenceKind::Multi(MultiChainSequence::new(
842                sequences,
843                &self.args.sig,
844                &self.build_data.build_data.target,
845                &self.script_config.config,
846                !self.args.broadcast,
847            )?)
848        };
849
850        Ok(BundledState {
851            args: self.args,
852            script_config: self.script_config,
853            script_wallets: self.script_wallets,
854            browser_wallet: self.browser_wallet,
855            build_data: self.build_data,
856            sequence,
857        })
858    }
859
860    /// Creates a [ScriptSequence] object from the given transactions.
861    fn create_sequence(
862        &self,
863        multi: bool,
864        chain: u64,
865        transactions: VecDeque<TransactionWithMetadata<FEN::Network>>,
866    ) -> Result<ScriptSequence<FEN::Network>> {
867        // Paths are set to None for multi-chain sequences parts, because they don't need to be
868        // saved to a separate file.
869        let paths = if multi {
870            None
871        } else {
872            Some(ScriptSequence::<FEN::Network>::get_paths(
873                &self.script_config.config,
874                &self.args.sig,
875                &self.build_data.build_data.target,
876                chain,
877                !self.args.broadcast,
878            )?)
879        };
880
881        let commit = get_commit_hash(&self.script_config.config.root);
882
883        let local_addresses = match &self.build_data.predeploy_libraries {
884            crate::build::ScriptPredeployLibraries::Default { local, .. }
885            | crate::build::ScriptPredeployLibraries::Create2 { local, .. } => local.as_slice(),
886        };
887        let local_addresses = local_addresses
888            .iter()
889            .map(|library| library.address.to_checksum(None))
890            .collect::<Vec<_>>();
891        let libraries = self
892            .build_data
893            .libraries
894            .libs
895            .iter()
896            .flat_map(|(file, libs)| {
897                libs.iter()
898                    .filter(|(_, address)| !local_addresses.contains(address))
899                    .map(|(name, address)| format!("{}:{name}:{address}", file.to_string_lossy()))
900            })
901            .collect();
902
903        let sequence = ScriptSequence {
904            transactions,
905            returns: self.execution_artifacts.returns.clone(),
906            receipts: vec![],
907            pending: vec![],
908            paths,
909            timestamp: now().as_millis(),
910            libraries,
911            chain,
912            commit,
913        };
914        Ok(sequence)
915    }
916}