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