Skip to main content

forge_script/
broadcast.rs

1use std::{cmp::Ordering, num::NonZeroU64, sync::Arc, time::Duration};
2
3use crate::{
4    ScriptArgs, ScriptConfig,
5    build::LinkedBuildData,
6    progress::ScriptProgress,
7    sequence::ScriptSequenceKind,
8    session::{
9        RemainingScriptTransaction, SignerScope,
10        insert_session_access_key_for_remaining_transactions,
11        script_session_expected_sender_if_configured,
12    },
13    verify::BroadcastedState,
14};
15use alloy_chains::{Chain, NamedChain};
16use alloy_consensus::{SignableTransaction, Signed};
17use alloy_eips::eip2718::Encodable2718;
18use alloy_network::{
19    EthereumWallet, Network, NetworkTransactionBuilder, ReceiptResponse, TransactionBuilder,
20};
21use alloy_primitives::{
22    Address, TxHash, TxKind, U256, keccak256,
23    map::{AddressHashMap, AddressHashSet, HashMap},
24    utils::format_units,
25};
26use alloy_provider::{Provider, RootProvider, utils::Eip1559Estimation};
27use alloy_rpc_types::TransactionRequest;
28use alloy_signer::Signature;
29use eyre::{Context, Result, bail};
30use forge_script_sequence::ScriptSequence;
31use foundry_cheatcodes::Wallets;
32use foundry_cli::utils::{has_batch_support, has_different_gas_calc};
33use foundry_common::{
34    FoundryTransactionBuilder, TransactionMaybeSigned,
35    provider::{
36        ProviderBuilder,
37        fee::{estimate_eip1559_fees, resolve_broadcast_eip1559_fees},
38    },
39    shell,
40    tempo::{TempoSponsor, maybe_print_fee_token, resolve_and_set_fee_token},
41};
42use foundry_config::Config;
43use foundry_evm::core::{
44    constants::DEFAULT_CREATE2_DEPLOYER_CODEHASH,
45    evm::{FoundryEvmNetwork, TempoEvmNetwork},
46    fork::ResolvedFork,
47    opts::EvmOpts,
48};
49use foundry_wallets::{TempoAccountsWallet, wallet_browser::signer::BrowserSigner};
50use futures::{FutureExt, StreamExt, future::join_all, stream::FuturesUnordered};
51use itertools::Itertools;
52use revm_inspectors::tracing::types::CallKind;
53use tempo_alloy::{TempoNetwork, rpc::TempoTransactionRequest};
54use tempo_primitives::transaction::Call;
55
56/// Represents how to send a single transaction.
57#[derive(Clone)]
58pub enum SendTransactionKind<'a, N: Network> {
59    Unlocked(N::TransactionRequest),
60    Raw(N::TransactionRequest, &'a EthereumWallet),
61    Browser(N::TransactionRequest, &'a BrowserSigner<N>),
62    Signed(N::TxEnvelope),
63    AccessKey(N::TransactionRequest, Box<TempoAccountsWallet>),
64}
65
66impl<'a, N: Network> SendTransactionKind<'a, N>
67where
68    N::TxEnvelope: From<Signed<N::UnsignedTx>>,
69    N::UnsignedTx: SignableTransaction<Signature>,
70    N::TransactionRequest: FoundryTransactionBuilder<N>,
71{
72    /// Prepares the transaction for broadcasting by synchronizing nonce and estimating gas.
73    ///
74    /// This method performs two key operations:
75    /// 1. Nonce synchronization: Waits for the provider's nonce to catch up to the expected
76    ///    transaction nonce when doing sequential broadcast
77    /// 2. Gas estimation: Re-estimates gas right before broadcasting for chains that require it
78    #[allow(clippy::too_many_arguments)]
79    pub async fn prepare(
80        &mut self,
81        provider: &RootProvider<N>,
82        sequential_broadcast: bool,
83        is_fixed_gas_limit: bool,
84        estimate_via_rpc: bool,
85        estimate_multiplier: u64,
86        tempo_sponsor: Option<&TempoSponsor>,
87        chain: Option<Chain>,
88    ) -> Result<()> {
89        let tempo_browser = matches!(self, Self::Browser(..)) && chain.is_some_and(Chain::is_tempo);
90        let (tx, tempo_wallet) = match self {
91            Self::Raw(tx, _) | Self::Unlocked(tx) | Self::Browser(tx, _) => (tx, None),
92            Self::AccessKey(tx, wallet) => (tx, Some(wallet)),
93            Self::Signed(_) => return Ok(()),
94        };
95
96        reject_access_key_create::<N>(tx, tempo_wallet.is_some())?;
97
98        if sequential_broadcast {
99            let from = tx.from().expect("no sender");
100
101            let tx_nonce = tx.nonce().expect("no nonce");
102            for attempt in 0..5 {
103                let nonce = provider.get_transaction_count(from).await?;
104                match nonce.cmp(&tx_nonce) {
105                    Ordering::Greater => {
106                        bail!(
107                            "EOA nonce changed unexpectedly while sending transactions. Expected {tx_nonce} got {nonce} from provider."
108                        );
109                    }
110                    Ordering::Less => {
111                        if attempt == 4 {
112                            bail!(
113                                "After 5 attempts, provider nonce ({nonce}) is still behind expected nonce ({tx_nonce})."
114                            );
115                        }
116                        warn!(
117                            "Expected nonce ({tx_nonce}) is ahead of provider nonce ({nonce}). Retrying in 1 second..."
118                        );
119                        tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
120                    }
121                    Ordering::Equal => {
122                        // Nonces are equal, we can proceed.
123                        break;
124                    }
125                }
126            }
127        }
128
129        if let Some(wallet) = tempo_wallet {
130            **wallet = tx.prepare_with_tempo_wallet(provider, wallet).await?;
131        }
132
133        let fee_token = if let Some(sponsor) = tempo_sponsor {
134            sponsor.resolve_and_set_fee_token(Some(provider), chain, tx).await?;
135            None
136        } else {
137            resolve_and_set_fee_token(Some(provider), chain, tx, tx.from()).await?
138        };
139
140        // A fee token, sponsor, validity window, or other Tempo field selects
141        // the AA transaction type. AA requests carry CREATE as their first
142        // call rather than as the Ethereum transaction `to` field.
143        convert_tempo_aa_create::<N>(tx);
144
145        // Chains which use `eth_estimateGas` are being sent sequentially and require their
146        // gas to be re-estimated right before broadcasting.
147        if !is_fixed_gas_limit && estimate_via_rpc {
148            estimate_gas(tx, provider, estimate_multiplier, tempo_browser).await?;
149        }
150
151        if let Some(sponsor) = tempo_sponsor {
152            let from = tx.from().expect("no sender");
153            sponsor.attach_and_print::<N>(tx, from).await?;
154        } else {
155            maybe_print_fee_token(Some(provider), fee_token).await?;
156        }
157
158        Ok(())
159    }
160
161    /// Sends the transaction to the network.
162    ///
163    /// Depending on the transaction kind, this will either:
164    /// - Submit via `eth_sendTransaction` for unlocked accounts
165    /// - Sign and submit via `eth_sendRawTransaction` for raw transactions
166    /// - Submit pre-signed transaction via `eth_sendRawTransaction`
167    pub async fn send(self, provider: Arc<RootProvider<N>>) -> Result<TxHash> {
168        match self {
169            Self::Unlocked(tx) => {
170                debug!("sending transaction from unlocked account {:?}", tx);
171
172                // Submit the transaction
173                let pending = provider.send_transaction(tx).await?;
174                Ok(*pending.tx_hash())
175            }
176            Self::Raw(tx, signer) => {
177                debug!("sending transaction: {:?}", tx);
178                let signed = tx.build(signer).await?;
179
180                // Submit the raw transaction
181                let pending = provider.send_raw_transaction(signed.encoded_2718().as_ref()).await?;
182                Ok(*pending.tx_hash())
183            }
184            Self::Signed(tx) => {
185                debug!("sending transaction: {:?}", tx);
186                let pending = provider.send_raw_transaction(tx.encoded_2718().as_ref()).await?;
187                Ok(*pending.tx_hash())
188            }
189            Self::Browser(tx, signer) => {
190                debug!("sending transaction: {:?}", tx);
191
192                // Sign and send the transaction via the browser wallet
193                Ok(signer.send_transaction_via_browser(tx).await?)
194            }
195            Self::AccessKey(tx, wallet) => {
196                debug!("sending transaction via tempo access key: {:?}", tx);
197
198                let raw_tx = tx.sign_with_tempo_wallet(&wallet).await?;
199
200                let pending = provider.send_raw_transaction(&raw_tx).await?;
201                Ok(*pending.tx_hash())
202            }
203        }
204    }
205
206    /// Prepares and sends the transaction in one operation.
207    ///
208    /// This is a convenience method that combines [`prepare`](Self::prepare) and
209    /// [`send`](Self::send) into a single call.
210    #[allow(clippy::too_many_arguments)]
211    pub async fn prepare_and_send(
212        mut self,
213        provider: Arc<RootProvider<N>>,
214        sequential_broadcast: bool,
215        is_fixed_gas_limit: bool,
216        estimate_via_rpc: bool,
217        estimate_multiplier: u64,
218        tempo_sponsor: Option<&TempoSponsor>,
219        chain: Option<Chain>,
220    ) -> Result<TxHash> {
221        self.prepare(
222            &provider,
223            sequential_broadcast,
224            is_fixed_gas_limit,
225            estimate_via_rpc,
226            estimate_multiplier,
227            tempo_sponsor,
228            chain,
229        )
230        .await?;
231
232        self.send(provider).await
233    }
234}
235
236pub(crate) fn remaining_unsigned_transactions<N: Network>(
237    sequences: &[ScriptSequence<N>],
238) -> impl Iterator<Item = RemainingScriptTransaction> + '_ {
239    sequences.iter().flat_map(|sequence| {
240        remaining_transactions(sequence).filter(|tx| tx.is_unsigned()).map(|tx| {
241            RemainingScriptTransaction {
242                chain: sequence.chain,
243                from: tx.from().expect("missing from"),
244            }
245        })
246    })
247}
248
249fn remaining_transaction_start<N: Network>(sequence: &ScriptSequence<N>) -> usize {
250    sequence.receipts.len().min(sequence.transactions.len())
251}
252
253fn remaining_transactions<N: Network>(
254    sequence: &ScriptSequence<N>,
255) -> impl Iterator<Item = &TransactionMaybeSigned<N>> + '_ {
256    sequence.transactions().skip(remaining_transaction_start(sequence))
257}
258
259/// Represents how to send _all_ transactions
260pub enum SendTransactionsKind<N: Network> {
261    /// Send via `eth_sendTransaction` and rely on the  `from` address being unlocked.
262    Unlocked(AddressHashSet),
263    /// Send a signed transaction via `eth_sendRawTransaction`, or via browser
264    Raw {
265        eth_wallets: AddressHashMap<EthereumWallet>,
266        browser: Option<BrowserSigner<N>>,
267        access_keys: HashMap<SignerScope, TempoAccountsWallet>,
268    },
269}
270
271impl<N: Network> SendTransactionsKind<N> {
272    /// Returns the [`SendTransactionKind`] for the given address
273    ///
274    /// Returns an error if no matching signer is found or the address is not unlocked
275    pub fn for_sender(
276        &self,
277        chain: u64,
278        addr: &Address,
279        tx: N::TransactionRequest,
280    ) -> Result<SendTransactionKind<'_, N>> {
281        match self {
282            Self::Unlocked(unlocked) => {
283                if !unlocked.contains(addr) {
284                    bail!("Sender address {:?} is not unlocked", addr);
285                }
286                Ok(SendTransactionKind::Unlocked(tx))
287            }
288            Self::Raw { eth_wallets, browser, access_keys } => {
289                if let Some(wallet) = access_keys.get(&SignerScope::new(chain, *addr)) {
290                    Ok(SendTransactionKind::AccessKey(tx, Box::new(wallet.clone())))
291                } else if let Some(wallet) = eth_wallets.get(addr) {
292                    Ok(SendTransactionKind::Raw(tx, wallet))
293                } else if let Some(b) = browser
294                    && b.address() == *addr
295                {
296                    Ok(SendTransactionKind::Browser(tx, b))
297                } else {
298                    bail!("No matching signer for {:?} found", addr);
299                }
300            }
301        }
302    }
303}
304
305/// State after we have bundled all
306/// [`TransactionWithMetadata`](forge_script_sequence::TransactionWithMetadata) objects into a
307/// single [`ScriptSequenceKind`] object containing one or more script sequences.
308pub struct BundledState<FEN: FoundryEvmNetwork> {
309    pub args: ScriptArgs,
310    pub script_config: ScriptConfig<FEN>,
311    pub script_wallets: Wallets,
312    pub browser_wallet: Option<BrowserSigner<FEN::Network>>,
313    pub build_data: LinkedBuildData,
314    pub sequence: ScriptSequenceKind<FEN::Network>,
315}
316
317impl<FEN: FoundryEvmNetwork> BundledState<FEN> {
318    pub async fn wait_for_pending(mut self) -> Result<Self> {
319        let progress = ScriptProgress::default();
320        let progress_ref = &progress;
321        let config = &self.script_config.config;
322        let futs = self
323            .sequence
324            .sequences_mut()
325            .iter_mut()
326            .enumerate()
327            .map(|(sequence_idx, sequence)| async move {
328                let rpc_url = sequence.rpc_url();
329                let provider =
330                    Arc::new(ProviderBuilder::from_config_with_url(config, rpc_url)?.build()?);
331                progress_ref
332                    .wait_for_pending(
333                        sequence_idx,
334                        sequence,
335                        &provider,
336                        self.script_config.config.transaction_timeout,
337                        self.args.confirmations,
338                    )
339                    .await
340            })
341            .collect::<Vec<_>>();
342
343        let errors = join_all(futs).await.into_iter().filter_map(Result::err).collect::<Vec<_>>();
344
345        self.sequence.save(true, false)?;
346
347        if !errors.is_empty() {
348            return Err(eyre::eyre!("{}", errors.iter().format("\n")));
349        }
350
351        Ok(self)
352    }
353
354    /// Broadcasts transactions from all sequences.
355    pub async fn broadcast(mut self) -> Result<BroadcastedState<FEN>> {
356        let remaining_transactions =
357            remaining_unsigned_transactions(self.sequence.sequences()).collect::<Vec<_>>();
358        let required_addresses =
359            remaining_transactions.iter().map(|tx| tx.from).collect::<AddressHashSet>();
360
361        if required_addresses.contains(&Config::DEFAULT_SENDER) {
362            eyre::bail!(
363                "You seem to be using Foundry's default sender. Be sure to set your own --sender."
364            );
365        }
366
367        let send_kind = if self.args.unlocked {
368            SendTransactionsKind::Unlocked(required_addresses.clone())
369        } else {
370            let expected_session_sender = script_session_expected_sender_if_configured(
371                &self.script_config.tempo,
372                &required_addresses,
373            )?;
374
375            // For addresses without an explicit signer, try the Tempo Accounts store.
376            let mut access_keys: HashMap<SignerScope, TempoAccountsWallet> = HashMap::default();
377            if let Some(expected_session_sender) = expected_session_sender
378                && let Some(session) =
379                    self.script_config.tempo.session_signer_for_multi_wallet_any_chain(
380                        &self.args.wallets,
381                        Some(expected_session_sender),
382                    )?
383            {
384                insert_session_access_key_for_remaining_transactions(
385                    &mut access_keys,
386                    session,
387                    &remaining_transactions,
388                )?;
389            }
390
391            let signers: Vec<Address> = self
392                .script_wallets
393                .signers()
394                .map_err(|e| eyre::eyre!("{e}"))?
395                .into_iter()
396                .chain(self.browser_wallet.as_ref().map(|b| b.address()))
397                .collect();
398
399            let mut missing_addresses = Vec::new();
400            let accounts_wallet = self
401                .script_config
402                .evm_opts
403                .networks
404                .is_tempo()
405                .then(TempoAccountsWallet::try_from_default_store)
406                .transpose()?
407                .flatten();
408
409            for tx in &remaining_transactions {
410                let scope = tx.scope();
411                if !signers.contains(&tx.from) && !access_keys.contains_key(&scope) {
412                    if let Some(wallet) = accounts_wallet.as_ref()
413                        && wallet.has_account(tx.from)?
414                    {
415                        access_keys.insert(scope, wallet.clone().with_chain_id(tx.chain));
416                    } else {
417                        missing_addresses.push(tx.from);
418                    }
419                }
420            }
421
422            missing_addresses.sort_unstable();
423            missing_addresses.dedup();
424
425            if !missing_addresses.is_empty() {
426                eyre::bail!(
427                    "No associated wallet for addresses: {:?}. Unlocked wallets: {:?}",
428                    missing_addresses,
429                    signers
430                );
431            }
432
433            let signers = self.script_wallets.into_multi_wallet().into_signers()?;
434            let eth_wallets: AddressHashMap<EthereumWallet> =
435                signers.into_iter().map(|(addr, signer)| (addr, signer.into())).collect();
436
437            SendTransactionsKind::Raw { eth_wallets, browser: self.browser_wallet, access_keys }
438        };
439
440        let tempo_sponsor = self.script_config.tempo.sponsor_config().await?.map(Arc::new);
441        if tempo_sponsor.is_some()
442            && self.script_config.tempo.sponsor_sig.is_some()
443            && remaining_transactions.len() > 1
444        {
445            eyre::bail!(
446                "--tempo.sponsor-sig can only sponsor one remaining script transaction; use --tempo.sponsor-signer for multi-transaction scripts"
447            );
448        }
449
450        let progress = ScriptProgress::default();
451
452        for i in 0..self.sequence.sequences().len() {
453            let mut sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
454
455            let provider = Arc::new(
456                ProviderBuilder::from_config_with_url(
457                    &self.script_config.config,
458                    sequence.rpc_url(),
459                )?
460                .build()?,
461            );
462            let already_broadcasted = sequence.receipts.len();
463
464            let seq_progress = progress.get_sequence_progress(i, sequence);
465
466            if already_broadcasted < sequence.transactions.len() {
467                let is_legacy = Chain::from(sequence.chain).is_legacy() || self.args.legacy;
468                // Make a one-time gas price estimation
469                let (gas_price, eip1559_fees) = match (
470                    is_legacy,
471                    self.args.with_gas_price,
472                    self.args.priority_gas_price,
473                ) {
474                    (true, Some(gas_price), _) => (Some(gas_price.to()), None),
475                    (true, None, _) => (Some(provider.get_gas_price().await?), None),
476                    (false, Some(max_fee_per_gas), Some(max_priority_fee_per_gas)) => {
477                        let max_fee: u128 = max_fee_per_gas.to();
478                        let max_priority: u128 = max_priority_fee_per_gas.to();
479                        if max_priority > max_fee {
480                            eyre::bail!(
481                                "--priority-gas-price ({max_priority}) cannot be higher than --with-gas-price ({max_fee})"
482                            );
483                        }
484                        (
485                            None,
486                            Some(Eip1559Estimation {
487                                max_fee_per_gas: max_fee,
488                                max_priority_fee_per_gas: max_priority,
489                            }),
490                        )
491                    }
492                    (false, _, _) => {
493                        let fees = estimate_eip1559_fees(
494                            &provider,
495                            self.script_config.config.eip1559_fee_estimate,
496                        )
497                        .await
498                        .wrap_err("Failed to estimate EIP1559 fees. This chain might not support EIP1559, try adding --legacy to your command.")?;
499
500                        // Browser wallets may suggest their own tip; query it best-effort.
501                        let browser_suggested_tip = if matches!(
502                            &send_kind,
503                            SendTransactionsKind::Raw { browser: Some(_), .. }
504                        ) {
505                            provider.get_max_priority_fee_per_gas().await.ok()
506                        } else {
507                            None
508                        };
509
510                        let fees = resolve_broadcast_eip1559_fees(
511                            fees,
512                            self.args.with_gas_price.map(|p| p.to()),
513                            self.args.priority_gas_price.map(|p| p.to()),
514                            browser_suggested_tip,
515                        )?;
516
517                        (None, Some(fees.estimation()))
518                    }
519                };
520
521                // Iterate through transactions, matching the `from` field with the associated
522                // wallet. Then send the transaction. Panics if we find a unknown `from`
523                let sequence_chain = sequence.chain;
524                let mut transactions = Vec::with_capacity(
525                    sequence.transactions.len().saturating_sub(already_broadcasted),
526                );
527                for tx_with_metadata in sequence.transactions.iter().skip(already_broadcasted) {
528                    let is_fixed_gas_limit = tx_with_metadata.is_fixed_gas_limit;
529
530                    let kind = match tx_with_metadata.tx().clone() {
531                        TransactionMaybeSigned::Signed { tx, .. } => {
532                            if tempo_sponsor.is_some() {
533                                eyre::bail!(
534                                    "cannot attach Tempo sponsor signature to an already signed script transaction"
535                                );
536                            }
537                            SendTransactionKind::Signed(tx)
538                        }
539                        TransactionMaybeSigned::Unsigned(mut tx) => {
540                            let from = tx.from().expect("No sender for onchain transaction!");
541
542                            tx.set_chain_id(sequence_chain);
543
544                            // Set TxKind::Create explicitly to satisfy `check_reqd_fields` in
545                            // alloy
546                            if tx.kind().is_none() {
547                                tx.set_create();
548                            }
549
550                            if let Some(gas_price) = gas_price {
551                                tx.set_gas_price(gas_price);
552                            } else {
553                                let eip1559_fees = eip1559_fees.expect("was set above");
554                                tx.set_max_priority_fee_per_gas(
555                                    eip1559_fees.max_priority_fee_per_gas,
556                                );
557                                tx.set_max_fee_per_gas(eip1559_fees.max_fee_per_gas);
558                            }
559
560                            self.script_config.tempo.apply::<FEN::Network>(&mut tx, None);
561
562                            send_kind.for_sender(sequence_chain, &from, tx)?
563                        }
564                    };
565
566                    transactions.push((kind, is_fixed_gas_limit));
567                }
568
569                let estimate_via_rpc = has_different_gas_calc(sequence.chain)
570                    || self.script_config.evm_opts.networks.is_tempo()
571                    || self.args.skip_simulation;
572
573                // We only wait for a transaction receipt before sending the next transaction, if
574                // there is more than one signer. There would be no way of assuring
575                // their order otherwise.
576                // Or if the chain does not support batched transactions (eg. Arbitrum).
577                // Or if we need to invoke eth_estimateGas before sending transactions.
578                let sequential_broadcast = estimate_via_rpc
579                    || self.args.slow
580                    || required_addresses.len() != 1
581                    || !has_batch_support(sequence.chain);
582
583                // We send transactions and wait for receipts in batches of 100, since some networks
584                // cannot handle more than that.
585                let batch_size = if sequential_broadcast { 1 } else { 100 };
586                let sequence_chain = sequence.chain;
587
588                for (batch_number, batch) in transactions.chunks(batch_size).enumerate() {
589                    seq_progress.inner.write().set_status(&format!(
590                        "Sending transactions [{} - {}]",
591                        batch_number * batch_size,
592                        batch_number * batch_size + std::cmp::min(batch_size, batch.len()) - 1
593                    ));
594
595                    if !batch.is_empty() {
596                        let pending_transactions = batch.iter().enumerate().map(
597                            |(position, (kind, is_fixed_gas_limit))| {
598                                let index =
599                                    already_broadcasted + batch_number * batch_size + position;
600                                let provider = provider.clone();
601                                let tempo_sponsor = tempo_sponsor.clone();
602                                async move {
603                                    let res = kind
604                                        .clone()
605                                        .prepare_and_send(
606                                            provider,
607                                            sequential_broadcast,
608                                            *is_fixed_gas_limit,
609                                            estimate_via_rpc,
610                                            self.args.gas_estimate_multiplier,
611                                            tempo_sponsor.as_deref(),
612                                            Some(sequence_chain.into()),
613                                        )
614                                        .await;
615                                    (res, kind, *is_fixed_gas_limit, 0, None, index)
616                                }
617                                .boxed()
618                            },
619                        );
620
621                        let mut buffer = pending_transactions.collect::<FuturesUnordered<_>>();
622
623                        'send: while let Some((
624                            res,
625                            kind,
626                            is_fixed_gas_limit,
627                            attempt,
628                            original_res,
629                            index,
630                        )) = buffer.next().await
631                        {
632                            if res.is_err()
633                                && self.script_config.tempo.sponsor_sig.is_some()
634                                && attempt == 0
635                            {
636                                debug!(
637                                    "not retrying transaction because --tempo.sponsor-sig is a static signature"
638                                );
639                            } else if res.is_err() && attempt <= 3 {
640                                // Try to resubmit the transaction
641                                let provider = provider.clone();
642                                let progress = seq_progress.inner.clone();
643                                let tempo_sponsor = tempo_sponsor.clone();
644                                buffer.push(Box::pin(async move {
645                                    debug!(err=?res, ?attempt, "retrying transaction ");
646                                    let attempt = attempt + 1;
647                                    progress.write().set_status(&format!(
648                                        "retrying transaction {res:?} (attempt {attempt})"
649                                    ));
650                                    tokio::time::sleep(Duration::from_millis(1000 * attempt)).await;
651                                    let r = kind
652                                        .clone()
653                                        .prepare_and_send(
654                                            provider,
655                                            sequential_broadcast,
656                                            is_fixed_gas_limit,
657                                            estimate_via_rpc,
658                                            self.args.gas_estimate_multiplier,
659                                            tempo_sponsor.as_deref(),
660                                            Some(sequence_chain.into()),
661                                        )
662                                        .await;
663                                    (
664                                        r,
665                                        kind,
666                                        is_fixed_gas_limit,
667                                        attempt,
668                                        original_res.or(Some(res)),
669                                        index,
670                                    )
671                                }));
672
673                                continue 'send;
674                            }
675
676                            // Preserve the original error if any
677                            let tx_hash = res.wrap_err_with(|| {
678                                if let Some(original_res) = original_res {
679                                    format!(
680                                        "Failed to send transaction after {attempt} attempts {original_res:?}"
681                                    )
682                                } else {
683                                    "Failed to send transaction".to_string()
684                                }
685                            })?;
686                            sequence.add_pending(index, tx_hash);
687
688                            // Checkpoint save
689                            self.sequence.save(true, false)?;
690                            sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
691
692                            seq_progress.inner.write().tx_sent(tx_hash);
693                        }
694
695                        // Checkpoint save
696                        self.sequence.save(true, false)?;
697                        sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
698
699                        progress
700                            .wait_for_pending(
701                                i,
702                                sequence,
703                                &provider,
704                                self.script_config.config.transaction_timeout,
705                                self.args.confirmations,
706                            )
707                            .await?
708                    }
709                    // Checkpoint save
710                    self.sequence.save(true, false)?;
711                    sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
712                }
713            }
714
715            let (total_gas, total_gas_price, total_paid) =
716                sequence.receipts.iter().fold((0, 0, 0), |acc, receipt| {
717                    let gas_used = receipt.gas_used();
718                    let gas_price = receipt.effective_gas_price() as u64;
719                    (acc.0 + gas_used, acc.1 + gas_price, acc.2 + gas_used * gas_price)
720                });
721            let paid = format_units(total_paid, 18).unwrap_or_else(|_| "N/A".to_string());
722            let avg_gas_price = total_gas_price
723                .checked_div(sequence.receipts.len() as u64)
724                .and_then(|avg| format_units(avg, 9).ok())
725                .unwrap_or_else(|| "N/A".to_string());
726
727            let token_symbol = NamedChain::try_from(sequence.chain)
728                .unwrap_or_default()
729                .native_currency_symbol()
730                .unwrap_or("ETH");
731            seq_progress.inner.write().set_status(&format!(
732                "Total Paid: {} {} ({} gas * avg {} gwei)\n",
733                paid.trim_end_matches('0'),
734                token_symbol,
735                total_gas,
736                avg_gas_price.trim_end_matches('0').trim_end_matches('.')
737            ));
738            seq_progress.inner.write().finish();
739        }
740
741        if !shell::is_json() {
742            sh_println!("\n\n==========================")?;
743            sh_println!("\nONCHAIN EXECUTION COMPLETE & SUCCESSFUL.")?;
744        }
745
746        Ok(BroadcastedState {
747            args: self.args,
748            script_config: self.script_config,
749            build_data: self.build_data,
750            sequence: self.sequence,
751        })
752    }
753
754    pub async fn verify_preflight_check(&self) -> Result<()> {
755        if self.args.verify_external && self.script_config.config.offline {
756            bail!("External contract verification is unavailable in offline mode");
757        }
758
759        for sequence in self.sequence.sequences() {
760            let chain: Chain = sequence.chain.into();
761            // Resolve the API key: CLI arg first, then per-chain config, then global fallback.
762            let etherscan_key = self
763                .script_config
764                .config
765                .get_etherscan_api_key(Some(chain))
766                .or_else(|| self.script_config.config.etherscan_api_key.clone());
767            let api_key =
768                self.args.verifier.resolve_api_key(etherscan_key.as_deref()).map(str::to_owned);
769            let has_url = self.args.verifier.verifier_url.is_some();
770            let is_explicit = self.args.verifier.is_explicitly_set();
771            // Presence check: use the fully-resolved provider type so that implicit Etherscan
772            // selection (key from env/config, no explicit --verifier flag) is validated too.
773            self.args
774                .verifier
775                .resolve(api_key.as_deref(), Some(chain))
776                .client(api_key.as_deref(), Some(chain), has_url, is_explicit)
777                .wrap_err_with(|| {
778                    format!("Verification preflight check failed for chain {}", sequence.chain)
779                })?;
780            // Connectivity check: validates credentials are actually accepted by the verifier.
781            self.args
782                .verifier
783                .check_credentials(api_key.as_deref(), chain, &self.script_config.config)
784                .await
785                .wrap_err_with(|| {
786                    format!("Verification preflight check failed for chain {}", sequence.chain)
787                })?;
788        }
789
790        Ok(())
791    }
792}
793
794impl BundledState<TempoEvmNetwork> {
795    /// Broadcasts all transactions as a single Tempo batch transaction (type 0x76).
796    ///
797    /// This method collects all individual transactions from the script and combines them
798    /// into a single batch transaction for atomic execution on Tempo.
799    pub async fn broadcast_batch(mut self) -> Result<BroadcastedState<TempoEvmNetwork>> {
800        // Batch mode only supports single chain for now
801        if self.sequence.sequences().len() != 1 {
802            bail!(
803                "--batch mode only supports single-chain scripts. \
804                 Use --multi without --batch for multi-chain."
805            );
806        }
807
808        let sequence = self.sequence.sequences_mut().get_mut(0).unwrap();
809        let total_transactions = sequence.transactions.len();
810        let remaining_start = remaining_transaction_start(sequence);
811
812        if remaining_start == total_transactions {
813            sh_println!("No transactions to broadcast in batch mode.")?;
814            return Ok(BroadcastedState {
815                args: self.args,
816                script_config: self.script_config,
817                build_data: self.build_data,
818                sequence: self.sequence,
819            });
820        }
821
822        // Reject pre-signed transactions: a batch is a single atomic tx from one sender,
823        // so any tx already signed by another key would silently be re-attributed.
824        if let Some((idx, _)) =
825            sequence.transactions().enumerate().find(|(_, tx)| !tx.is_unsigned())
826        {
827            bail!(
828                "--batch cannot include pre-signed transactions (found at position {}); \
829                 batch mode signs a single atomic transaction from one sender.",
830                idx + 1
831            );
832        }
833
834        // Collect sender addresses - batch mode requires single sender
835        let senders: AddressHashSet = remaining_transactions(sequence)
836            .filter(|tx| tx.is_unsigned())
837            .filter_map(|tx| tx.from())
838            .collect();
839
840        if senders.len() != 1 {
841            bail!(
842                "--batch mode requires all transactions to have the same sender. \
843                 Found {} unique senders: {:?}",
844                senders.len(),
845                senders
846            );
847        }
848
849        let sender = *senders.iter().next().unwrap();
850        let chain_id = sequence.chain;
851
852        if sender == Config::DEFAULT_SENDER {
853            bail!(
854                "You seem to be using Foundry's default sender. Be sure to set your own --sender."
855            );
856        }
857
858        let provider = Arc::new(
859            ProviderBuilder::<TempoNetwork>::from_config_with_url(
860                &self.script_config.config,
861                sequence.rpc_url(),
862            )?
863            .build()?,
864        );
865
866        // Resume detection happens before signer resolution, gas estimation, and sponsor attachment
867        // so that recovering an already-submitted batch tx never requires the original
868        // signer/sponsor or a fresh estimate.
869        //
870        // If the hash is found in the stamped transactions but a receipt cannot be obtained within
871        // the timeout, the tx is assumed dropped. We clear the stamped hashes so that a subsequent
872        // --resume will re-send a replacement instead of waiting on a dead hash.
873        let pending_batch_hash: Option<TxHash> =
874            sequence.transactions.iter().skip(remaining_start).find_map(|tx| tx.hash);
875
876        if let Some(tx_hash) = pending_batch_hash {
877            sh_println!(
878                "Resuming batch: tx {tx_hash:#x} already submitted, waiting for receipt..."
879            )?;
880
881            let timeout = self.script_config.config.transaction_timeout;
882            let receipt_result = tokio::time::timeout(
883                Duration::from_secs(timeout),
884                wait_for_batch_receipt(provider.as_ref(), tx_hash, self.args.confirmations),
885            )
886            .await;
887
888            match receipt_result {
889                Ok(Ok(Some(receipt))) => {
890                    // Tx confirmed, process receipt and return without touching signer/sponsor.
891                    let success = receipt.status();
892                    if success {
893                        sh_println!(
894                            "Batch transaction confirmed in block {}",
895                            receipt.block_number.unwrap_or(0)
896                        )?;
897                    } else {
898                        bail!("Batch transaction failed (reverted)");
899                    }
900
901                    let sequence = self.sequence.sequences_mut().get_mut(0).unwrap();
902                    let remaining_len = sequence.transactions.len() - remaining_start;
903                    let per_tx_addresses: Vec<Option<Address>> = sequence
904                        .transactions
905                        .iter()
906                        .skip(remaining_start)
907                        .map(|tx| match tx.call_kind {
908                            CallKind::Create | CallKind::Create2 => tx.contract_address,
909                            _ => None,
910                        })
911                        .collect();
912
913                    for (idx, addr) in per_tx_addresses.iter().enumerate() {
914                        if let Some(addr) = addr {
915                            sh_println!("  call[{idx}] deployed at: {addr:#x}")?;
916                        }
917                    }
918
919                    for addr in &per_tx_addresses {
920                        let mut tx_receipt = receipt.clone();
921                        tx_receipt.contract_address = *addr;
922                        sequence.receipts.push(tx_receipt);
923                    }
924                    // Clear the pending entry now that we have a receipt.
925                    sequence.remove_pending(tx_hash);
926
927                    let chain = sequence.chain;
928                    let _ = sequence;
929                    self.sequence.save(true, false)?;
930
931                    let total_gas = receipt.gas_used();
932                    let gas_price = receipt.effective_gas_price() as u64;
933                    let total_paid = total_gas * gas_price;
934                    let paid = format_units(total_paid, 18).unwrap_or_else(|_| "N/A".to_string());
935                    let gas_price_gwei =
936                        format_units(gas_price, 9).unwrap_or_else(|_| "N/A".to_string());
937                    let token_symbol = NamedChain::try_from(chain)
938                        .unwrap_or_default()
939                        .native_currency_symbol()
940                        .unwrap_or("ETH");
941                    sh_println!(
942                        "\nTotal Paid: {} {} ({} gas * {} gwei)\n(resumed from previous run, {} tx(s))",
943                        paid.trim_end_matches('0'),
944                        token_symbol,
945                        total_gas,
946                        gas_price_gwei.trim_end_matches('0').trim_end_matches('.'),
947                        remaining_len,
948                    )?;
949
950                    if !shell::is_json() {
951                        sh_println!("\n\n==========================")?;
952                        sh_println!("\nBATCH EXECUTION COMPLETE & SUCCESSFUL.")?;
953                        sh_println!(
954                            "All {} calls executed atomically in a single transaction.",
955                            remaining_len
956                        )?;
957                    }
958
959                    return Ok(BroadcastedState {
960                        args: self.args,
961                        script_config: self.script_config,
962                        build_data: self.build_data,
963                        sequence: self.sequence,
964                    });
965                }
966                Ok(Ok(None)) => {
967                    // Dropped from mempool, clear stamped hashes so the next --resume re-sends.
968                    sh_println!(
969                        "Batch tx {tx_hash:#x} was dropped from the mempool; will re-send..."
970                    )?;
971                    let sequence = self.sequence.sequences_mut().get_mut(0).unwrap();
972                    sequence.remove_pending(tx_hash);
973                    for tx in sequence.transactions.iter_mut().skip(remaining_start) {
974                        tx.hash = None;
975                    }
976                    self.sequence.save(true, false)?;
977                    // Fall through to full send path below.
978                }
979                Ok(Err(e)) => return Err(e),
980                Err(_) => {
981                    // Timeout, clear stamped hashes so the next --resume can re-send rather than
982                    // waiting indefinitely on a potentially dead hash.
983                    sh_println!(
984                        "Timeout waiting for batch tx {tx_hash:#x}; clearing checkpoint so \
985                         --resume can re-send a replacement."
986                    )?;
987                    let sequence = self.sequence.sequences_mut().get_mut(0).unwrap();
988                    sequence.remove_pending(tx_hash);
989                    for tx in sequence.transactions.iter_mut().skip(remaining_start) {
990                        tx.hash = None;
991                    }
992                    self.sequence.save(true, false)?;
993                    return Err(eyre::eyre!(
994                        "Timeout waiting for batch transaction receipt (tx: {tx_hash:#x}). \
995                         The transaction hash has been cleared; run with --resume to retry."
996                    ));
997                }
998            }
999        }
1000
1001        // Reborrow after the potential save above.
1002        let sequence = self.sequence.sequences_mut().get_mut(0).unwrap();
1003
1004        let tempo_sponsor = self.script_config.tempo.sponsor_config().await?;
1005
1006        // Get wallet for signing
1007        enum BatchSigner {
1008            Unlocked,
1009            Wallet(EthereumWallet),
1010            TempoKeychain(Box<TempoAccountsWallet>),
1011        }
1012
1013        let mut batch_signer = if self.args.unlocked {
1014            BatchSigner::Unlocked
1015        } else if let Some(session) = self.script_config.tempo.session_signer_for_multi_wallet(
1016            &self.args.wallets,
1017            Some(sender),
1018            chain_id,
1019        )? {
1020            BatchSigner::TempoKeychain(Box::new(session.access_key))
1021        } else {
1022            let mut signers = self.script_wallets.into_multi_wallet().into_signers()?;
1023            if let Some(signer) = signers.remove(&sender) {
1024                BatchSigner::Wallet(EthereumWallet::new(signer))
1025            } else {
1026                // Try the Tempo Accounts store only for Tempo broadcasts.
1027                if self.script_config.evm_opts.networks.is_tempo()
1028                    && let Some(wallet) = TempoAccountsWallet::try_from_default_store()?
1029                    && wallet.has_account(sender)?
1030                {
1031                    BatchSigner::TempoKeychain(Box::new(wallet.with_chain_id(chain_id)))
1032                } else {
1033                    bail!("No wallet found for sender {}", sender);
1034                }
1035            }
1036        };
1037
1038        let create2_deployer = self.script_config.evm_opts.create2_deployer;
1039        let mut calls: Vec<Call> = Vec::new();
1040        for (call_index, tx) in remaining_transactions(sequence).enumerate() {
1041            // --batch cannot carry EIP-7702 authorization lists: they require per-tx signing
1042            // and cannot be atomically bundled into a Tempo batch.
1043            if tx.authorization_list().is_some_and(|l| !l.is_empty()) {
1044                bail!(
1045                    "--batch does not support EIP-7702 authorization lists \
1046                     (found at transaction {}); use regular broadcast instead.",
1047                    call_index + 1
1048                );
1049            }
1050            // --batch cannot carry blob sidecars: Tempo batch txs are not blob-carrying txs.
1051            if let TransactionMaybeSigned::Unsigned(inner) = tx
1052                && inner.blob_sidecar().is_some()
1053            {
1054                bail!(
1055                    "--batch does not support blob (EIP-4844) transactions \
1056                     (found at transaction {}); use regular broadcast instead.",
1057                    call_index + 1
1058                );
1059            }
1060
1061            // CREATEs are rewritten to CREATE2 via the Arachnid factory by the batch
1062            // inspector before broadcast, so tx.to() should always be Some here.
1063            let to = match tx.to() {
1064                Some(addr) => TxKind::Call(addr),
1065                None => {
1066                    bail!(
1067                        "Unexpected raw CREATE in --batch mode at position {} — \
1068                     this is a bug; CREATEs should have been rewritten by the inspector.",
1069                        call_index + 1
1070                    );
1071                }
1072            };
1073            let value = tx.value().unwrap_or(U256::ZERO);
1074            let input = tx.input().cloned().unwrap_or_default();
1075
1076            calls.push(Call { to, value, input });
1077        }
1078
1079        if calls.is_empty() {
1080            sh_println!("No transactions to broadcast in batch mode.")?;
1081            return Ok(BroadcastedState {
1082                args: self.args,
1083                script_config: self.script_config,
1084                build_data: self.build_data,
1085                sequence: self.sequence,
1086            });
1087        }
1088
1089        // CREATE2 deployer must exist on-chain for any rewritten CREATEs.
1090        let needs_factory = sequence
1091            .transactions
1092            .iter()
1093            .skip(remaining_start)
1094            .any(|tx| matches!(tx.call_kind, CallKind::Create | CallKind::Create2));
1095        if needs_factory {
1096            let code = provider.get_code_at(create2_deployer).await?;
1097            if keccak256(&code) != DEFAULT_CREATE2_DEPLOYER_CODEHASH {
1098                bail!(
1099                    "CREATE2 deployer {create2_deployer:#x} is not deployed on this Tempo network; \
1100                     --batch requires it. Deploy it first and retry."
1101                );
1102            }
1103        }
1104
1105        sh_println!(
1106            "\n## Broadcasting batch transaction with {} call(s) to chain {}...",
1107            calls.len(),
1108            sequence.chain
1109        )?;
1110
1111        // Build the batch transaction request
1112        let nonce = provider.get_transaction_count(sender).await?;
1113
1114        // Batch transactions are Tempo-only and always use EIP-1559 style fees.
1115        let fees = estimate_eip1559_fees(&provider, self.script_config.config.eip1559_fee_estimate)
1116            .await?;
1117        let fees = resolve_broadcast_eip1559_fees(
1118            fees,
1119            self.args.with_gas_price.map(|p| p.to()),
1120            self.args.priority_gas_price.map(|p| p.to()),
1121            None,
1122        )?;
1123        let max_fee_per_gas = fees.max_fee_per_gas;
1124        let max_priority_fee_per_gas = fees.max_priority_fee_per_gas;
1125
1126        let mut batch_tx = TempoTransactionRequest {
1127            inner: TransactionRequest {
1128                from: Some(sender),
1129                to: None,
1130                value: None,
1131                input: Default::default(),
1132                nonce: Some(nonce),
1133                chain_id: Some(chain_id),
1134                max_fee_per_gas: Some(max_fee_per_gas),
1135                max_priority_fee_per_gas: Some(max_priority_fee_per_gas),
1136                ..Default::default()
1137            },
1138            fee_token: self.script_config.tempo.fee_token,
1139            calls: calls.clone(),
1140            nonce_key: self.script_config.tempo.expiring_nonce.then_some(U256::MAX),
1141            valid_before: self.script_config.tempo.valid_before.and_then(NonZeroU64::new),
1142            ..Default::default()
1143        };
1144        self.script_config.tempo.apply::<TempoNetwork>(&mut batch_tx, None);
1145        let fee_token = if let Some(sponsor) = &tempo_sponsor {
1146            sponsor
1147                .resolve_and_set_fee_token(
1148                    Some(provider.as_ref()),
1149                    Some(Chain::from_named(NamedChain::Tempo)),
1150                    &mut batch_tx,
1151                )
1152                .await?;
1153            None
1154        } else {
1155            resolve_and_set_fee_token(
1156                Some(provider.as_ref()),
1157                Some(Chain::from_named(NamedChain::Tempo)),
1158                &mut batch_tx,
1159                Some(sender),
1160            )
1161            .await?
1162        };
1163
1164        if let BatchSigner::TempoKeychain(wallet) = &mut batch_signer {
1165            **wallet = batch_tx.prepare_with_tempo_wallet(provider.as_ref(), wallet).await?;
1166        }
1167
1168        // Estimate gas for the batch transaction
1169        estimate_gas(&mut batch_tx, provider.as_ref(), self.args.gas_estimate_multiplier, false)
1170            .await?;
1171
1172        sh_println!("Estimated gas: {}", batch_tx.inner.gas.unwrap_or(0))?;
1173
1174        if let Some(sponsor) = &tempo_sponsor {
1175            sponsor.attach_and_print::<TempoNetwork>(&mut batch_tx, sender).await?;
1176        } else {
1177            maybe_print_fee_token(Some(provider.as_ref()), fee_token).await?;
1178        }
1179
1180        // Sign and send.
1181        let tx_hash = match batch_signer {
1182            BatchSigner::Wallet(wallet) => {
1183                let provider_with_wallet =
1184                    alloy_provider::ProviderBuilder::<_, _, TempoNetwork>::default()
1185                        .wallet(wallet)
1186                        .connect_provider(provider.as_ref());
1187
1188                let pending = provider_with_wallet.send_transaction(batch_tx).await?;
1189                *pending.tx_hash()
1190            }
1191            BatchSigner::TempoKeychain(wallet) => {
1192                let raw_tx = batch_tx.sign_with_tempo_wallet(&wallet).await?;
1193
1194                let pending = provider.send_raw_transaction(&raw_tx).await?;
1195                *pending.tx_hash()
1196            }
1197            BatchSigner::Unlocked => {
1198                let pending = provider.send_transaction(batch_tx).await?;
1199                *pending.tx_hash()
1200            }
1201        };
1202
1203        sh_println!("Batch transaction sent: {:#x}", tx_hash)?;
1204
1205        // Checkpoint: stamp the batch hash on all remaining transactions (so that resume
1206        // detection finds it regardless of which tx it inspects first), register one entry
1207        // in sequence.pending for drop/timeout tracking, then save.
1208        for tx in sequence.transactions.iter_mut().skip(remaining_start) {
1209            tx.hash = Some(tx_hash);
1210        }
1211        if !sequence.pending.contains(&tx_hash) {
1212            sequence.pending.push(tx_hash);
1213        }
1214        self.sequence.save(true, false)?;
1215
1216        // Wait for receipt
1217        let timeout = self.script_config.config.transaction_timeout;
1218        let receipt = tokio::time::timeout(
1219            Duration::from_secs(timeout),
1220            wait_for_batch_receipt(provider.as_ref(), tx_hash, self.args.confirmations),
1221        )
1222        .await
1223        .map_err(|_| eyre::eyre!("Timeout waiting for batch transaction receipt (tx: {tx_hash:#x}). Run with --resume to retry."))??
1224        .ok_or_else(|| eyre::eyre!("Batch transaction {tx_hash:#x} was dropped from the mempool. Run with --resume to retry."))?;
1225
1226        let success = receipt.status();
1227        if success {
1228            sh_println!(
1229                "Batch transaction confirmed in block {}",
1230                receipt.block_number.unwrap_or(0)
1231            )?;
1232        } else {
1233            bail!("Batch transaction failed (reverted)");
1234        }
1235
1236        let sequence = self.sequence.sequences_mut().get_mut(0).unwrap();
1237        sequence.remove_pending(tx_hash);
1238
1239        // Receipts are pushed 1:1 with the remaining (not-yet-receipted) transactions.
1240        let remaining_len = sequence.transactions.len() - remaining_start;
1241        if calls.len() != remaining_len {
1242            bail!(
1243                "batch call count ({}) does not match remaining transactions ({}); \
1244                 refusing to push misaligned receipts",
1245                calls.len(),
1246                remaining_len
1247            );
1248        }
1249        // Only carry through contract_address for actual deployments; plain calls also
1250        // store the callee in `contract_address`, which would otherwise be copied into
1251        // the receipt and treated as a fresh deployment by downstream consumers
1252        // (broadcast JSON, verifier).
1253        let per_tx_addresses: Vec<Option<Address>> = sequence
1254            .transactions
1255            .iter()
1256            .skip(remaining_start)
1257            .map(|tx| match tx.call_kind {
1258                CallKind::Create | CallKind::Create2 => tx.contract_address,
1259                _ => None,
1260            })
1261            .collect();
1262
1263        for (idx, addr) in per_tx_addresses.iter().enumerate() {
1264            if let Some(addr) = addr {
1265                sh_println!("  call[{idx}] deployed at: {addr:#x}")?;
1266            }
1267        }
1268
1269        // gasUsed reflects the whole batch; per-call attribution is unavailable from the receipt.
1270        for addr in &per_tx_addresses {
1271            let mut tx_receipt = receipt.clone();
1272            tx_receipt.contract_address = *addr;
1273            sequence.receipts.push(tx_receipt);
1274        }
1275
1276        let chain = sequence.chain;
1277        let _ = sequence;
1278
1279        self.sequence.save(true, false)?;
1280
1281        let total_gas = receipt.gas_used();
1282        let gas_price = receipt.effective_gas_price() as u64;
1283        let total_paid = total_gas * gas_price;
1284        let paid = format_units(total_paid, 18).unwrap_or_else(|_| "N/A".to_string());
1285        let gas_price_gwei = format_units(gas_price, 9).unwrap_or_else(|_| "N/A".to_string());
1286
1287        let token_symbol = NamedChain::try_from(chain)
1288            .unwrap_or_default()
1289            .native_currency_symbol()
1290            .unwrap_or("ETH");
1291        sh_println!(
1292            "\nTotal Paid: {} {} ({} gas * {} gwei)",
1293            paid.trim_end_matches('0'),
1294            token_symbol,
1295            total_gas,
1296            gas_price_gwei.trim_end_matches('0').trim_end_matches('.')
1297        )?;
1298
1299        if !shell::is_json() {
1300            sh_println!("\n\n==========================")?;
1301            sh_println!("\nBATCH EXECUTION COMPLETE & SUCCESSFUL.")?;
1302            sh_println!("All {} calls executed atomically in a single transaction.", calls.len())?;
1303        }
1304
1305        Ok(BroadcastedState {
1306            args: self.args,
1307            script_config: self.script_config,
1308            build_data: self.build_data,
1309            sequence: self.sequence,
1310        })
1311    }
1312}
1313
1314async fn wait_for_batch_receipt<N: Network>(
1315    provider: &RootProvider<N>,
1316    tx_hash: TxHash,
1317    confirmations: u64,
1318) -> Result<Option<N::ReceiptResponse>> {
1319    loop {
1320        if let Some(receipt) = provider.get_transaction_receipt(tx_hash).await?
1321            && let Some(receipt_block) = receipt.block_number()
1322        {
1323            let latest_block = provider.get_block_number().await?;
1324            if latest_block >= receipt_block.saturating_add(confirmations.saturating_sub(1)) {
1325                return Ok(Some(receipt));
1326            }
1327        }
1328
1329        if provider.get_transaction_by_hash(tx_hash).await?.is_none() {
1330            return Ok(None);
1331        }
1332
1333        tokio::time::sleep(Duration::from_millis(500)).await;
1334    }
1335}
1336
1337pub async fn estimate_gas<N: Network, P: Provider<N>>(
1338    tx: &mut N::TransactionRequest,
1339    provider: &P,
1340    estimate_multiplier: u64,
1341    tempo_browser: bool,
1342) -> Result<()>
1343where
1344    N::TransactionRequest: FoundryTransactionBuilder<N>,
1345{
1346    // if already set, some RPC endpoints might simply return the gas value that is already
1347    // set in the request and omit the estimate altogether, so we remove it here
1348    tx.reset_gas_limit();
1349
1350    let request =
1351        if tempo_browser { tx.browser_wallet_gas_estimation_request() } else { tx.clone() };
1352    tx.set_gas_limit(
1353        provider.estimate_gas(request).await.wrap_err("Failed to estimate gas for tx")?
1354            * estimate_multiplier
1355            / 100,
1356    );
1357    Ok(())
1358}
1359
1360/// Returns `caller`'s nonce at an already resolved fork block.
1361pub(super) async fn next_nonce_resolved(
1362    caller: Address,
1363    evm_opts: &EvmOpts,
1364    fork: &ResolvedFork,
1365) -> eyre::Result<u64> {
1366    evm_opts.transaction_count_at_resolved_fork(caller, fork).await
1367}
1368
1369fn reject_access_key_create<N: Network>(
1370    tx: &N::TransactionRequest,
1371    uses_access_key: bool,
1372) -> Result<()>
1373where
1374    N::TransactionRequest: FoundryTransactionBuilder<N>,
1375{
1376    if uses_access_key && tx.tempo_calls().iter().any(|(to, _)| to.is_create()) {
1377        bail!("Tempo access-key transactions cannot use CREATE");
1378    }
1379    Ok(())
1380}
1381
1382fn convert_tempo_aa_create<N: Network>(tx: &mut N::TransactionRequest)
1383where
1384    N::TransactionRequest: FoundryTransactionBuilder<N>,
1385{
1386    if tx.is_tempo_aa() {
1387        tx.convert_create_to_call();
1388    }
1389}
1390
1391#[cfg(test)]
1392mod tests {
1393    use super::*;
1394    use alloy_consensus::{Eip658Value, Receipt, ReceiptEnvelope, ReceiptWithBloom};
1395    use alloy_eips::BlockId;
1396    use alloy_network::Ethereum;
1397    use alloy_primitives::{Bloom, address};
1398    use alloy_rpc_types::TransactionReceipt;
1399    use alloy_signer::Signer;
1400    use forge_script_sequence::TransactionWithMetadata;
1401
1402    const ROOT_PRIVATE_KEY: &str =
1403        "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
1404    const ACCESS_KEY_PRIVATE_KEY: &str =
1405        "0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0";
1406
1407    #[tokio::test(flavor = "multi_thread")]
1408    async fn next_nonce_uses_exact_fork_hash() {
1409        let (_api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
1410        let provider = handle.http_provider();
1411        let sender = handle.dev_accounts().next().unwrap();
1412        let recipient = Address::with_last_byte(1);
1413
1414        let receipt = provider
1415            .send_transaction(
1416                TransactionRequest::default()
1417                    .from(sender)
1418                    .to(recipient)
1419                    .value(U256::from(1))
1420                    .into(),
1421            )
1422            .await
1423            .unwrap()
1424            .get_receipt()
1425            .await
1426            .unwrap();
1427        let block_number = receipt.block_number.unwrap();
1428        let evm_opts = EvmOpts {
1429            fork_url: Some(handle.http_endpoint()),
1430            fork_block_number: Some(block_number),
1431            ..Default::default()
1432        };
1433        let fork = evm_opts.resolve_fork().await.unwrap().unwrap();
1434        assert_eq!(next_nonce_resolved(sender, &evm_opts, &fork).await.unwrap(), 1);
1435
1436        provider
1437            .raw_request::<_, ()>("anvil_reorg".into(), (1_u64, Vec::<serde_json::Value>::new()))
1438            .await
1439            .unwrap();
1440        assert_eq!(
1441            provider
1442                .get_transaction_count(sender)
1443                .block_id(BlockId::number(block_number))
1444                .await
1445                .unwrap(),
1446            0
1447        );
1448
1449        match next_nonce_resolved(sender, &evm_opts, &fork).await {
1450            Ok(0) => panic!("the exact lookup fell back to the replacement block"),
1451            Ok(1) | Err(_) => {}
1452            Ok(nonce) => panic!("unexpected nonce: {nonce}"),
1453        }
1454    }
1455
1456    #[test]
1457    fn access_key_signer_takes_precedence_over_same_sender_wallet() {
1458        let root = foundry_wallets::utils::create_private_key_signer(ROOT_PRIVATE_KEY).unwrap();
1459        let root_address = root.address();
1460        let access_key =
1461            foundry_wallets::utils::create_local_signer(ACCESS_KEY_PRIVATE_KEY).unwrap();
1462        let access_key_address = access_key.address();
1463        let mut eth_wallets = AddressHashMap::default();
1464        eth_wallets.insert(root_address, EthereumWallet::new(root));
1465        let mut access_keys = HashMap::default();
1466        access_keys.insert(
1467            SignerScope::new(4217, root_address),
1468            TempoAccountsWallet::from_secp256k1(root_address, access_key, None).with_chain_id(4217),
1469        );
1470        let send_kind =
1471            SendTransactionsKind::<Ethereum>::Raw { eth_wallets, browser: None, access_keys };
1472
1473        let tx = TransactionRequest { from: Some(root_address), ..Default::default() };
1474        let sender = send_kind.for_sender(4217, &root_address, tx).unwrap();
1475
1476        match sender {
1477            SendTransactionKind::AccessKey(_, wallet) => {
1478                assert_eq!(wallet.key_id().unwrap(), access_key_address);
1479                assert_eq!(wallet.account(), root_address);
1480            }
1481            _ => panic!("expected access key signer"),
1482        }
1483    }
1484
1485    #[test]
1486    fn access_key_signer_is_scoped_to_chain() {
1487        let root = foundry_wallets::utils::create_private_key_signer(ROOT_PRIVATE_KEY).unwrap();
1488        let root_address = root.address();
1489        let access_key =
1490            foundry_wallets::utils::create_local_signer(ACCESS_KEY_PRIVATE_KEY).unwrap();
1491        let mut eth_wallets = AddressHashMap::default();
1492        eth_wallets.insert(root_address, EthereumWallet::new(root));
1493        let mut access_keys = HashMap::default();
1494        access_keys.insert(
1495            SignerScope::new(4217, root_address),
1496            TempoAccountsWallet::from_secp256k1(root_address, access_key, None).with_chain_id(4217),
1497        );
1498        let send_kind =
1499            SendTransactionsKind::<Ethereum>::Raw { eth_wallets, browser: None, access_keys };
1500
1501        let tx = TransactionRequest { from: Some(root_address), ..Default::default() };
1502        let sender = send_kind.for_sender(1, &root_address, tx).unwrap();
1503
1504        match sender {
1505            SendTransactionKind::Raw(_, wallet) => {
1506                assert_eq!(wallet.default_signer().address(), root_address);
1507            }
1508            _ => panic!("expected root wallet signer for non-session chain"),
1509        }
1510    }
1511
1512    #[test]
1513    fn remaining_unsigned_transactions_skip_completed_transactions() {
1514        let completed = address!("0x1111111111111111111111111111111111111111");
1515        let remaining_sender = address!("0x2222222222222222222222222222222222222222");
1516        let mut sequence = ScriptSequence::<Ethereum> {
1517            chain: 4217,
1518            transactions: [script_tx(completed), script_tx(remaining_sender)].into(),
1519            receipts: vec![receipt()],
1520            ..Default::default()
1521        };
1522
1523        let remaining =
1524            remaining_unsigned_transactions(std::slice::from_ref(&sequence)).collect::<Vec<_>>();
1525        assert_eq!(remaining.len(), 1);
1526        assert_eq!(remaining[0].from, remaining_sender);
1527        assert_eq!(remaining[0].chain, 4217);
1528
1529        sequence.receipts.push(receipt());
1530        let remaining =
1531            remaining_unsigned_transactions(std::slice::from_ref(&sequence)).collect::<Vec<_>>();
1532        assert!(remaining.is_empty());
1533
1534        let completed_sequence = ScriptSequence::<Ethereum> {
1535            chain: 1,
1536            transactions: [script_tx(completed)].into(),
1537            receipts: vec![receipt()],
1538            ..Default::default()
1539        };
1540        let remaining_sequence = ScriptSequence::<Ethereum> {
1541            chain: 4217,
1542            transactions: [script_tx(remaining_sender)].into(),
1543            ..Default::default()
1544        };
1545
1546        let remaining = remaining_unsigned_transactions(&[completed_sequence, remaining_sequence])
1547            .collect::<Vec<_>>();
1548        assert_eq!(remaining.len(), 1);
1549        assert_eq!(remaining[0].chain, 4217);
1550    }
1551
1552    #[test]
1553    fn remaining_transactions_skip_receipt_prefix() {
1554        let completed = address!("0x1111111111111111111111111111111111111111");
1555        let second = address!("0x2222222222222222222222222222222222222222");
1556        let third = address!("0x3333333333333333333333333333333333333333");
1557        let mut sequence = ScriptSequence::<Ethereum> {
1558            chain: 4217,
1559            transactions: [script_tx(completed), script_tx(second), script_tx(third)].into(),
1560            receipts: vec![receipt()],
1561            ..Default::default()
1562        };
1563
1564        let remaining =
1565            remaining_transactions(&sequence).map(|tx| tx.from().unwrap()).collect::<Vec<_>>();
1566
1567        assert_eq!(remaining, vec![second, third]);
1568
1569        sequence.receipts = (0..4).map(|_| receipt()).collect();
1570        assert!(remaining_transactions(&sequence).next().is_none());
1571    }
1572
1573    #[tokio::test]
1574    async fn access_key_sets_key_id_before_estimation() {
1575        let root_address = address!("0x1111111111111111111111111111111111111111");
1576        let access_key =
1577            foundry_wallets::utils::create_local_signer(ACCESS_KEY_PRIVATE_KEY).unwrap();
1578        let access_key_address = access_key.address();
1579        let access_key_wallet =
1580            TempoAccountsWallet::from_secp256k1(root_address, access_key, None).with_chain_id(4217);
1581        let mut sender = SendTransactionKind::<TempoNetwork>::AccessKey(
1582            TempoTransactionRequest {
1583                inner: TransactionRequest { from: Some(root_address), ..Default::default() },
1584                ..Default::default()
1585            },
1586            Box::new(access_key_wallet),
1587        );
1588        let provider =
1589            RootProvider::<TempoNetwork>::new_http("http://localhost:8545".parse().unwrap());
1590
1591        sender
1592            .prepare(
1593                &provider,
1594                false,
1595                true,
1596                false,
1597                100,
1598                None,
1599                Some(Chain::from_named(NamedChain::Mainnet)),
1600            )
1601            .await
1602            .unwrap();
1603
1604        match sender {
1605            SendTransactionKind::AccessKey(tx, _) => {
1606                assert_eq!(tx.key_id, Some(access_key_address));
1607            }
1608            _ => panic!("expected access key transaction"),
1609        }
1610    }
1611
1612    #[test]
1613    fn tempo_aa_create_moves_deployment_into_calls() {
1614        let mut tx = TempoTransactionRequest {
1615            inner: TransactionRequest { to: Some(TxKind::Create), ..Default::default() },
1616            fee_token: Some(address!("0x20c0000000000000000000000000000000000000")),
1617            ..Default::default()
1618        };
1619
1620        convert_tempo_aa_create::<TempoNetwork>(&mut tx);
1621
1622        assert!(tx.inner.to.is_none());
1623        assert_eq!(tx.calls.len(), 1);
1624        assert!(tx.calls[0].to.is_create());
1625    }
1626
1627    #[test]
1628    fn tempo_access_key_create_is_rejected_before_preparation() {
1629        let tx = TempoTransactionRequest {
1630            inner: TransactionRequest { to: Some(TxKind::Create), ..Default::default() },
1631            ..Default::default()
1632        };
1633
1634        let error = reject_access_key_create::<TempoNetwork>(&tx, true).unwrap_err();
1635
1636        assert!(error.to_string().contains("Tempo access-key transactions cannot use CREATE"));
1637    }
1638
1639    fn script_tx(from: Address) -> TransactionWithMetadata<Ethereum> {
1640        TransactionWithMetadata::from_tx_request(TransactionMaybeSigned::new(TransactionRequest {
1641            from: Some(from),
1642            ..Default::default()
1643        }))
1644    }
1645
1646    fn receipt() -> TransactionReceipt {
1647        TransactionReceipt {
1648            inner: ReceiptEnvelope::Legacy(ReceiptWithBloom {
1649                receipt: Receipt {
1650                    status: Eip658Value::success(),
1651                    cumulative_gas_used: 0,
1652                    logs: vec![],
1653                },
1654                logs_bloom: Bloom::ZERO,
1655            }),
1656            transaction_hash: Default::default(),
1657            transaction_index: None,
1658            block_hash: None,
1659            block_number: None,
1660            gas_used: 0,
1661            effective_gas_price: 0,
1662            blob_gas_used: None,
1663            blob_gas_price: None,
1664            from: Address::ZERO,
1665            to: None,
1666            contract_address: None,
1667        }
1668    }
1669}