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
46pub 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 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 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 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 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 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 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 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 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 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 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 async fn build_rpc_decoders(&self) -> Result<HashMap<String, CallTraceDecoder>> {
366 let rpcs = &self.execution_artifacts.rpc_data.total_rpcs;
367 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
624pub 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 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 let mut new_sequence = VecDeque::new();
654 let mut manager = ProvidersManager::<FEN::Network>::default();
655 let mut sequences = vec![];
656
657 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 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 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 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 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 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 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 fn create_sequence(
778 &self,
779 multi: bool,
780 chain: u64,
781 transactions: VecDeque<TransactionWithMetadata<FEN::Network>>,
782 ) -> Result<ScriptSequence<FEN::Network>> {
783 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 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 let resolved_eip1559_fees = if self.args.with_gas_price.is_none() {
865 if let Some(fees) = provider_info.eip1559_fees().copied() {
866 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 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 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 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}