Skip to main content

forge_script/
broadcast.rs

1use std::{cmp::Ordering, num::NonZeroU64, sync::Arc, time::Duration};
2
3use crate::{
4    ScriptArgs, ScriptConfig, build::LinkedBuildData, progress::ScriptProgress,
5    sequence::ScriptSequenceKind, verify::BroadcastedState,
6};
7use alloy_chains::{Chain, NamedChain};
8use alloy_consensus::{SignableTransaction, Signed};
9use alloy_eips::{BlockId, eip2718::Encodable2718};
10use alloy_network::{
11    EthereumWallet, Network, NetworkTransactionBuilder, ReceiptResponse, TransactionBuilder,
12};
13use alloy_primitives::{
14    Address, TxHash, TxKind, U256,
15    map::{AddressHashMap, AddressHashSet},
16    utils::format_units,
17};
18use alloy_provider::{Provider, RootProvider, utils::Eip1559Estimation};
19use alloy_rpc_types::TransactionRequest;
20use alloy_signer::Signature;
21use eyre::{Context, Result, bail};
22use forge_verify::provider::VerificationProviderType;
23use foundry_cheatcodes::Wallets;
24use foundry_cli::utils::{has_batch_support, has_different_gas_calc};
25use foundry_common::{
26    FoundryTransactionBuilder, TransactionMaybeSigned,
27    provider::{ProviderBuilder, try_get_http_provider},
28    shell,
29    tempo::TempoSponsor,
30};
31use foundry_config::Config;
32use foundry_evm::core::evm::{FoundryEvmNetwork, TempoEvmNetwork};
33use foundry_wallets::{
34    TempoAccessKeyConfig, WalletSigner,
35    tempo::{TempoLookup, lookup_signer},
36    wallet_browser::signer::BrowserSigner,
37};
38use futures::{FutureExt, StreamExt, future::join_all, stream::FuturesUnordered};
39use itertools::Itertools;
40use tempo_alloy::{TempoNetwork, rpc::TempoTransactionRequest};
41use tempo_primitives::transaction::Call;
42
43pub async fn estimate_gas<N: Network, P: Provider<N>>(
44    tx: &mut N::TransactionRequest,
45    provider: &P,
46    estimate_multiplier: u64,
47) -> Result<()>
48where
49    N::TransactionRequest: FoundryTransactionBuilder<N>,
50{
51    // if already set, some RPC endpoints might simply return the gas value that is already
52    // set in the request and omit the estimate altogether, so we remove it here
53    tx.reset_gas_limit();
54
55    tx.set_gas_limit(
56        provider.estimate_gas(tx.clone()).await.wrap_err("Failed to estimate gas for tx")?
57            * estimate_multiplier
58            / 100,
59    );
60    Ok(())
61}
62
63pub async fn next_nonce(
64    caller: Address,
65    provider_url: &str,
66    block_number: Option<u64>,
67) -> eyre::Result<u64> {
68    let provider = try_get_http_provider(provider_url)
69        .wrap_err_with(|| format!("bad fork_url provider: {provider_url}"))?;
70
71    let block_id = block_number.map_or(BlockId::latest(), BlockId::number);
72    Ok(provider.get_transaction_count(caller).block_id(block_id).await?)
73}
74
75/// Represents how to send a single transaction.
76#[derive(Clone)]
77pub enum SendTransactionKind<'a, N: Network> {
78    Unlocked(N::TransactionRequest),
79    Raw(N::TransactionRequest, &'a EthereumWallet),
80    Browser(N::TransactionRequest, &'a BrowserSigner<N>),
81    Signed(N::TxEnvelope),
82    AccessKey(N::TransactionRequest, &'a WalletSigner, &'a TempoAccessKeyConfig),
83}
84
85impl<'a, N: Network> SendTransactionKind<'a, N>
86where
87    N::TxEnvelope: From<Signed<N::UnsignedTx>>,
88    N::UnsignedTx: SignableTransaction<Signature>,
89    N::TransactionRequest: FoundryTransactionBuilder<N>,
90{
91    /// Prepares the transaction for broadcasting by synchronizing nonce and estimating gas.
92    ///
93    /// This method performs two key operations:
94    /// 1. Nonce synchronization: Waits for the provider's nonce to catch up to the expected
95    ///    transaction nonce when doing sequential broadcast
96    /// 2. Gas estimation: Re-estimates gas right before broadcasting for chains that require it
97    pub async fn prepare(
98        &mut self,
99        provider: &RootProvider<N>,
100        sequential_broadcast: bool,
101        is_fixed_gas_limit: bool,
102        estimate_via_rpc: bool,
103        estimate_multiplier: u64,
104        tempo_sponsor: Option<&TempoSponsor>,
105    ) -> Result<()> {
106        let access_key_authorization = match self {
107            Self::AccessKey(_, _, access_key) => Some((
108                access_key.wallet_address,
109                access_key.key_address,
110                access_key.key_authorization.clone(),
111            )),
112            _ => None,
113        };
114
115        if let Self::Raw(tx, _)
116        | Self::Unlocked(tx)
117        | Self::Browser(tx, _)
118        | Self::AccessKey(tx, _, _) = self
119        {
120            if sequential_broadcast {
121                let from = tx.from().expect("no sender");
122
123                let tx_nonce = tx.nonce().expect("no nonce");
124                for attempt in 0..5 {
125                    let nonce = provider.get_transaction_count(from).await?;
126                    match nonce.cmp(&tx_nonce) {
127                        Ordering::Greater => {
128                            bail!(
129                                "EOA nonce changed unexpectedly while sending transactions. Expected {tx_nonce} got {nonce} from provider."
130                            )
131                        }
132                        Ordering::Less => {
133                            if attempt == 4 {
134                                bail!(
135                                    "After 5 attempts, provider nonce ({nonce}) is still behind expected nonce ({tx_nonce})."
136                                )
137                            }
138                            warn!(
139                                "Expected nonce ({tx_nonce}) is ahead of provider nonce ({nonce}). Retrying in 1 second..."
140                            );
141                            tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
142                        }
143                        Ordering::Equal => {
144                            // Nonces are equal, we can proceed.
145                            break;
146                        }
147                    }
148                }
149            }
150
151            if let Some((wallet_address, key_address, key_authorization)) =
152                access_key_authorization.as_ref()
153            {
154                tx.prepare_access_key_authorization(
155                    provider,
156                    *wallet_address,
157                    *key_address,
158                    key_authorization.as_ref(),
159                )
160                .await?;
161            }
162
163            // Chains which use `eth_estimateGas` are being sent sequentially and require their
164            // gas to be re-estimated right before broadcasting.
165            if !is_fixed_gas_limit && estimate_via_rpc {
166                estimate_gas(tx, provider, estimate_multiplier).await?;
167            }
168
169            if let Some(sponsor) = tempo_sponsor {
170                let from = tx.from().expect("no sender");
171                sponsor.attach_and_print::<N>(tx, from).await?;
172            }
173        }
174
175        Ok(())
176    }
177
178    /// Sends the transaction to the network.
179    ///
180    /// Depending on the transaction kind, this will either:
181    /// - Submit via `eth_sendTransaction` for unlocked accounts
182    /// - Sign and submit via `eth_sendRawTransaction` for raw transactions
183    /// - Submit pre-signed transaction via `eth_sendRawTransaction`
184    pub async fn send(self, provider: Arc<RootProvider<N>>) -> Result<TxHash> {
185        match self {
186            Self::Unlocked(tx) => {
187                debug!("sending transaction from unlocked account {:?}", tx);
188
189                // Submit the transaction
190                let pending = provider.send_transaction(tx).await?;
191                Ok(*pending.tx_hash())
192            }
193            Self::Raw(tx, signer) => {
194                debug!("sending transaction: {:?}", tx);
195                let signed = tx.build(signer).await?;
196
197                // Submit the raw transaction
198                let pending = provider.send_raw_transaction(signed.encoded_2718().as_ref()).await?;
199                Ok(*pending.tx_hash())
200            }
201            Self::Signed(tx) => {
202                debug!("sending transaction: {:?}", tx);
203                let pending = provider.send_raw_transaction(tx.encoded_2718().as_ref()).await?;
204                Ok(*pending.tx_hash())
205            }
206            Self::Browser(tx, signer) => {
207                debug!("sending transaction: {:?}", tx);
208
209                // Sign and send the transaction via the browser wallet
210                Ok(signer.send_transaction_via_browser(tx).await?)
211            }
212            Self::AccessKey(tx, signer, access_key) => {
213                debug!("sending transaction via tempo access key: {:?}", tx);
214
215                let raw_tx = tx
216                    .sign_with_access_key(
217                        provider.as_ref(),
218                        signer,
219                        access_key.wallet_address,
220                        access_key.key_address,
221                        access_key.key_authorization.as_ref(),
222                    )
223                    .await?;
224
225                let pending = provider.send_raw_transaction(&raw_tx).await?;
226                Ok(*pending.tx_hash())
227            }
228        }
229    }
230
231    /// Prepares and sends the transaction in one operation.
232    ///
233    /// This is a convenience method that combines [`prepare`](Self::prepare) and
234    /// [`send`](Self::send) into a single call.
235    pub async fn prepare_and_send(
236        mut self,
237        provider: Arc<RootProvider<N>>,
238        sequential_broadcast: bool,
239        is_fixed_gas_limit: bool,
240        estimate_via_rpc: bool,
241        estimate_multiplier: u64,
242        tempo_sponsor: Option<&TempoSponsor>,
243    ) -> Result<TxHash> {
244        self.prepare(
245            &provider,
246            sequential_broadcast,
247            is_fixed_gas_limit,
248            estimate_via_rpc,
249            estimate_multiplier,
250            tempo_sponsor,
251        )
252        .await?;
253
254        self.send(provider).await
255    }
256}
257
258/// Represents how to send _all_ transactions
259pub enum SendTransactionsKind<N: Network> {
260    /// Send via `eth_sendTransaction` and rely on the  `from` address being unlocked.
261    Unlocked(AddressHashSet),
262    /// Send a signed transaction via `eth_sendRawTransaction`, or via browser
263    Raw {
264        eth_wallets: AddressHashMap<EthereumWallet>,
265        browser: Option<BrowserSigner<N>>,
266        access_keys: AddressHashMap<(WalletSigner, TempoAccessKeyConfig)>,
267    },
268}
269
270impl<N: Network> SendTransactionsKind<N> {
271    /// Returns the [`SendTransactionKind`] for the given address
272    ///
273    /// Returns an error if no matching signer is found or the address is not unlocked
274    pub fn for_sender(
275        &self,
276        addr: &Address,
277        tx: N::TransactionRequest,
278    ) -> Result<SendTransactionKind<'_, N>> {
279        match self {
280            Self::Unlocked(unlocked) => {
281                if !unlocked.contains(addr) {
282                    bail!("Sender address {:?} is not unlocked", addr)
283                }
284                Ok(SendTransactionKind::Unlocked(tx))
285            }
286            Self::Raw { eth_wallets, browser, access_keys } => {
287                if let Some((signer, config)) = access_keys.get(addr) {
288                    Ok(SendTransactionKind::AccessKey(tx, signer, config))
289                } else if let Some(wallet) = eth_wallets.get(addr) {
290                    Ok(SendTransactionKind::Raw(tx, wallet))
291                } else if let Some(b) = browser
292                    && b.address() == *addr
293                {
294                    Ok(SendTransactionKind::Browser(tx, b))
295                } else {
296                    bail!("No matching signer for {:?} found", addr)
297                }
298            }
299        }
300    }
301}
302
303/// State after we have bundled all
304/// [`TransactionWithMetadata`](forge_script_sequence::TransactionWithMetadata) objects into a
305/// single [`ScriptSequenceKind`] object containing one or more script sequences.
306pub struct BundledState<FEN: FoundryEvmNetwork> {
307    pub args: ScriptArgs,
308    pub script_config: ScriptConfig<FEN>,
309    pub script_wallets: Wallets,
310    pub browser_wallet: Option<BrowserSigner<FEN::Network>>,
311    pub build_data: LinkedBuildData,
312    pub sequence: ScriptSequenceKind<FEN::Network>,
313}
314
315impl<FEN: FoundryEvmNetwork> BundledState<FEN> {
316    pub async fn wait_for_pending(mut self) -> Result<Self> {
317        let progress = ScriptProgress::default();
318        let progress_ref = &progress;
319        let futs = self
320            .sequence
321            .sequences_mut()
322            .iter_mut()
323            .enumerate()
324            .map(|(sequence_idx, sequence)| async move {
325                let rpc_url = sequence.rpc_url();
326                let provider = Arc::new(ProviderBuilder::new(rpc_url).build()?);
327                progress_ref
328                    .wait_for_pending(
329                        sequence_idx,
330                        sequence,
331                        &provider,
332                        self.script_config.config.transaction_timeout,
333                    )
334                    .await
335            })
336            .collect::<Vec<_>>();
337
338        let errors = join_all(futs).await.into_iter().filter_map(Result::err).collect::<Vec<_>>();
339
340        self.sequence.save(true, false)?;
341
342        if !errors.is_empty() {
343            return Err(eyre::eyre!("{}", errors.iter().format("\n")));
344        }
345
346        Ok(self)
347    }
348
349    /// Broadcasts transactions from all sequences.
350    pub async fn broadcast(mut self) -> Result<BroadcastedState<FEN>> {
351        let required_addresses = self
352            .sequence
353            .sequences()
354            .iter()
355            .flat_map(|sequence| {
356                sequence
357                    .transactions()
358                    .filter(|tx| tx.is_unsigned())
359                    .map(|tx| tx.from().expect("missing from"))
360            })
361            .collect::<AddressHashSet>();
362
363        if required_addresses.contains(&Config::DEFAULT_SENDER) {
364            eyre::bail!(
365                "You seem to be using Foundry's default sender. Be sure to set your own --sender."
366            );
367        }
368
369        let send_kind = if self.args.unlocked {
370            SendTransactionsKind::Unlocked(required_addresses.clone())
371        } else {
372            let signers: Vec<Address> = self
373                .script_wallets
374                .signers()
375                .map_err(|e| eyre::eyre!("{e}"))?
376                .into_iter()
377                .chain(self.browser_wallet.as_ref().map(|b| b.address()))
378                .collect();
379
380            // For addresses without an explicit signer, try Tempo keys.toml fallback.
381            let mut access_keys: AddressHashMap<(WalletSigner, TempoAccessKeyConfig)> =
382                AddressHashMap::default();
383            let mut direct_signers: AddressHashMap<WalletSigner> = AddressHashMap::default();
384            let mut missing_addresses = Vec::new();
385
386            for addr in &required_addresses {
387                if !signers.contains(addr) {
388                    match lookup_signer(*addr) {
389                        Ok(TempoLookup::Direct(signer)) => {
390                            direct_signers.insert(*addr, signer);
391                        }
392                        Ok(TempoLookup::Keychain(signer, config)) => {
393                            access_keys.insert(*addr, (signer, *config));
394                        }
395                        _ => {
396                            missing_addresses.push(addr);
397                        }
398                    }
399                }
400            }
401
402            if !missing_addresses.is_empty() {
403                eyre::bail!(
404                    "No associated wallet for addresses: {:?}. Unlocked wallets: {:?}",
405                    missing_addresses,
406                    signers
407                );
408            }
409
410            let signers = self.script_wallets.into_multi_wallet().into_signers()?;
411            let mut eth_wallets: AddressHashMap<EthereumWallet> =
412                signers.into_iter().map(|(addr, signer)| (addr, signer.into())).collect();
413            for (addr, signer) in direct_signers {
414                eth_wallets.insert(addr, signer.into());
415            }
416
417            SendTransactionsKind::Raw { eth_wallets, browser: self.browser_wallet, access_keys }
418        };
419
420        let tempo_sponsor = self.script_config.tempo.sponsor_config().await?.map(Arc::new);
421        if tempo_sponsor.is_some() && self.script_config.tempo.sponsor_sig.is_some() {
422            let remaining = self
423                .sequence
424                .sequences()
425                .iter()
426                .map(|sequence| {
427                    sequence
428                        .transactions()
429                        .skip(sequence.receipts.len())
430                        .filter(|tx| tx.is_unsigned())
431                        .count()
432                })
433                .sum::<usize>();
434            if remaining > 1 {
435                eyre::bail!(
436                    "--tempo.sponsor-sig can only sponsor one remaining script transaction; use --tempo.sponsor-signer for multi-transaction scripts"
437                );
438            }
439        }
440
441        let progress = ScriptProgress::default();
442
443        for i in 0..self.sequence.sequences().len() {
444            let mut sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
445
446            let provider = Arc::new(ProviderBuilder::new(sequence.rpc_url()).build()?);
447            let already_broadcasted = sequence.receipts.len();
448
449            let seq_progress = progress.get_sequence_progress(i, sequence);
450
451            if already_broadcasted < sequence.transactions.len() {
452                let is_legacy = Chain::from(sequence.chain).is_legacy() || self.args.legacy;
453                // Make a one-time gas price estimation
454                let (gas_price, eip1559_fees) = match (
455                    is_legacy,
456                    self.args.with_gas_price,
457                    self.args.priority_gas_price,
458                ) {
459                    (true, Some(gas_price), _) => (Some(gas_price.to()), None),
460                    (true, None, _) => (Some(provider.get_gas_price().await?), None),
461                    (false, Some(max_fee_per_gas), Some(max_priority_fee_per_gas)) => {
462                        let max_fee: u128 = max_fee_per_gas.to();
463                        let max_priority: u128 = max_priority_fee_per_gas.to();
464                        if max_priority > max_fee {
465                            eyre::bail!(
466                                "--priority-gas-price ({max_priority}) cannot be higher than --with-gas-price ({max_fee})"
467                            );
468                        }
469                        (
470                            None,
471                            Some(Eip1559Estimation {
472                                max_fee_per_gas: max_fee,
473                                max_priority_fee_per_gas: max_priority,
474                            }),
475                        )
476                    }
477                    (false, _, _) => {
478                        let mut fees = provider.estimate_eip1559_fees().await.wrap_err("Failed to estimate EIP1559 fees. This chain might not support EIP1559, try adding --legacy to your command.")?;
479
480                        // When using --browser, the browser wallet may override the
481                        // priority fee with its own estimate (from
482                        // eth_maxPriorityFeePerGas) without adjusting maxFeePerGas,
483                        // leading to maxPriorityFeePerGas > maxFeePerGas.
484                        // This is common on OP Stack chains (e.g. Base) where
485                        // eth_feeHistory returns empty reward arrays, causing the
486                        // estimator to fall back to a 1 wei priority fee.
487                        if matches!(&send_kind, SendTransactionsKind::Raw { browser: Some(_), .. })
488                            && let Ok(suggested_tip) = provider.get_max_priority_fee_per_gas().await
489                            && suggested_tip > fees.max_priority_fee_per_gas
490                        {
491                            fees.max_fee_per_gas += suggested_tip - fees.max_priority_fee_per_gas;
492                            fees.max_priority_fee_per_gas = suggested_tip;
493                        }
494
495                        if let Some(gas_price) = self.args.with_gas_price {
496                            fees.max_fee_per_gas = gas_price.to();
497                        }
498
499                        if let Some(priority_gas_price) = self.args.priority_gas_price {
500                            fees.max_priority_fee_per_gas = priority_gas_price.to();
501                        }
502
503                        (None, Some(fees))
504                    }
505                };
506
507                // Iterate through transactions, matching the `from` field with the associated
508                // wallet. Then send the transaction. Panics if we find a unknown `from`
509                let transactions = sequence
510                    .transactions
511                    .iter()
512                    .skip(already_broadcasted)
513                    .map(|tx_with_metadata| {
514                        let is_fixed_gas_limit = tx_with_metadata.is_fixed_gas_limit;
515
516                        let kind = match tx_with_metadata.tx().clone() {
517                            TransactionMaybeSigned::Signed { tx, .. } => {
518                                if tempo_sponsor.is_some() {
519                                    eyre::bail!(
520                                        "cannot attach Tempo sponsor signature to an already signed script transaction"
521                                    );
522                                }
523                                SendTransactionKind::Signed(tx)
524                            }
525                            TransactionMaybeSigned::Unsigned(mut tx) => {
526                                let from = tx.from().expect("No sender for onchain transaction!");
527
528                                tx.set_chain_id(sequence.chain);
529
530                                // Set TxKind::Create explicitly to satisfy `check_reqd_fields` in
531                                // alloy
532                                if tx.kind().is_none() {
533                                    tx.set_create();
534                                }
535
536                                if let Some(gas_price) = gas_price {
537                                    tx.set_gas_price(gas_price);
538                                } else {
539                                    let eip1559_fees = eip1559_fees.expect("was set above");
540                                    tx.set_max_priority_fee_per_gas(
541                                        eip1559_fees.max_priority_fee_per_gas,
542                                    );
543                                    tx.set_max_fee_per_gas(eip1559_fees.max_fee_per_gas);
544                                }
545
546                                self.script_config.tempo.apply::<FEN::Network>(&mut tx, None);
547
548                                send_kind.for_sender(&from, tx)?
549                            }
550                        };
551
552                        Ok((kind, is_fixed_gas_limit))
553                    })
554                    .collect::<Result<Vec<_>>>()?;
555
556                let estimate_via_rpc = has_different_gas_calc(sequence.chain)
557                    || self.script_config.evm_opts.networks.is_tempo()
558                    || self.args.skip_simulation;
559
560                // We only wait for a transaction receipt before sending the next transaction, if
561                // there is more than one signer. There would be no way of assuring
562                // their order otherwise.
563                // Or if the chain does not support batched transactions (eg. Arbitrum).
564                // Or if we need to invoke eth_estimateGas before sending transactions.
565                let sequential_broadcast = estimate_via_rpc
566                    || self.args.slow
567                    || required_addresses.len() != 1
568                    || !has_batch_support(sequence.chain);
569
570                // We send transactions and wait for receipts in batches of 100, since some networks
571                // cannot handle more than that.
572                let batch_size = if sequential_broadcast { 1 } else { 100 };
573                let mut index = already_broadcasted;
574
575                for (batch_number, batch) in transactions.chunks(batch_size).enumerate() {
576                    seq_progress.inner.write().set_status(&format!(
577                        "Sending transactions [{} - {}]",
578                        batch_number * batch_size,
579                        batch_number * batch_size + std::cmp::min(batch_size, batch.len()) - 1
580                    ));
581
582                    if !batch.is_empty() {
583                        let pending_transactions =
584                            batch.iter().map(|(kind, is_fixed_gas_limit)| {
585                                let provider = provider.clone();
586                                let tempo_sponsor = tempo_sponsor.clone();
587                                async move {
588                                    let res = kind
589                                        .clone()
590                                        .prepare_and_send(
591                                            provider,
592                                            sequential_broadcast,
593                                            *is_fixed_gas_limit,
594                                            estimate_via_rpc,
595                                            self.args.gas_estimate_multiplier,
596                                            tempo_sponsor.as_deref(),
597                                        )
598                                        .await;
599                                    (res, kind, *is_fixed_gas_limit, 0, None)
600                                }
601                                .boxed()
602                            });
603
604                        let mut buffer = pending_transactions.collect::<FuturesUnordered<_>>();
605
606                        'send: while let Some((
607                            res,
608                            kind,
609                            is_fixed_gas_limit,
610                            attempt,
611                            original_res,
612                        )) = buffer.next().await
613                        {
614                            if res.is_err()
615                                && self.script_config.tempo.sponsor_sig.is_some()
616                                && attempt == 0
617                            {
618                                debug!(
619                                    "not retrying transaction because --tempo.sponsor-sig is a static signature"
620                                );
621                            } else if res.is_err() && attempt <= 3 {
622                                // Try to resubmit the transaction
623                                let provider = provider.clone();
624                                let progress = seq_progress.inner.clone();
625                                let tempo_sponsor = tempo_sponsor.clone();
626                                buffer.push(Box::pin(async move {
627                                    debug!(err=?res, ?attempt, "retrying transaction ");
628                                    let attempt = attempt + 1;
629                                    progress.write().set_status(&format!(
630                                        "retrying transaction {res:?} (attempt {attempt})"
631                                    ));
632                                    tokio::time::sleep(Duration::from_millis(1000 * attempt)).await;
633                                    let r = kind
634                                        .clone()
635                                        .prepare_and_send(
636                                            provider,
637                                            sequential_broadcast,
638                                            is_fixed_gas_limit,
639                                            estimate_via_rpc,
640                                            self.args.gas_estimate_multiplier,
641                                            tempo_sponsor.as_deref(),
642                                        )
643                                        .await;
644                                    (
645                                        r,
646                                        kind,
647                                        is_fixed_gas_limit,
648                                        attempt,
649                                        original_res.or(Some(res)),
650                                    )
651                                }));
652
653                                continue 'send;
654                            }
655
656                            // Preserve the original error if any
657                            let tx_hash = res.wrap_err_with(|| {
658                                if let Some(original_res) = original_res {
659                                    format!(
660                                        "Failed to send transaction after {attempt} attempts {original_res:?}"
661                                    )
662                                } else {
663                                    "Failed to send transaction".to_string()
664                                }
665                            })?;
666                            sequence.add_pending(index, tx_hash);
667
668                            // Checkpoint save
669                            self.sequence.save(true, false)?;
670                            sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
671
672                            seq_progress.inner.write().tx_sent(tx_hash);
673                            index += 1;
674                        }
675
676                        // Checkpoint save
677                        self.sequence.save(true, false)?;
678                        sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
679
680                        progress
681                            .wait_for_pending(
682                                i,
683                                sequence,
684                                &provider,
685                                self.script_config.config.transaction_timeout,
686                            )
687                            .await?
688                    }
689                    // Checkpoint save
690                    self.sequence.save(true, false)?;
691                    sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
692                }
693            }
694
695            let (total_gas, total_gas_price, total_paid) =
696                sequence.receipts.iter().fold((0, 0, 0), |acc, receipt| {
697                    let gas_used = receipt.gas_used();
698                    let gas_price = receipt.effective_gas_price() as u64;
699                    (acc.0 + gas_used, acc.1 + gas_price, acc.2 + gas_used * gas_price)
700                });
701            let paid = format_units(total_paid, 18).unwrap_or_else(|_| "N/A".to_string());
702            let avg_gas_price = total_gas_price
703                .checked_div(sequence.receipts.len() as u64)
704                .and_then(|avg| format_units(avg, 9).ok())
705                .unwrap_or_else(|| "N/A".to_string());
706
707            let token_symbol = NamedChain::try_from(sequence.chain)
708                .unwrap_or_default()
709                .native_currency_symbol()
710                .unwrap_or("ETH");
711            seq_progress.inner.write().set_status(&format!(
712                "Total Paid: {} {} ({} gas * avg {} gwei)\n",
713                paid.trim_end_matches('0'),
714                token_symbol,
715                total_gas,
716                avg_gas_price.trim_end_matches('0').trim_end_matches('.')
717            ));
718            seq_progress.inner.write().finish();
719        }
720
721        if !shell::is_json() {
722            sh_println!("\n\n==========================")?;
723            sh_println!("\nONCHAIN EXECUTION COMPLETE & SUCCESSFUL.")?;
724        }
725
726        Ok(BroadcastedState {
727            args: self.args,
728            script_config: self.script_config,
729            build_data: self.build_data,
730            sequence: self.sequence,
731        })
732    }
733
734    pub fn verify_preflight_check(&self) -> Result<()> {
735        for sequence in self.sequence.sequences() {
736            if self.args.verifier.verifier == VerificationProviderType::Etherscan
737                && self
738                    .script_config
739                    .config
740                    .get_etherscan_api_key(Some(sequence.chain.into()))
741                    .is_none()
742            {
743                eyre::bail!("Missing etherscan key for chain {}", sequence.chain);
744            }
745        }
746
747        Ok(())
748    }
749}
750
751impl BundledState<TempoEvmNetwork> {
752    /// Broadcasts all transactions as a single Tempo batch transaction (type 0x76).
753    ///
754    /// This method collects all individual transactions from the script and combines them
755    /// into a single batch transaction for atomic execution on Tempo.
756    pub async fn broadcast_batch(mut self) -> Result<BroadcastedState<TempoEvmNetwork>> {
757        // Batch mode only supports single chain for now
758        if self.sequence.sequences().len() != 1 {
759            bail!(
760                "--batch mode only supports single-chain scripts. \
761                 Use --multi without --batch for multi-chain."
762            );
763        }
764
765        let sequence = self.sequence.sequences_mut().get_mut(0).unwrap();
766        let provider = Arc::new(ProviderBuilder::<TempoNetwork>::new(sequence.rpc_url()).build()?);
767        let tempo_sponsor = self.script_config.tempo.sponsor_config().await?;
768
769        // Collect sender addresses - batch mode requires single sender
770        let senders: AddressHashSet = sequence
771            .transactions()
772            .filter(|tx| tx.is_unsigned())
773            .filter_map(|tx| tx.from())
774            .collect();
775
776        if senders.len() != 1 {
777            bail!(
778                "--batch mode requires all transactions to have the same sender. \
779                 Found {} unique senders: {:?}",
780                senders.len(),
781                senders
782            );
783        }
784
785        let sender = *senders.iter().next().unwrap();
786
787        if sender == Config::DEFAULT_SENDER {
788            bail!(
789                "You seem to be using Foundry's default sender. Be sure to set your own --sender."
790            );
791        }
792
793        // Get wallet for signing
794        enum BatchSigner {
795            Unlocked,
796            Wallet(EthereumWallet),
797            TempoKeychain(Box<WalletSigner>, Box<TempoAccessKeyConfig>),
798        }
799
800        let batch_signer = if self.args.unlocked {
801            BatchSigner::Unlocked
802        } else {
803            let mut signers = self.script_wallets.into_multi_wallet().into_signers()?;
804            if let Some(signer) = signers.remove(&sender) {
805                BatchSigner::Wallet(EthereumWallet::new(signer))
806            } else {
807                // Try Tempo keys.toml fallback
808                match lookup_signer(sender)? {
809                    TempoLookup::Direct(signer) => BatchSigner::Wallet(EthereumWallet::new(signer)),
810                    TempoLookup::Keychain(signer, config) => {
811                        BatchSigner::TempoKeychain(Box::new(signer), config)
812                    }
813                    TempoLookup::NotFound => {
814                        bail!("No wallet found for sender {}", sender);
815                    }
816                }
817            }
818        };
819
820        // Collect all transactions into Call structs
821        // Tempo batch transactions support CREATE only as the first call
822        let mut calls: Vec<Call> = Vec::new();
823        let mut has_create = false;
824        for (idx, tx) in sequence.transactions().enumerate() {
825            let to = match tx.to() {
826                Some(addr) => TxKind::Call(addr),
827                None => {
828                    if idx > 0 {
829                        bail!(
830                            "Contract creation must be the first transaction in --batch mode. \
831                             Found CREATE at position {}. Reorder your script or deploy separately.",
832                            idx + 1
833                        );
834                    }
835                    if has_create {
836                        bail!("Only one contract creation is allowed per --batch transaction.");
837                    }
838                    has_create = true;
839                    TxKind::Create
840                }
841            };
842            let value = tx.value().unwrap_or(U256::ZERO);
843            let input = tx.input().cloned().unwrap_or_default();
844
845            calls.push(Call { to, value, input });
846        }
847
848        if calls.is_empty() {
849            sh_println!("No transactions to broadcast in batch mode.")?;
850            return Ok(BroadcastedState {
851                args: self.args,
852                script_config: self.script_config,
853                build_data: self.build_data,
854                sequence: self.sequence,
855            });
856        }
857
858        sh_println!(
859            "\n## Broadcasting batch transaction with {} call(s) to chain {}...",
860            calls.len(),
861            sequence.chain
862        )?;
863
864        // Build the batch transaction request
865        let nonce = provider.get_transaction_count(sender).await?;
866        let chain_id = sequence.chain;
867
868        // Get gas prices - batch transactions are Tempo-only, always use EIP-1559 style fees
869        let fees = provider.estimate_eip1559_fees().await?;
870        let max_fee_per_gas =
871            self.args.with_gas_price.map(|p| p.to()).unwrap_or(fees.max_fee_per_gas);
872        let max_priority_fee_per_gas =
873            self.args.priority_gas_price.map(|p| p.to()).unwrap_or(fees.max_priority_fee_per_gas);
874
875        let mut batch_tx = TempoTransactionRequest {
876            inner: TransactionRequest {
877                from: Some(sender),
878                to: None,
879                value: None,
880                input: Default::default(),
881                nonce: Some(nonce),
882                chain_id: Some(chain_id),
883                max_fee_per_gas: Some(max_fee_per_gas),
884                max_priority_fee_per_gas: Some(max_priority_fee_per_gas),
885                ..Default::default()
886            },
887            fee_token: self.script_config.tempo.fee_token,
888            calls: calls.clone(),
889            nonce_key: self.script_config.tempo.expiring_nonce.then_some(U256::MAX),
890            valid_before: self.script_config.tempo.valid_before.and_then(NonZeroU64::new),
891            ..Default::default()
892        };
893        self.script_config.tempo.apply::<TempoNetwork>(&mut batch_tx, None);
894
895        if let BatchSigner::TempoKeychain(_, ak) = &batch_signer {
896            batch_tx.key_id = Some(ak.key_address);
897            batch_tx
898                .prepare_access_key_authorization(
899                    provider.as_ref(),
900                    ak.wallet_address,
901                    ak.key_address,
902                    ak.key_authorization.as_ref(),
903                )
904                .await?;
905        }
906
907        // Estimate gas for the batch transaction
908        estimate_gas(&mut batch_tx, provider.as_ref(), self.args.gas_estimate_multiplier).await?;
909
910        sh_println!("Estimated gas: {}", batch_tx.inner.gas.unwrap_or(0))?;
911
912        if let Some(sponsor) = &tempo_sponsor {
913            sponsor.attach_and_print::<TempoNetwork>(&mut batch_tx, sender).await?;
914        }
915
916        // Sign and send
917        let tx_hash = match batch_signer {
918            BatchSigner::Wallet(wallet) => {
919                let provider_with_wallet =
920                    alloy_provider::ProviderBuilder::<_, _, TempoNetwork>::default()
921                        .wallet(wallet)
922                        .connect_provider(provider.as_ref());
923
924                let pending = provider_with_wallet.send_transaction(batch_tx).await?;
925                *pending.tx_hash()
926            }
927            BatchSigner::TempoKeychain(signer, access_key) => {
928                let raw_tx = batch_tx
929                    .sign_with_access_key(
930                        provider.as_ref(),
931                        &*signer,
932                        access_key.wallet_address,
933                        access_key.key_address,
934                        access_key.key_authorization.as_ref(),
935                    )
936                    .await?;
937
938                let pending = provider.send_raw_transaction(&raw_tx).await?;
939                *pending.tx_hash()
940            }
941            BatchSigner::Unlocked => {
942                let pending = provider.send_transaction(batch_tx).await?;
943                *pending.tx_hash()
944            }
945        };
946
947        sh_println!("Batch transaction sent: {:#x}", tx_hash)?;
948
949        // Wait for receipt
950        let timeout = self.script_config.config.transaction_timeout;
951        let receipt = tokio::time::timeout(Duration::from_secs(timeout), async {
952            loop {
953                if let Some(receipt) = provider.get_transaction_receipt(tx_hash).await? {
954                    return Ok::<_, eyre::Error>(receipt);
955                }
956                tokio::time::sleep(Duration::from_millis(500)).await;
957            }
958        })
959        .await
960        .map_err(|_| eyre::eyre!("Timeout waiting for batch transaction receipt"))??;
961
962        let success = receipt.status();
963        if success {
964            sh_println!(
965                "Batch transaction confirmed in block {}",
966                receipt.block_number.unwrap_or(0)
967            )?;
968        } else {
969            bail!("Batch transaction failed (reverted)");
970        }
971
972        // For CREATE transactions, compute the deployed contract address
973        let created_address = if has_create {
974            let deployed_addr = sender.create(nonce);
975            sh_println!("Contract deployed at: {:#x}", deployed_addr)?;
976            Some(deployed_addr)
977        } else {
978            None
979        };
980
981        // Add receipt to sequence for each original transaction.
982        // In batch mode, all calls share the same receipt. Set contract_address
983        // only for index 0 if CREATE, clear for the rest to prevent the verifier
984        // from attempting to verify the same address multiple times.
985        for idx in 0..calls.len() {
986            let mut tx_receipt = receipt.clone();
987            if idx == 0 && has_create {
988                tx_receipt.contract_address = created_address;
989            } else {
990                tx_receipt.contract_address = None;
991            }
992            sequence.receipts.push(tx_receipt);
993        }
994
995        // Mark all transactions as pending with the batch tx hash
996        for i in 0..sequence.transactions.len() {
997            sequence.add_pending(i, tx_hash);
998        }
999
1000        let chain = sequence.chain;
1001        let _ = sequence;
1002
1003        self.sequence.save(true, false)?;
1004
1005        let total_gas = receipt.gas_used();
1006        let gas_price = receipt.effective_gas_price() as u64;
1007        let total_paid = total_gas * gas_price;
1008        let paid = format_units(total_paid, 18).unwrap_or_else(|_| "N/A".to_string());
1009        let gas_price_gwei = format_units(gas_price, 9).unwrap_or_else(|_| "N/A".to_string());
1010
1011        let token_symbol = NamedChain::try_from(chain)
1012            .unwrap_or_default()
1013            .native_currency_symbol()
1014            .unwrap_or("ETH");
1015        sh_println!(
1016            "\nTotal Paid: {} {} ({} gas * {} gwei)",
1017            paid.trim_end_matches('0'),
1018            token_symbol,
1019            total_gas,
1020            gas_price_gwei.trim_end_matches('0').trim_end_matches('.')
1021        )?;
1022
1023        if !shell::is_json() {
1024            sh_println!("\n\n==========================")?;
1025            sh_println!("\nBATCH EXECUTION COMPLETE & SUCCESSFUL.")?;
1026            sh_println!("All {} calls executed atomically in a single transaction.", calls.len())?;
1027        }
1028
1029        Ok(BroadcastedState {
1030            args: self.args,
1031            script_config: self.script_config,
1032            build_data: self.build_data,
1033            sequence: self.sequence,
1034        })
1035    }
1036}