Skip to main content

forge_script/
simulate.rs

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