Skip to main content

forge_script/
simulate.rs

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