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