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