Skip to main content

forge_script/
broadcast.rs

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