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    receipts::is_mined_receipt_for,
8    recovery::{AttemptKind, DelegatedStatus},
9    sequence::{ScriptSequenceKind, completed_transaction_prefix},
10    session::{
11        SignerScope, insert_session_access_key_for_remaining_transactions,
12        script_session_expected_sender_if_configured,
13    },
14    verify::BroadcastedState,
15};
16use alloy_chains::{Chain, NamedChain};
17use alloy_consensus::{SignableTransaction, Signed, transaction::SignerRecoverable};
18use alloy_eips::eip2718::{Decodable2718, Encodable2718};
19use alloy_network::{
20    EthereumWallet, Network, NetworkTransactionBuilder, ReceiptResponse, TransactionBuilder,
21    TransactionResponse,
22};
23use alloy_primitives::{
24    Address, Bytes, TxHash, TxKind, U256, keccak256,
25    map::{AddressHashMap, AddressHashSet, HashMap},
26    utils::format_units,
27};
28use alloy_provider::{
29    Provider, RootProvider,
30    transport::{RpcError, TransportError},
31    utils::Eip1559Estimation,
32};
33use alloy_rpc_types::TransactionRequest;
34use alloy_signer::Signature;
35use eyre::{Context, ContextCompat, Result, bail};
36use forge_script_sequence::ScriptSequence;
37use foundry_cheatcodes::Wallets;
38use foundry_cli::utils::{has_batch_support, has_different_gas_calc};
39use foundry_common::{
40    FoundryTransactionBuilder, TransactionMaybeSigned,
41    provider::{
42        ProviderBuilder,
43        fee::{estimate_eip1559_fees, resolve_broadcast_eip1559_fees},
44    },
45    shell,
46    tempo::{TempoSponsor, maybe_print_fee_token, resolve_and_set_fee_token},
47};
48use foundry_config::Config;
49use foundry_evm::{
50    core::{
51        constants::DEFAULT_CREATE2_DEPLOYER_CODEHASH,
52        evm::{FoundryEvmNetwork, TempoEvmNetwork},
53    },
54    traces::CallKind,
55};
56use foundry_wallets::{
57    TempoAccountsWallet,
58    wallet_browser::{error::BrowserWalletError, signer::BrowserSigner},
59};
60use futures::{FutureExt, StreamExt, future::join_all, stream::FuturesUnordered};
61use itertools::Itertools;
62use tempo_alloy::{
63    TempoNetwork,
64    rpc::{TempoTransactionReceipt, TempoTransactionRequest},
65};
66use tempo_primitives::transaction::{Call, TempoTxEnvelope};
67
68/// Represents how to send a single transaction.
69#[derive(Clone)]
70pub enum SendTransactionKind<'a, N: Network> {
71    Unlocked(N::TransactionRequest),
72    Raw(N::TransactionRequest, &'a EthereumWallet),
73    Browser(N::TransactionRequest, &'a BrowserSigner<N>),
74    Signed(N::TxEnvelope),
75    AccessKey(N::TransactionRequest, Box<TempoAccountsWallet>),
76    PreparedRaw(Bytes, TxHash),
77}
78
79impl<'a, N: Network> SendTransactionKind<'a, N>
80where
81    N::TxEnvelope: From<Signed<N::UnsignedTx>>,
82    N::UnsignedTx: SignableTransaction<Signature>,
83    N::TransactionRequest: FoundryTransactionBuilder<N>,
84{
85    /// Prepares the transaction for broadcasting by synchronizing nonce and estimating gas.
86    ///
87    /// This method performs two key operations:
88    /// 1. Nonce synchronization: Waits for the provider's nonce to catch up to the expected
89    ///    transaction nonce when doing sequential broadcast
90    /// 2. Gas estimation: Re-estimates gas right before broadcasting for chains that require it
91    #[allow(clippy::too_many_arguments)]
92    pub async fn prepare(
93        &mut self,
94        provider: &RootProvider<N>,
95        sequential_broadcast: bool,
96        is_fixed_gas_limit: bool,
97        estimate_via_rpc: bool,
98        estimate_multiplier: u64,
99        tempo_sponsor: Option<&TempoSponsor>,
100    ) -> Result<()> {
101        let browser = matches!(self, Self::Browser(..));
102        let (tx, tempo_wallet) = match self {
103            Self::Raw(tx, _) | Self::Unlocked(tx) | Self::Browser(tx, _) => (tx, None),
104            Self::AccessKey(tx, wallet) => (tx, Some(wallet)),
105            Self::Signed(_) | Self::PreparedRaw(..) => return Ok(()),
106        };
107
108        reject_access_key_create::<N>(tx, tempo_wallet.is_some())?;
109
110        if sequential_broadcast {
111            let from = tx.from().expect("no sender");
112
113            let tx_nonce = tx.nonce().expect("no nonce");
114            for attempt in 0..5 {
115                let nonce = provider.get_transaction_count(from).await?;
116                match nonce.cmp(&tx_nonce) {
117                    Ordering::Greater => {
118                        bail!(
119                            "EOA nonce changed unexpectedly while sending transactions. Expected {tx_nonce} got {nonce} from provider."
120                        );
121                    }
122                    Ordering::Less => {
123                        if attempt == 4 {
124                            bail!(
125                                "After 5 attempts, provider nonce ({nonce}) is still behind expected nonce ({tx_nonce})."
126                            );
127                        }
128                        warn!(
129                            "Expected nonce ({tx_nonce}) is ahead of provider nonce ({nonce}). Retrying in 1 second..."
130                        );
131                        tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
132                    }
133                    Ordering::Equal => {
134                        // Nonces are equal, we can proceed.
135                        break;
136                    }
137                }
138            }
139        }
140
141        if let Some(wallet) = tempo_wallet {
142            **wallet = tx.prepare_with_tempo_wallet(provider, wallet).await?;
143        }
144
145        let fee_token = if let Some(sponsor) = tempo_sponsor {
146            sponsor.resolve_and_set_fee_token(Some(provider), tx).await?;
147            None
148        } else {
149            resolve_and_set_fee_token(Some(provider), tx, tx.from()).await?
150        };
151
152        // A fee token, sponsor, validity window, or other Tempo field selects
153        // the AA transaction type. AA requests carry CREATE as their first
154        // call rather than as the Ethereum transaction `to` field.
155        convert_tempo_aa_create::<N>(tx);
156
157        // Chains which use `eth_estimateGas` are being sent sequentially and require their
158        // gas to be re-estimated right before broadcasting.
159        if !is_fixed_gas_limit && estimate_via_rpc {
160            estimate_gas(tx, provider, estimate_multiplier, browser).await?;
161        }
162
163        if let Some(sponsor) = tempo_sponsor {
164            let from = tx.from().expect("no sender");
165            sponsor.attach_and_print::<N>(tx, from).await?;
166        } else {
167            maybe_print_fee_token(Some(provider), fee_token).await?;
168        }
169
170        Ok(())
171    }
172
173    #[allow(clippy::too_many_arguments)]
174    async fn prepare_for_durable_send(
175        mut self,
176        provider: &RootProvider<N>,
177        sequential_broadcast: bool,
178        is_fixed_gas_limit: bool,
179        estimate_via_rpc: bool,
180        estimate_multiplier: u64,
181        tempo_sponsor: Option<&TempoSponsor>,
182    ) -> Result<PreparedTransaction<'a, N>> {
183        self.prepare(
184            provider,
185            sequential_broadcast,
186            is_fixed_gas_limit,
187            estimate_via_rpc,
188            estimate_multiplier,
189            tempo_sponsor,
190        )
191        .await?;
192        if let Self::Unlocked(tx) = &mut self {
193            tx.prep_for_submission();
194        }
195        Ok(match self {
196            Self::Raw(tx, signer) => {
197                let signed = tx.build(signer).await?;
198                PreparedTransaction::Raw(Bytes::from(signed.encoded_2718()), signed.trie_hash())
199            }
200            Self::Signed(tx) => {
201                let hash = tx.trie_hash();
202                PreparedTransaction::Raw(Bytes::from(tx.encoded_2718()), hash)
203            }
204            Self::AccessKey(tx, wallet) => {
205                let payload = Bytes::from(tx.sign_with_tempo_wallet(&wallet).await?);
206                let envelope = N::TxEnvelope::decode_2718_exact(&payload)?;
207                PreparedTransaction::Raw(payload, envelope.trie_hash())
208            }
209            Self::PreparedRaw(payload, hash) => PreparedTransaction::Raw(payload, hash),
210            Self::Unlocked(request) => PreparedTransaction::Unlocked(request),
211            Self::Browser(request, signer) => PreparedTransaction::Browser(request, signer),
212        })
213    }
214}
215
216/// A prepared submission, with local signatures fixed before checkpointing and sending.
217#[derive(Clone)]
218enum PreparedTransaction<'a, N: Network> {
219    Raw(Bytes, TxHash),
220    Unlocked(N::TransactionRequest),
221    Browser(N::TransactionRequest, &'a BrowserSigner<N>),
222}
223
224impl<N: Network> PreparedTransaction<'_, N>
225where
226    N::TransactionRequest: FoundryTransactionBuilder<N>,
227{
228    const fn delegated_request(&self) -> Option<&N::TransactionRequest> {
229        match self {
230            Self::Unlocked(request) | Self::Browser(request, _) => Some(request),
231            _ => None,
232        }
233    }
234
235    fn validate_delegated_submission(&self) -> Result<()> {
236        let Self::Browser(request, signer) = self else { return Ok(()) };
237        if request.from().is_some_and(|from| from != signer.address()) {
238            bail!("Transaction `from` address does not match connected wallet address");
239        }
240        if request.chain_id().is_some_and(|chain_id| chain_id != signer.chain_id()) {
241            bail!("Transaction `chainId` does not match connected wallet chain ID");
242        }
243        Ok(())
244    }
245
246    fn is_definite_non_submission(&self, error: &eyre::Report) -> bool {
247        match self {
248            Self::Browser(..) => is_definite_browser_non_submission(error),
249            Self::Unlocked(_) => is_definite_unlocked_non_submission(error),
250            _ => false,
251        }
252    }
253
254    async fn send(self, provider: Arc<RootProvider<N>>) -> Result<TxHash> {
255        match self {
256            Self::Raw(payload, _) => {
257                let pending = provider.send_raw_transaction(&payload).await?;
258                Ok(*pending.tx_hash())
259            }
260            Self::Unlocked(tx) => {
261                debug!("sending transaction from unlocked account {:?}", tx);
262                let pending = provider.send_transaction(tx).await?;
263                Ok(*pending.tx_hash())
264            }
265            Self::Browser(tx, signer) => {
266                debug!("sending transaction: {:?}", tx);
267                Ok(signer.send_transaction_via_browser(tx).await?)
268            }
269        }
270    }
271}
272
273fn is_definite_browser_non_submission(error: &eyre::Report) -> bool {
274    let Some(alloy_signer::Error::Other(error)) = error.downcast_ref::<alloy_signer::Error>()
275    else {
276        return false;
277    };
278    matches!(
279        error.downcast_ref::<BrowserWalletError>(),
280        Some(BrowserWalletError::Rejected { .. } | BrowserWalletError::NotConnected)
281    )
282}
283
284fn is_definite_unlocked_non_submission(error: &eyre::Report) -> bool {
285    matches!(
286        error.downcast_ref::<TransportError>(),
287        Some(RpcError::ErrorResp(error))
288            if is_unlocked_non_submission(error.code, &error.message)
289    )
290}
291
292fn is_unlocked_non_submission(code: i64, message: &str) -> bool {
293    code == -32602 && message == "No Signer available"
294}
295
296fn tempo_batch_calls(
297    sequence: &ScriptSequence<TempoNetwork>,
298    first_operation: usize,
299) -> Result<Vec<Call>> {
300    sequence
301        .transactions()
302        .skip(first_operation)
303        .enumerate()
304        .map(|(call_index, tx)| {
305            if tx.authorization_list().is_some_and(|list| !list.is_empty()) {
306                bail!(
307                    "--batch does not support EIP-7702 authorization lists \
308                     (found at transaction {}); use regular broadcast instead.",
309                    call_index + first_operation + 1
310                );
311            }
312            if let TransactionMaybeSigned::Unsigned(inner) = tx
313                && inner.blob_sidecar().is_some()
314            {
315                bail!(
316                    "--batch does not support blob (EIP-4844) transactions \
317                     (found at transaction {}); use regular broadcast instead.",
318                    call_index + first_operation + 1
319                );
320            }
321            let to = tx.to().map(TxKind::Call).ok_or_else(|| {
322                eyre::eyre!(
323                    "Unexpected raw CREATE in --batch mode at position {} — \
324                     this is a bug; CREATEs should have been rewritten by the inspector.",
325                    call_index + first_operation + 1
326                )
327            })?;
328            Ok(Call {
329                to,
330                value: tx.value().unwrap_or(U256::ZERO),
331                input: tx.input().cloned().unwrap_or_default(),
332            })
333        })
334        .collect()
335}
336
337fn validate_tempo_batch_request(
338    request: &TempoTransactionRequest,
339    sender: Address,
340    chain: u64,
341    calls: &[Call],
342) -> Result<()> {
343    if request.inner.from != Some(sender) {
344        bail!("batch request does not match its planned sender");
345    }
346    let expected = request
347        .clone()
348        .build_aa()
349        .map_err(|error| eyre::eyre!("invalid persisted batch request: {error}"))?;
350    if expected.chain_id != chain || expected.calls != calls {
351        bail!("batch request does not match its planned operations");
352    }
353    Ok(())
354}
355
356fn validate_tempo_batch_payload(
357    request: &TempoTransactionRequest,
358    payload: &[u8],
359    sender: Address,
360    chain: u64,
361    calls: &[Call],
362) -> Result<TxHash> {
363    validate_tempo_batch_request(request, sender, chain, calls)?;
364    let expected = request
365        .clone()
366        .build_aa()
367        .map_err(|error| eyre::eyre!("invalid persisted batch request: {error}"))?;
368    let envelope = TempoTxEnvelope::decode_2718_exact(payload)
369        .wrap_err("recovery plan contains an invalid signed batch payload")?;
370    validate_tempo_batch_envelope(&envelope, sender, chain, calls)?;
371    let TempoTxEnvelope::AA(signed) = &envelope else {
372        unreachable!();
373    };
374    if signed.tx() != &expected {
375        bail!("signed batch payload does not match its persisted request");
376    }
377    Ok(envelope.trie_hash())
378}
379
380fn validate_tempo_batch_envelope(
381    envelope: &TempoTxEnvelope,
382    sender: Address,
383    chain: u64,
384    calls: &[Call],
385) -> Result<()> {
386    let TempoTxEnvelope::AA(signed) = envelope else {
387        bail!("recovered batch transaction is not a Tempo transaction");
388    };
389    if signed.recover_signer().wrap_err("recovered batch transaction has an invalid signature")?
390        != sender
391    {
392        bail!("recovered batch transaction does not match its planned sender");
393    }
394    if signed.tx().chain_id != chain || signed.tx().calls != calls {
395        bail!("recovered batch transaction does not match its planned operations");
396    }
397    Ok(())
398}
399
400pub(crate) fn remaining_unsigned_transactions_for_recovery<N: Network>(
401    sequence: &ScriptSequenceKind<N>,
402) -> Vec<SignerScope>
403where
404    N::TxEnvelope: for<'de> serde::Deserialize<'de> + serde::Serialize,
405{
406    sequence
407        .sequences()
408        .iter()
409        .enumerate()
410        .filter(|(sequence_index, _)| sequence.batch_attempt(*sequence_index).is_none())
411        .flat_map(|(sequence_index, deployment)| {
412            remaining_operation_indices(sequence, sequence_index).into_iter().filter_map(
413                move |index| {
414                    let tx = deployment.transactions[index].tx();
415                    (tx.is_unsigned() && sequence.signed_payload(sequence_index, index).is_none())
416                        .then(|| SignerScope {
417                            chain: deployment.chain,
418                            sender: tx.from().expect("missing from"),
419                        })
420                },
421            )
422        })
423        .collect()
424}
425
426fn remaining_operation_indices<N: Network>(
427    sequence: &ScriptSequenceKind<N>,
428    sequence_index: usize,
429) -> Vec<usize>
430where
431    N::TxEnvelope: for<'de> serde::Deserialize<'de> + serde::Serialize,
432{
433    let deployment = &sequence.sequences()[sequence_index];
434    deployment
435        .transactions
436        .iter()
437        .enumerate()
438        .filter_map(|(index, transaction)| {
439            let hash = sequence
440                .signed_payload(sequence_index, index)
441                .map(|signed| signed.hash)
442                .or_else(|| match sequence.delegated_status(sequence_index, index) {
443                    Some(DelegatedStatus::Pending { hash }) => Some(hash),
444                    _ => None,
445                })
446                .or(transaction.hash)
447                .or_else(|| match transaction.tx() {
448                    TransactionMaybeSigned::Signed { tx, .. } => Some(tx.trie_hash()),
449                    TransactionMaybeSigned::Unsigned(_) => None,
450                });
451            let completed = hash.is_some_and(|hash| {
452                deployment.receipts.iter().any(|receipt| receipt.transaction_hash() == hash)
453            });
454            (!completed).then_some(index)
455        })
456        .collect()
457}
458
459fn remaining_sender_addresses<N: Network>(sequence: &ScriptSequenceKind<N>) -> AddressHashSet
460where
461    N::TxEnvelope: for<'de> serde::Deserialize<'de> + serde::Serialize,
462{
463    sequence
464        .sequences()
465        .iter()
466        .enumerate()
467        .flat_map(|(sequence_index, deployment)| {
468            remaining_operation_indices(sequence, sequence_index)
469                .into_iter()
470                .filter_map(|index| deployment.transactions[index].tx().from())
471        })
472        .collect()
473}
474
475const fn should_broadcast_sequentially(
476    estimate_via_rpc: bool,
477    slow: bool,
478    required_signers: usize,
479    ordering_senders: usize,
480    batch_supported: bool,
481) -> bool {
482    estimate_via_rpc || slow || required_signers == 0 || ordering_senders != 1 || !batch_supported
483}
484
485/// Represents how to send _all_ transactions
486pub enum SendTransactionsKind<N: Network> {
487    /// Send via `eth_sendTransaction` and rely on the  `from` address being unlocked.
488    Unlocked(AddressHashSet),
489    /// Send a signed transaction via `eth_sendRawTransaction`, or via browser
490    Raw {
491        eth_wallets: AddressHashMap<EthereumWallet>,
492        browser: Option<BrowserSigner<N>>,
493        access_keys: HashMap<SignerScope, TempoAccountsWallet>,
494    },
495}
496
497impl<N: Network> SendTransactionsKind<N> {
498    /// Returns the [`SendTransactionKind`] for the given address
499    ///
500    /// Returns an error if no matching signer is found or the address is not unlocked
501    pub fn for_sender(
502        &self,
503        chain: u64,
504        addr: &Address,
505        tx: N::TransactionRequest,
506    ) -> Result<SendTransactionKind<'_, N>> {
507        match self {
508            Self::Unlocked(unlocked) => {
509                if !unlocked.contains(addr) {
510                    bail!("Sender address {:?} is not unlocked", addr);
511                }
512                Ok(SendTransactionKind::Unlocked(tx))
513            }
514            Self::Raw { eth_wallets, browser, access_keys } => {
515                if let Some(wallet) = access_keys.get(&SignerScope::new(chain, *addr)) {
516                    Ok(SendTransactionKind::AccessKey(tx, Box::new(wallet.clone())))
517                } else if let Some(wallet) = eth_wallets.get(addr) {
518                    Ok(SendTransactionKind::Raw(tx, wallet))
519                } else if let Some(b) = browser
520                    && b.address() == *addr
521                {
522                    Ok(SendTransactionKind::Browser(tx, b))
523                } else {
524                    bail!("No matching signer for {:?} found", addr);
525                }
526            }
527        }
528    }
529}
530
531/// State after we have bundled all
532/// [`TransactionWithMetadata`](forge_script_sequence::TransactionWithMetadata) objects into a
533/// single [`ScriptSequenceKind`] object containing one or more script sequences.
534pub struct BundledState<FEN: FoundryEvmNetwork> {
535    pub args: ScriptArgs,
536    pub script_config: ScriptConfig<FEN>,
537    pub script_wallets: Wallets,
538    pub browser_wallet: Option<BrowserSigner<FEN::Network>>,
539    pub build_data: LinkedBuildData,
540    pub sequence: ScriptSequenceKind<FEN::Network>,
541}
542
543impl<FEN: FoundryEvmNetwork> BundledState<FEN> {
544    pub async fn wait_for_pending(mut self) -> Result<Self> {
545        let progress = ScriptProgress::default();
546        let progress_ref = &progress;
547        let config = &self.script_config.config;
548        let submission_hashes = (0..self.sequence.sequences().len())
549            .map(|sequence| self.sequence.submission_hashes(sequence))
550            .collect::<Vec<_>>();
551        let futs = self
552            .sequence
553            .sequences_mut()
554            .iter_mut()
555            .zip(submission_hashes)
556            .enumerate()
557            .map(|(sequence_idx, (sequence, (durable_hashes, replayable_hashes)))| async move {
558                let rpc_url = sequence.rpc_url();
559                let provider =
560                    Arc::new(ProviderBuilder::from_config_with_url(config, rpc_url)?.build()?);
561                progress_ref
562                    .wait_for_pending(
563                        sequence_idx,
564                        sequence,
565                        &provider,
566                        self.script_config.config.transaction_timeout,
567                        self.args.confirmations,
568                        (&durable_hashes, &replayable_hashes),
569                    )
570                    .await
571            })
572            .collect::<Vec<_>>();
573
574        let errors = join_all(futs).await.into_iter().filter_map(Result::err).collect::<Vec<_>>();
575
576        self.sequence.save(true, false)?;
577
578        if !errors.is_empty() {
579            return Err(eyre::eyre!("{}", errors.iter().format("\n")));
580        }
581
582        Ok(self)
583    }
584
585    /// Broadcasts transactions from all sequences.
586    pub async fn broadcast(mut self) -> Result<BroadcastedState<FEN>> {
587        let remaining_transactions = remaining_unsigned_transactions_for_recovery(&self.sequence);
588        let ordering_addresses = remaining_sender_addresses(&self.sequence);
589        let has_unprepared_transactions =
590            self.sequence.sequences().iter().enumerate().any(|(sequence_index, _)| {
591                remaining_operation_indices(&self.sequence, sequence_index)
592                    .into_iter()
593                    .any(|index| self.sequence.signed_payload(sequence_index, index).is_none())
594            });
595        let required_addresses =
596            remaining_transactions.iter().map(|tx| tx.sender).collect::<AddressHashSet>();
597
598        if required_addresses.contains(&Config::DEFAULT_SENDER) {
599            eyre::bail!(
600                "You seem to be using Foundry's default sender. Be sure to set your own --sender."
601            );
602        }
603
604        let send_kind = if required_addresses.is_empty() {
605            SendTransactionsKind::Raw {
606                eth_wallets: AddressHashMap::default(),
607                browser: None,
608                access_keys: HashMap::default(),
609            }
610        } else if self.args.unlocked {
611            SendTransactionsKind::Unlocked(required_addresses.clone())
612        } else {
613            let expected_session_sender = script_session_expected_sender_if_configured(
614                &self.script_config.tempo,
615                &required_addresses,
616            )?;
617
618            // For addresses without an explicit signer, try the Tempo Accounts store.
619            let mut access_keys: HashMap<SignerScope, TempoAccountsWallet> = HashMap::default();
620            if let Some(expected_session_sender) = expected_session_sender
621                && let Some(session) =
622                    self.script_config.tempo.session_signer_for_multi_wallet_any_chain(
623                        &self.args.wallets,
624                        Some(expected_session_sender),
625                    )?
626            {
627                insert_session_access_key_for_remaining_transactions(
628                    &mut access_keys,
629                    session,
630                    &remaining_transactions,
631                )?;
632            }
633
634            let signers: Vec<Address> = self
635                .script_wallets
636                .signers()
637                .map_err(|e| eyre::eyre!("{e}"))?
638                .into_iter()
639                .chain(self.browser_wallet.as_ref().map(|b| b.address()))
640                .collect();
641
642            let mut missing_addresses = Vec::new();
643            let accounts_wallet = self
644                .script_config
645                .evm_opts
646                .networks
647                .is_tempo()
648                .then(TempoAccountsWallet::try_from_default_store)
649                .transpose()?
650                .flatten();
651
652            for tx in &remaining_transactions {
653                let scope = *tx;
654                if !signers.contains(&tx.sender) && !access_keys.contains_key(&scope) {
655                    if let Some(wallet) = accounts_wallet.as_ref()
656                        && wallet.has_account(tx.sender)?
657                    {
658                        access_keys.insert(scope, wallet.clone().with_chain_id(tx.chain));
659                    } else {
660                        missing_addresses.push(tx.sender);
661                    }
662                }
663            }
664
665            missing_addresses.sort_unstable();
666            missing_addresses.dedup();
667
668            if !missing_addresses.is_empty() {
669                eyre::bail!(
670                    "No associated wallet for addresses: {:?}. Unlocked wallets: {:?}",
671                    missing_addresses,
672                    signers
673                );
674            }
675
676            let signers = self.script_wallets.into_multi_wallet().into_signers()?;
677            let eth_wallets: AddressHashMap<EthereumWallet> =
678                signers.into_iter().map(|(addr, signer)| (addr, signer.into())).collect();
679
680            SendTransactionsKind::Raw { eth_wallets, browser: self.browser_wallet, access_keys }
681        };
682
683        let tempo_sponsor = if has_unprepared_transactions {
684            self.script_config.tempo.sponsor_config().await?.map(Arc::new)
685        } else {
686            None
687        };
688        if tempo_sponsor.is_some()
689            && self.script_config.tempo.sponsor_sig.is_some()
690            && remaining_transactions.len() > 1
691        {
692            eyre::bail!(
693                "--tempo.sponsor-sig can only sponsor one remaining script transaction; use --tempo.sponsor-signer for multi-transaction scripts"
694            );
695        }
696
697        let progress = ScriptProgress::default();
698
699        for i in 0..self.sequence.sequences().len() {
700            let remaining_indices = remaining_operation_indices(&self.sequence, i);
701            let signed_payloads = (0..self.sequence.sequences()[i].transactions.len())
702                .map(|index| {
703                    self.sequence
704                        .signed_payload(i, index)
705                        .map(|signed| (signed.payload.clone(), signed.hash))
706                })
707                .collect::<Vec<_>>();
708            let mut sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
709
710            let provider = Arc::new(
711                ProviderBuilder::from_config_with_url(
712                    &self.script_config.config,
713                    sequence.rpc_url(),
714                )?
715                .build()?,
716            );
717            let unlocked_submission_provider = Arc::new(
718                ProviderBuilder::from_config_with_url(
719                    &self.script_config.config,
720                    sequence.rpc_url(),
721                )?
722                .max_retry(0)
723                .build()?,
724            );
725
726            let seq_progress = progress.get_sequence_progress(i, sequence);
727
728            if !remaining_indices.is_empty() {
729                let is_legacy = Chain::from(sequence.chain).is_legacy() || self.args.legacy;
730                // Make a one-time gas price estimation
731                let all_signed =
732                    remaining_indices.iter().all(|index| signed_payloads[*index].is_some());
733                let (gas_price, eip1559_fees) = match (
734                    all_signed,
735                    is_legacy,
736                    self.args.with_gas_price,
737                    self.args.priority_gas_price,
738                ) {
739                    (true, _, _, _) => (None, None),
740                    (false, true, Some(gas_price), _) => (Some(gas_price.to()), None),
741                    (false, true, None, _) => (Some(provider.get_gas_price().await?), None),
742                    (false, false, Some(max_fee_per_gas), Some(max_priority_fee_per_gas)) => {
743                        let max_fee: u128 = max_fee_per_gas.to();
744                        let max_priority: u128 = max_priority_fee_per_gas.to();
745                        if max_priority > max_fee {
746                            eyre::bail!(
747                                "--priority-gas-price ({max_priority}) cannot be higher than --with-gas-price ({max_fee})"
748                            );
749                        }
750                        (
751                            None,
752                            Some(Eip1559Estimation {
753                                max_fee_per_gas: max_fee,
754                                max_priority_fee_per_gas: max_priority,
755                            }),
756                        )
757                    }
758                    (false, false, _, _) => {
759                        let fees = estimate_eip1559_fees(
760                            &provider,
761                            self.script_config.config.eip1559_fee_estimate,
762                        )
763                        .await
764                        .wrap_err("Failed to estimate EIP1559 fees. This chain might not support EIP1559, try adding --legacy to your command.")?;
765
766                        // Browser wallets may suggest their own tip; query it best-effort.
767                        let browser_suggested_tip = if matches!(
768                            &send_kind,
769                            SendTransactionsKind::Raw { browser: Some(_), .. }
770                        ) {
771                            provider.get_max_priority_fee_per_gas().await.ok()
772                        } else {
773                            None
774                        };
775
776                        let fees = resolve_broadcast_eip1559_fees(
777                            fees,
778                            self.args.with_gas_price.map(|p| p.to()),
779                            self.args.priority_gas_price.map(|p| p.to()),
780                            browser_suggested_tip,
781                        )?;
782
783                        (None, Some(fees.estimation()))
784                    }
785                };
786
787                // Iterate through transactions, matching the `from` field with the associated
788                // wallet. Then send the transaction. Panics if we find a unknown `from`
789                let sequence_chain = sequence.chain;
790                let mut transactions = Vec::with_capacity(remaining_indices.len());
791                for index in remaining_indices {
792                    let tx_with_metadata = &sequence.transactions[index];
793                    let is_fixed_gas_limit = tx_with_metadata.is_fixed_gas_limit;
794
795                    let kind = match (&signed_payloads[index], tx_with_metadata.tx().clone()) {
796                        (Some((payload, hash)), _) => {
797                            SendTransactionKind::PreparedRaw(payload.clone(), *hash)
798                        }
799                        (None, TransactionMaybeSigned::Signed { tx, .. }) => {
800                            if tempo_sponsor.is_some() {
801                                eyre::bail!(
802                                    "cannot attach Tempo sponsor signature to an already signed script transaction"
803                                );
804                            }
805                            SendTransactionKind::Signed(tx)
806                        }
807                        (None, TransactionMaybeSigned::Unsigned(mut tx)) => {
808                            let from = tx.from().expect("No sender for onchain transaction!");
809
810                            tx.set_chain_id(sequence_chain);
811
812                            // Set TxKind::Create explicitly to satisfy `check_reqd_fields` in
813                            // alloy
814                            if tx.kind().is_none() {
815                                tx.set_create();
816                            }
817
818                            if let Some(gas_price) = gas_price {
819                                tx.set_gas_price(gas_price);
820                            } else {
821                                let eip1559_fees = eip1559_fees.expect("was set above");
822                                tx.set_max_priority_fee_per_gas(
823                                    eip1559_fees.max_priority_fee_per_gas,
824                                );
825                                tx.set_max_fee_per_gas(eip1559_fees.max_fee_per_gas);
826                            }
827
828                            self.script_config.tempo.apply::<FEN::Network>(&mut tx, None);
829
830                            send_kind.for_sender(sequence_chain, &from, tx)?
831                        }
832                    };
833                    transactions.push((kind, is_fixed_gas_limit, index));
834                }
835
836                let estimate_via_rpc = has_different_gas_calc(sequence.chain)
837                    || self.script_config.evm_opts.networks.is_tempo()
838                    || self.args.skip_simulation;
839
840                // We only wait for a transaction receipt before sending the next transaction, if
841                // there is more than one signer. There would be no way of assuring
842                // their order otherwise.
843                // Or if the chain does not support batched transactions (eg. Arbitrum).
844                // Or if we need to invoke eth_estimateGas before sending transactions.
845                let sequential_broadcast = should_broadcast_sequentially(
846                    estimate_via_rpc,
847                    self.args.slow,
848                    required_addresses.len(),
849                    ordering_addresses.len(),
850                    has_batch_support(sequence.chain),
851                ) || transactions.iter().any(|(kind, _, _)| {
852                    matches!(
853                        kind,
854                        SendTransactionKind::Browser(..) | SendTransactionKind::Unlocked(..)
855                    )
856                });
857
858                // We send transactions and wait for receipts in batches of 100, since some networks
859                // cannot handle more than that.
860                let batch_size = if sequential_broadcast { 1 } else { 100 };
861
862                for (batch_number, batch) in transactions.chunks(batch_size).enumerate() {
863                    seq_progress.inner.write().set_status(&format!(
864                        "Sending transactions [{} - {}]",
865                        batch_number * batch_size,
866                        batch_number * batch_size + std::cmp::min(batch_size, batch.len()) - 1
867                    ));
868
869                    let mut prepared = Vec::with_capacity(batch.len());
870                    for (kind, is_fixed_gas_limit, index) in batch {
871                        let mut kind = kind
872                            .clone()
873                            .prepare_for_durable_send(
874                                &provider,
875                                sequential_broadcast,
876                                *is_fixed_gas_limit,
877                                estimate_via_rpc,
878                                self.args.gas_estimate_multiplier,
879                                tempo_sponsor.as_deref(),
880                            )
881                            .await?;
882                        if let PreparedTransaction::Raw(payload, hash) = &mut kind {
883                            *hash =
884                                self.sequence.persist_signed_payload(i, *index, payload.clone())?;
885                        }
886                        prepared.push((kind, *index));
887                    }
888
889                    let mut buffer = FuturesUnordered::new();
890                    for (kind, index) in prepared {
891                        if let Some(request) = kind.delegated_request() {
892                            kind.validate_delegated_submission()?;
893                            self.sequence.persist_delegated_request(i, index, request.clone())?;
894                        }
895                        let provider = if matches!(kind, PreparedTransaction::Unlocked(_)) {
896                            unlocked_submission_provider.clone()
897                        } else {
898                            provider.clone()
899                        };
900                        let mut pending = async move {
901                            let res = kind.clone().send(provider).await;
902                            (res, kind, 0, None, index)
903                        }
904                        .boxed();
905                        if let Some(result) = pending.as_mut().now_or_never() {
906                            buffer.push(futures::future::ready(result).boxed());
907                        } else {
908                            buffer.push(pending);
909                        }
910                    }
911
912                    'send: while let Some((mut res, kind, attempt, original_res, index)) =
913                        buffer.next().await
914                    {
915                        if res.is_err()
916                            && let PreparedTransaction::Raw(_, hash) = kind
917                            && provider
918                                .get_transaction_by_hash(hash)
919                                .await
920                                .is_ok_and(|transaction| transaction.is_some())
921                        {
922                            res = Ok(hash);
923                        }
924                        if kind.delegated_request().is_some()
925                            && let Err(error) = res
926                        {
927                            if kind.is_definite_non_submission(&error) {
928                                self.sequence.clear_delegated_request(i, index)?;
929                                return Err(error);
930                            }
931                            self.sequence.persist_delegated_status(
932                                i,
933                                index,
934                                DelegatedStatus::OutcomeUnknown,
935                            )?;
936                            bail!(
937                                "submission outcome for delegated operation {index} is unknown; refusing to risk a duplicate transaction"
938                            );
939                        }
940                        if res.is_err() && attempt <= 3 {
941                            // Try to resubmit the transaction
942                            let provider = provider.clone();
943                            let progress = seq_progress.inner.clone();
944                            buffer.push(Box::pin(async move {
945                                debug!(err=?res, ?attempt, "retrying transaction ");
946                                let attempt = attempt + 1;
947                                progress.write().set_status(&format!(
948                                    "retrying transaction {res:?} (attempt {attempt})"
949                                ));
950                                tokio::time::sleep(Duration::from_millis(1000 * attempt)).await;
951                                let r = kind.clone().send(provider).await;
952                                (r, kind, attempt, original_res.or(Some(res)), index)
953                            }));
954
955                            continue 'send;
956                        }
957
958                        // Preserve the original error if any
959                        let tx_hash = res.wrap_err_with(|| {
960                                if let Some(original_res) = original_res {
961                                    format!(
962                                        "Failed to send transaction after {attempt} attempts {original_res:?}"
963                                    )
964                                } else {
965                                    "Failed to send transaction".to_string()
966                                }
967                            })?;
968                        if let PreparedTransaction::Raw(_, expected) = kind
969                            && expected != tx_hash
970                        {
971                            bail!("RPC returned hash {tx_hash} for signed payload {expected}");
972                        }
973                        if kind.delegated_request().is_some() {
974                            self.sequence.persist_delegated_status(
975                                i,
976                                index,
977                                DelegatedStatus::Pending { hash: tx_hash },
978                            )?;
979                        }
980                        sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
981                        sequence.add_pending(index, tx_hash);
982
983                        // Checkpoint save
984                        self.sequence.save(true, false)?;
985
986                        seq_progress.inner.write().tx_sent(tx_hash);
987                    }
988
989                    // Checkpoint save
990                    self.sequence.save(true, false)?;
991                    let (durable_hashes, replayable_hashes) = self.sequence.submission_hashes(i);
992                    sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
993
994                    progress
995                        .wait_for_pending(
996                            i,
997                            sequence,
998                            &provider,
999                            self.script_config.config.transaction_timeout,
1000                            self.args.confirmations,
1001                            (&durable_hashes, &replayable_hashes),
1002                        )
1003                        .await?;
1004                    self.sequence.ensure_delegated_outcomes_known(i)?;
1005
1006                    // Checkpoint save
1007                    self.sequence.save(true, false)?;
1008                    sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
1009                }
1010            }
1011
1012            let (total_gas, avg_gas_price, total_paid) = fee_totals(
1013                sequence.receipts.iter().map(|r| (r.gas_used(), r.effective_gas_price())),
1014            );
1015            let paid = format_units(total_paid, 18).unwrap_or_else(|_| "N/A".to_string());
1016            let avg_gas_price = avg_gas_price
1017                .and_then(|avg| format_units(avg, 9).ok())
1018                .unwrap_or_else(|| "N/A".to_string());
1019
1020            let token_symbol = NamedChain::try_from(sequence.chain)
1021                .unwrap_or_default()
1022                .native_currency_symbol()
1023                .unwrap_or("ETH");
1024            seq_progress.inner.write().set_status(&format!(
1025                "Total Paid: {} {} ({} gas * avg {} gwei)\n",
1026                paid.trim_end_matches('0'),
1027                token_symbol,
1028                total_gas,
1029                avg_gas_price.trim_end_matches('0').trim_end_matches('.')
1030            ));
1031            seq_progress.inner.write().finish();
1032        }
1033
1034        if !shell::is_json() {
1035            sh_println!("\n\n==========================")?;
1036            sh_println!("\nONCHAIN EXECUTION COMPLETE & SUCCESSFUL.")?;
1037        }
1038
1039        Ok(BroadcastedState {
1040            args: self.args,
1041            script_config: self.script_config,
1042            build_data: self.build_data,
1043            sequence: self.sequence,
1044        })
1045    }
1046
1047    pub async fn verify_preflight_check(&self) -> Result<()> {
1048        if self.args.verify_external && self.script_config.config.offline {
1049            bail!("External contract verification is unavailable in offline mode");
1050        }
1051
1052        for sequence in self.sequence.sequences() {
1053            let chain: Chain = sequence.chain.into();
1054            // Resolve the API key: CLI arg first, then per-chain config, then global fallback.
1055            let etherscan_key = self
1056                .script_config
1057                .config
1058                .get_etherscan_api_key(Some(chain))
1059                .or_else(|| self.script_config.config.etherscan_api_key.clone());
1060            let api_key =
1061                self.args.verifier.resolve_api_key(etherscan_key.as_deref()).map(str::to_owned);
1062            let has_url = self.args.verifier.verifier_url.is_some();
1063            let is_explicit = self.args.verifier.is_explicitly_set();
1064            // Presence check: use the fully-resolved provider type so that implicit Etherscan
1065            // selection (key from env/config, no explicit --verifier flag) is validated too.
1066            self.args
1067                .verifier
1068                .resolve(api_key.as_deref(), Some(chain))
1069                .client(api_key.as_deref(), Some(chain), has_url, is_explicit)
1070                .wrap_err_with(|| {
1071                    format!("Verification preflight check failed for chain {}", sequence.chain)
1072                })?;
1073            // Connectivity check: validates credentials are actually accepted by the verifier.
1074            self.args
1075                .verifier
1076                .check_credentials(api_key.as_deref(), chain, &self.script_config.config)
1077                .await
1078                .wrap_err_with(|| {
1079                    format!("Verification preflight check failed for chain {}", sequence.chain)
1080                })?;
1081        }
1082
1083        Ok(())
1084    }
1085}
1086
1087impl BundledState<TempoEvmNetwork> {
1088    /// Broadcasts all transactions as a single Tempo batch transaction (type 0x76).
1089    ///
1090    /// This method collects all individual transactions from the script and combines them
1091    /// into a single batch transaction for atomic execution on Tempo.
1092    pub async fn broadcast_batch(mut self) -> Result<BroadcastedState<TempoEvmNetwork>> {
1093        // Batch mode only supports single chain for now
1094        if self.sequence.sequences().len() != 1 {
1095            bail!(
1096                "--batch mode only supports single-chain scripts. \
1097                 Use --multi without --batch for multi-chain."
1098            );
1099        }
1100
1101        let batch_resolution = if self.args.resume_tx_hash.is_some() {
1102            self.sequence
1103                .restore_delegated_pending(
1104                    self.args.resume_attempt,
1105                    self.args.resume_tx_hash,
1106                    false,
1107                )?
1108                .map(|(_, _, attempt, hash)| (attempt, hash))
1109        } else {
1110            None
1111        };
1112        let mut recovered_batch = self
1113            .sequence
1114            .batch_attempt(0)
1115            .map(|(first, attempt, kind)| (first, attempt, kind.clone()));
1116
1117        let sequence = self.sequence.sequences().first().unwrap();
1118        let total_transactions = sequence.transactions.len();
1119        let completed_prefix = completed_transaction_prefix(sequence)?;
1120        let batch_start =
1121            recovered_batch.as_ref().map(|(first, _, _)| *first).unwrap_or(completed_prefix);
1122
1123        if batch_start == total_transactions {
1124            sh_println!("No transactions to broadcast in batch mode.")?;
1125            return Ok(BroadcastedState {
1126                args: self.args,
1127                script_config: self.script_config,
1128                build_data: self.build_data,
1129                sequence: self.sequence,
1130            });
1131        }
1132
1133        // Reject pre-signed transactions: a batch is a single atomic tx from one sender,
1134        // so any tx already signed by another key would silently be re-attributed.
1135        if let Some((idx, _)) =
1136            sequence.transactions().enumerate().find(|(_, tx)| !tx.is_unsigned())
1137        {
1138            bail!(
1139                "--batch cannot include pre-signed transactions (found at position {}); \
1140                 batch mode signs a single atomic transaction from one sender.",
1141                idx + 1
1142            );
1143        }
1144
1145        // Collect sender addresses - batch mode requires single sender
1146        let senders: AddressHashSet = sequence
1147            .transactions()
1148            .skip(batch_start)
1149            .filter(|tx| tx.is_unsigned())
1150            .filter_map(|tx| tx.from())
1151            .collect();
1152
1153        if senders.len() != 1 {
1154            bail!(
1155                "--batch mode requires all transactions to have the same sender. \
1156                 Found {} unique senders: {:?}",
1157                senders.len(),
1158                senders
1159            );
1160        }
1161
1162        let sender = *senders.iter().next().unwrap();
1163        let chain_id = sequence.chain;
1164        let calls = tempo_batch_calls(sequence, batch_start)?;
1165        if let Some((_, _, kind)) = &recovered_batch {
1166            match kind {
1167                AttemptKind::Signed { request: Some(request), payload } => {
1168                    let hash = validate_tempo_batch_payload(
1169                        request,
1170                        &payload.payload,
1171                        sender,
1172                        chain_id,
1173                        &calls,
1174                    )?;
1175                    if hash != payload.hash {
1176                        bail!("recovery plan batch payload does not match its hash");
1177                    }
1178                }
1179                AttemptKind::Delegated { request, .. } => {
1180                    validate_tempo_batch_request(request, sender, chain_id, &calls)?;
1181                }
1182                AttemptKind::Legacy { .. } => {}
1183                AttemptKind::Signed { request: None, .. } => {
1184                    bail!("recovery plan batch payload has no persisted request");
1185                }
1186            }
1187        }
1188
1189        let rpc_url = sequence.rpc_url().to_owned();
1190        let _ = sequence;
1191        if let Some((attempt_id, hash)) = batch_resolution {
1192            let provider = ProviderBuilder::<TempoNetwork>::from_config_with_url(
1193                &self.script_config.config,
1194                &rpc_url,
1195            )?
1196            .build()?;
1197            let transaction = provider
1198                .get_transaction_by_hash(hash)
1199                .await?
1200                .context("resolved transaction is not available from the recovery endpoint")?;
1201            self.sequence.resolve_delegated_hash(attempt_id, hash, &transaction)?;
1202            recovered_batch = self
1203                .sequence
1204                .batch_attempt(0)
1205                .map(|(first, attempt, kind)| (first, attempt, kind.clone()));
1206        }
1207
1208        let recovered_hash = recovered_batch.as_ref().and_then(|(_, _, kind)| match kind {
1209            AttemptKind::Signed { payload, .. } => Some(payload.hash),
1210            AttemptKind::Delegated { status: DelegatedStatus::Pending { hash }, .. }
1211            | AttemptKind::Legacy { hash } => Some(*hash),
1212            AttemptKind::Delegated { .. } => None,
1213        });
1214        if completed_prefix == total_transactions {
1215            sh_println!("No transactions to broadcast in batch mode.")?;
1216            return Ok(BroadcastedState {
1217                args: self.args,
1218                script_config: self.script_config,
1219                build_data: self.build_data,
1220                sequence: self.sequence,
1221            });
1222        }
1223
1224        if matches!(
1225            recovered_batch,
1226            Some((_, _, AttemptKind::Delegated { status: DelegatedStatus::OutcomeUnknown, .. }))
1227        ) {
1228            bail!(
1229                "submission outcome for delegated Tempo batch is unknown; refusing to risk a duplicate transaction"
1230            );
1231        }
1232
1233        let sequence = self.sequence.sequences_mut().get_mut(0).unwrap();
1234
1235        if sender == Config::DEFAULT_SENDER {
1236            bail!(
1237                "You seem to be using Foundry's default sender. Be sure to set your own --sender."
1238            );
1239        }
1240
1241        let provider = Arc::new(
1242            ProviderBuilder::<TempoNetwork>::from_config_with_url(
1243                &self.script_config.config,
1244                sequence.rpc_url(),
1245            )?
1246            .build()?,
1247        );
1248
1249        if let Some((_, _, AttemptKind::Legacy { hash })) = &recovered_batch {
1250            let transaction = provider
1251                .get_transaction_by_hash(*hash)
1252                .await?
1253                .context("legacy batch transaction is not available for validation")?;
1254            if transaction.tx_hash() != *hash {
1255                bail!("legacy batch endpoint returned a transaction with the wrong hash");
1256            }
1257            validate_tempo_batch_envelope(transaction.as_ref(), sender, chain_id, &calls)?;
1258        }
1259
1260        // Resume detection happens before signer resolution, gas estimation, and sponsor attachment
1261        // so that recovering an already-submitted batch tx never requires the original
1262        // signer/sponsor or a fresh estimate.
1263        if recovered_batch.is_none()
1264            && (!sequence.pending.is_empty()
1265                || sequence.transactions.iter().skip(batch_start).any(|tx| tx.hash.is_some()))
1266        {
1267            bail!("batch progress exists without a durable submission attempt");
1268        }
1269
1270        if let Some(hash) = recovered_hash {
1271            let mut changed = false;
1272            for tx in sequence.transactions.iter_mut().skip(batch_start) {
1273                if tx.hash != Some(hash) {
1274                    tx.hash = Some(hash);
1275                    changed = true;
1276                }
1277            }
1278            if !sequence.receipts.iter().any(|receipt| receipt.transaction_hash() == hash)
1279                && !sequence.pending.contains(&hash)
1280            {
1281                sequence.pending.push(hash);
1282                changed = true;
1283            }
1284            if changed {
1285                self.sequence.save(true, false)?;
1286            }
1287        }
1288
1289        if let Some((_, _, AttemptKind::Signed { payload, .. })) = &recovered_batch
1290            && provider.get_transaction_receipt(payload.hash).await?.is_none()
1291            && provider.get_transaction_by_hash(payload.hash).await?.is_none()
1292        {
1293            send_tempo_batch_payload(provider.as_ref(), &payload.payload, payload.hash).await?;
1294        }
1295
1296        if let Some(tx_hash) = recovered_hash {
1297            sh_println!(
1298                "Resuming batch: tx {tx_hash:#x} already submitted, waiting for receipt..."
1299            )?;
1300
1301            let timeout = self.script_config.config.transaction_timeout;
1302            let receipt_result = tokio::time::timeout(
1303                Duration::from_secs(timeout),
1304                wait_for_batch_receipt(provider.as_ref(), tx_hash, self.args.confirmations),
1305            )
1306            .await;
1307
1308            match receipt_result {
1309                Ok(Ok(Some(receipt))) => {
1310                    Self::finish_batch(&mut self.sequence, receipt, tx_hash, batch_start, true)?;
1311                    return Ok(BroadcastedState {
1312                        args: self.args,
1313                        script_config: self.script_config,
1314                        build_data: self.build_data,
1315                        sequence: self.sequence,
1316                    });
1317                }
1318                Ok(Ok(None)) => {
1319                    bail!(
1320                        "Tempo batch {tx_hash:#x} is not currently visible; its pending hash remains checkpointed"
1321                    );
1322                }
1323                Ok(Err(e)) => return Err(e),
1324                Err(_) => {
1325                    return Err(eyre::eyre!(
1326                        "Timeout waiting for batch transaction receipt (tx: {tx_hash:#x}). \
1327                         The transaction hash remains checkpointed; run with --resume to retry."
1328                    ));
1329                }
1330            }
1331        }
1332
1333        // Reborrow after the potential save above.
1334        let sequence = self.sequence.sequences_mut().get_mut(0).unwrap();
1335
1336        let tempo_sponsor = self.script_config.tempo.sponsor_config().await?;
1337
1338        // Get wallet for signing
1339        enum BatchSigner {
1340            Unlocked,
1341            Wallet(EthereumWallet),
1342            TempoKeychain(Box<TempoAccountsWallet>),
1343        }
1344
1345        let mut batch_signer = if self.args.unlocked {
1346            BatchSigner::Unlocked
1347        } else if let Some(session) = self.script_config.tempo.session_signer_for_multi_wallet(
1348            &self.args.wallets,
1349            Some(sender),
1350            chain_id,
1351        )? {
1352            BatchSigner::TempoKeychain(Box::new(session.access_key))
1353        } else {
1354            let mut signers = self.script_wallets.into_multi_wallet().into_signers()?;
1355            if let Some(signer) = signers.remove(&sender) {
1356                BatchSigner::Wallet(EthereumWallet::new(signer))
1357            } else {
1358                // Try the Tempo Accounts store only for Tempo broadcasts.
1359                if self.script_config.evm_opts.networks.is_tempo()
1360                    && let Some(wallet) = TempoAccountsWallet::try_from_default_store()?
1361                    && wallet.has_account(sender)?
1362                {
1363                    BatchSigner::TempoKeychain(Box::new(wallet.with_chain_id(chain_id)))
1364                } else {
1365                    bail!("No wallet found for sender {}", sender);
1366                }
1367            }
1368        };
1369
1370        // CREATE2 deployer must exist on-chain for any rewritten CREATEs.
1371        let create2_deployer = self.script_config.evm_opts.create2_deployer;
1372        let needs_factory =
1373            sequence.transactions.iter().skip(batch_start).any(|tx| tx.call_kind.is_any_create());
1374        if needs_factory {
1375            let code = provider.get_code_at(create2_deployer).await?;
1376            if keccak256(&code) != DEFAULT_CREATE2_DEPLOYER_CODEHASH {
1377                bail!(
1378                    "CREATE2 deployer {create2_deployer:#x} is not deployed on this Tempo network; \
1379                     --batch requires it. Deploy it first and retry."
1380                );
1381            }
1382        }
1383
1384        sh_println!(
1385            "\n## Broadcasting batch transaction with {} call(s) to chain {}...",
1386            calls.len(),
1387            sequence.chain
1388        )?;
1389
1390        // Build the batch transaction request
1391        let nonce = provider.get_transaction_count(sender).await?;
1392
1393        // Batch transactions are Tempo-only and always use EIP-1559 style fees.
1394        let fees = estimate_eip1559_fees(&provider, self.script_config.config.eip1559_fee_estimate)
1395            .await?;
1396        let fees = resolve_broadcast_eip1559_fees(
1397            fees,
1398            self.args.with_gas_price.map(|p| p.to()),
1399            self.args.priority_gas_price.map(|p| p.to()),
1400            None,
1401        )?;
1402        let max_fee_per_gas = fees.max_fee_per_gas;
1403        let max_priority_fee_per_gas = fees.max_priority_fee_per_gas;
1404
1405        let mut batch_tx = TempoTransactionRequest {
1406            inner: TransactionRequest {
1407                from: Some(sender),
1408                to: None,
1409                value: None,
1410                input: Default::default(),
1411                nonce: Some(nonce),
1412                chain_id: Some(chain_id),
1413                max_fee_per_gas: Some(max_fee_per_gas),
1414                max_priority_fee_per_gas: Some(max_priority_fee_per_gas),
1415                ..Default::default()
1416            },
1417            fee_token: self.script_config.tempo.fee_token,
1418            calls: calls.clone(),
1419            nonce_key: self.script_config.tempo.expiring_nonce.then_some(U256::MAX),
1420            valid_before: self.script_config.tempo.valid_before.and_then(NonZeroU64::new),
1421            ..Default::default()
1422        };
1423        self.script_config.tempo.apply::<TempoNetwork>(&mut batch_tx, None);
1424        let fee_token = if let Some(sponsor) = &tempo_sponsor {
1425            sponsor.resolve_and_set_fee_token(Some(provider.as_ref()), &mut batch_tx).await?;
1426            None
1427        } else {
1428            resolve_and_set_fee_token(Some(provider.as_ref()), &mut batch_tx, Some(sender)).await?
1429        };
1430
1431        if let BatchSigner::TempoKeychain(wallet) = &mut batch_signer {
1432            **wallet = batch_tx.prepare_with_tempo_wallet(provider.as_ref(), wallet).await?;
1433        }
1434
1435        // Estimate gas for the batch transaction
1436        estimate_gas(&mut batch_tx, provider.as_ref(), self.args.gas_estimate_multiplier, false)
1437            .await?;
1438
1439        sh_println!("Estimated gas: {}", batch_tx.inner.gas.unwrap_or(0))?;
1440
1441        if let Some(sponsor) = &tempo_sponsor {
1442            sponsor.attach_and_print::<TempoNetwork>(&mut batch_tx, sender).await?;
1443        } else {
1444            maybe_print_fee_token(Some(provider.as_ref()), fee_token).await?;
1445        }
1446        batch_tx.prep_for_submission();
1447
1448        let _ = sequence;
1449
1450        // Sign and send.
1451        let signed_payload = match &batch_signer {
1452            BatchSigner::Wallet(wallet) => {
1453                Some(Bytes::from(batch_tx.clone().build(wallet).await?.encoded_2718()))
1454            }
1455            BatchSigner::TempoKeychain(wallet) => {
1456                Some(Bytes::from(batch_tx.clone().sign_with_tempo_wallet(wallet).await?))
1457            }
1458            BatchSigner::Unlocked => None,
1459        };
1460        let tx_hash = if let Some(payload) = signed_payload {
1461            validate_tempo_batch_payload(&batch_tx, &payload, sender, chain_id, &calls)?;
1462            let expected = self.sequence.persist_batch_signed_payload(
1463                0,
1464                batch_start,
1465                batch_tx,
1466                payload.clone(),
1467            )?;
1468            send_tempo_batch_payload(provider.as_ref(), &payload, expected).await?;
1469            expected
1470        } else {
1471            let provider = ProviderBuilder::<TempoNetwork>::from_config_with_url(
1472                &self.script_config.config,
1473                &rpc_url,
1474            )?
1475            .max_retry(0)
1476            .build()?;
1477            self.sequence.persist_batch_delegated_request(0, batch_start, batch_tx.clone())?;
1478            let (_, attempt_id, _) = self.sequence.batch_attempt(0).unwrap();
1479            let pending = match provider.send_transaction(batch_tx).await {
1480                Ok(pending) => pending,
1481                Err(error) => {
1482                    let error = eyre::Report::from(error);
1483                    if is_definite_unlocked_non_submission(&error) {
1484                        self.sequence.clear_delegated_request(0, batch_start)?;
1485                        return Err(error);
1486                    }
1487                    self.sequence.persist_delegated_status(
1488                        0,
1489                        batch_start,
1490                        DelegatedStatus::OutcomeUnknown,
1491                    )?;
1492                    return Err(error.wrap_err(format!(
1493                        "submission outcome for delegated Tempo batch attempt {attempt_id} is unknown; resolve it with --resume-attempt and --resume-tx-hash, or use --resume-retry only after proving it was not submitted"
1494                    )));
1495                }
1496            };
1497            let hash = *pending.tx_hash();
1498            self.sequence.persist_delegated_status(
1499                0,
1500                batch_start,
1501                DelegatedStatus::Pending { hash },
1502            )?;
1503            hash
1504        };
1505
1506        sh_println!("Batch transaction sent: {:#x}", tx_hash)?;
1507
1508        // Checkpoint: stamp the batch hash on all remaining transactions (so that resume
1509        // detection finds it regardless of which tx it inspects first), register one entry
1510        // in sequence.pending for drop/timeout tracking, then save.
1511        let sequence = self.sequence.sequences_mut().get_mut(0).unwrap();
1512        for tx in sequence.transactions.iter_mut().skip(batch_start) {
1513            tx.hash = Some(tx_hash);
1514        }
1515        if !sequence.pending.contains(&tx_hash) {
1516            sequence.pending.push(tx_hash);
1517        }
1518        self.sequence.save(true, false)?;
1519
1520        // Wait for receipt
1521        let timeout = self.script_config.config.transaction_timeout;
1522        let receipt = tokio::time::timeout(
1523            Duration::from_secs(timeout),
1524            wait_for_batch_receipt(provider.as_ref(), tx_hash, self.args.confirmations),
1525        )
1526        .await
1527        .map_err(|_| eyre::eyre!("Timeout waiting for batch transaction receipt (tx: {tx_hash:#x}). Run with --resume to retry."))??
1528        .ok_or_else(|| eyre::eyre!("Batch transaction {tx_hash:#x} was dropped from the mempool. Run with --resume to retry."))?;
1529
1530        Self::finish_batch(&mut self.sequence, receipt, tx_hash, batch_start, false)?;
1531        Ok(BroadcastedState {
1532            args: self.args,
1533            script_config: self.script_config,
1534            build_data: self.build_data,
1535            sequence: self.sequence,
1536        })
1537    }
1538
1539    fn finish_batch(
1540        sequences: &mut ScriptSequenceKind<TempoNetwork>,
1541        receipt: TempoTransactionReceipt,
1542        tx_hash: TxHash,
1543        batch_start: usize,
1544        resumed: bool,
1545    ) -> Result<()> {
1546        if receipt.status() {
1547            sh_println!(
1548                "Batch transaction confirmed in block {}",
1549                receipt.block_number.unwrap_or(0)
1550            )?;
1551        } else {
1552            bail!("Batch transaction failed (reverted)");
1553        }
1554
1555        let sequence = sequences.sequences_mut().get_mut(0).unwrap();
1556        sequence.remove_pending(tx_hash);
1557
1558        // Receipts are pushed 1:1 with the remaining (not-yet-receipted) transactions.
1559        let remaining_len = sequence.transactions.len() - batch_start;
1560        // Only carry through contract_address for actual deployments; plain calls also
1561        // store the callee in `contract_address`, which would otherwise be copied into
1562        // the receipt and treated as a fresh deployment by downstream consumers
1563        // (broadcast JSON, verifier).
1564        let per_tx_addresses: Vec<Option<Address>> = sequence
1565            .transactions
1566            .iter()
1567            .skip(batch_start)
1568            .map(|tx| match tx.call_kind {
1569                CallKind::Create | CallKind::Create2 => tx.contract_address,
1570                _ => None,
1571            })
1572            .collect();
1573
1574        for (idx, addr) in per_tx_addresses.iter().enumerate() {
1575            if let Some(addr) = addr {
1576                sh_println!("  call[{idx}] deployed at: {addr:#x}")?;
1577            }
1578        }
1579
1580        // gasUsed reflects the whole batch; per-call attribution is unavailable from the receipt.
1581        for addr in &per_tx_addresses {
1582            let mut tx_receipt = receipt.clone();
1583            tx_receipt.contract_address = *addr;
1584            sequence.receipts.push(tx_receipt);
1585        }
1586
1587        let chain = sequence.chain;
1588        let _ = sequence;
1589
1590        sequences.save(true, false)?;
1591
1592        let total_gas = receipt.gas_used();
1593        let gas_price = receipt.effective_gas_price();
1594        let total_paid = u128::from(total_gas).saturating_mul(gas_price);
1595        let paid = format_units(total_paid, 18).unwrap_or_else(|_| "N/A".to_string());
1596        let gas_price_gwei = format_units(gas_price, 9).unwrap_or_else(|_| "N/A".to_string());
1597
1598        let token_symbol = NamedChain::try_from(chain)
1599            .unwrap_or_default()
1600            .native_currency_symbol()
1601            .unwrap_or("ETH");
1602        let resumed =
1603            resumed.then(|| format!("\n(resumed from previous run, {remaining_len} tx(s))"));
1604        sh_println!(
1605            "\nTotal Paid: {} {} ({} gas * {} gwei){}",
1606            paid.trim_end_matches('0'),
1607            token_symbol,
1608            total_gas,
1609            gas_price_gwei.trim_end_matches('0').trim_end_matches('.'),
1610            resumed.as_deref().unwrap_or_default(),
1611        )?;
1612
1613        if !shell::is_json() {
1614            sh_println!("\n\n==========================")?;
1615            sh_println!("\nBATCH EXECUTION COMPLETE & SUCCESSFUL.")?;
1616            sh_println!(
1617                "All {} calls executed atomically in a single transaction.",
1618                remaining_len
1619            )?;
1620        }
1621
1622        Ok(())
1623    }
1624}
1625
1626async fn send_tempo_batch_payload(
1627    provider: &RootProvider<TempoNetwork>,
1628    payload: &[u8],
1629    expected: TxHash,
1630) -> Result<()> {
1631    match provider.send_raw_transaction(payload).await {
1632        Ok(pending) if *pending.tx_hash() == expected => Ok(()),
1633        Ok(pending) => {
1634            bail!("RPC returned hash {} for signed batch payload {expected}", pending.tx_hash())
1635        }
1636        Err(error) => {
1637            if provider.get_transaction_receipt(expected).await.ok().flatten().is_some()
1638                || provider.get_transaction_by_hash(expected).await.ok().flatten().is_some()
1639            {
1640                Ok(())
1641            } else {
1642                Err(error.into())
1643            }
1644        }
1645    }
1646}
1647
1648async fn wait_for_batch_receipt<N: Network>(
1649    provider: &RootProvider<N>,
1650    tx_hash: TxHash,
1651    confirmations: u64,
1652) -> Result<Option<N::ReceiptResponse>> {
1653    loop {
1654        if let Some(receipt) = provider.get_transaction_receipt(tx_hash).await?
1655            && is_mined_receipt_for(&receipt, tx_hash)
1656            && let Some(receipt_block) = receipt.block_number()
1657        {
1658            let latest_block = provider.get_block_number().await?;
1659            if latest_block >= receipt_block.saturating_add(confirmations.saturating_sub(1)) {
1660                return Ok(Some(receipt));
1661            }
1662        }
1663
1664        if provider.get_transaction_by_hash(tx_hash).await?.is_none() {
1665            return Ok(None);
1666        }
1667
1668        tokio::time::sleep(Duration::from_millis(500)).await;
1669    }
1670}
1671
1672pub async fn estimate_gas<N: Network, P: Provider<N>>(
1673    tx: &mut N::TransactionRequest,
1674    provider: &P,
1675    estimate_multiplier: u64,
1676    browser: bool,
1677) -> Result<()>
1678where
1679    N::TransactionRequest: FoundryTransactionBuilder<N>,
1680{
1681    // if already set, some RPC endpoints might simply return the gas value that is already
1682    // set in the request and omit the estimate altogether, so we remove it here
1683    tx.reset_gas_limit();
1684
1685    let request = if browser { tx.browser_wallet_gas_estimation_request() } else { tx.clone() };
1686    tx.set_gas_limit(
1687        provider.estimate_gas(request).await.wrap_err("Failed to estimate gas for tx")?
1688            * estimate_multiplier
1689            / 100,
1690    );
1691    Ok(())
1692}
1693
1694fn reject_access_key_create<N: Network>(
1695    tx: &N::TransactionRequest,
1696    uses_access_key: bool,
1697) -> Result<()>
1698where
1699    N::TransactionRequest: FoundryTransactionBuilder<N>,
1700{
1701    if uses_access_key && tx.tempo_calls().iter().any(|(to, _)| to.is_create()) {
1702        bail!("Tempo access-key transactions cannot use CREATE");
1703    }
1704    Ok(())
1705}
1706
1707fn convert_tempo_aa_create<N: Network>(tx: &mut N::TransactionRequest)
1708where
1709    N::TransactionRequest: FoundryTransactionBuilder<N>,
1710{
1711    if tx.is_tempo_aa() {
1712        tx.convert_create_to_call();
1713    }
1714}
1715
1716/// Returns the total gas used, the average gas price and the total fee paid in wei for the given
1717/// `(gas_used, effective_gas_price)` receipt pairs.
1718fn fee_totals(receipts: impl IntoIterator<Item = (u64, u128)>) -> (u64, Option<u128>, u128) {
1719    let (count, total_gas, total_gas_price, total_paid) =
1720        receipts.into_iter().fold((0u128, 0u64, 0u128, 0u128), |acc, (gas_used, gas_price)| {
1721            (
1722                acc.0 + 1,
1723                acc.1 + gas_used,
1724                acc.2.saturating_add(gas_price),
1725                acc.3.saturating_add(u128::from(gas_used).saturating_mul(gas_price)),
1726            )
1727        });
1728    (total_gas, total_gas_price.checked_div(count), total_paid)
1729}
1730
1731#[cfg(test)]
1732mod tests {
1733    use super::*;
1734    use alloy_consensus::{Eip658Value, Receipt, ReceiptEnvelope, ReceiptWithBloom, TxEnvelope};
1735    use alloy_network::Ethereum;
1736    use alloy_primitives::{B256, Bloom, address, hex};
1737    use alloy_provider::mock::Asserter;
1738    use alloy_rpc_types::TransactionReceipt;
1739    use alloy_signer::Signer;
1740    use forge_script_sequence::TransactionWithMetadata;
1741    use foundry_common::tempo::PATH_USD_ADDRESS;
1742    use foundry_evm::{backend::Backend, core::evm::EthEvmNetwork, opts::EvmOpts};
1743
1744    const ROOT_PRIVATE_KEY: &str =
1745        "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
1746    const ACCESS_KEY_PRIVATE_KEY: &str =
1747        "0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0";
1748    const SIGNED_TX: &[u8] = &hex!(
1749        "02f86b0180843b9aca008502540be4008252089400000000000000000000000000000000000000016480c001a070d55e79ed3ac9fc8f51e78eb91fd054720d943d66633f2eb1bc960f0126b0eca052eda05a792680de3181e49bab4093541f75b49d1ecbe443077b3660c836016a"
1750    );
1751    const OTHER_SIGNED_TX: &[u8] = &hex!(
1752        "02f86b0180843b9aca008502540be4008252089400000000000000000000000000000000000000018080c001a0cce9a61187b5d18a89ecd27ec675e3b3f10d37f165627ef89a15a7fe76395ce8a07537f5bffb358ffbef22cda84b1c92f7211723f9e09ae037e81686805d3e5505"
1753    );
1754
1755    #[tokio::test(flavor = "multi_thread")]
1756    async fn next_nonce_uses_exact_fork_hash() {
1757        let (_api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
1758        let provider = handle.http_provider();
1759        let sender = handle.dev_accounts().next().unwrap();
1760        let recipient = Address::with_last_byte(1);
1761
1762        let receipt = provider
1763            .send_transaction(
1764                TransactionRequest::default().from(sender).to(recipient).value(U256::ONE).into(),
1765            )
1766            .await
1767            .unwrap()
1768            .get_receipt()
1769            .await
1770            .unwrap();
1771        let block_number = receipt.block_number.unwrap();
1772        let evm_opts = EvmOpts {
1773            fork_url: Some(handle.http_endpoint()),
1774            fork_block_number: Some(block_number),
1775            ..Default::default()
1776        };
1777        let backend =
1778            Backend::<EthEvmNetwork>::spawn(evm_opts.get_fork(&Config::default(), 31337, None))
1779                .unwrap();
1780        assert_eq!(backend.transaction_count(sender).await.unwrap(), 1);
1781
1782        provider
1783            .raw_request::<_, ()>("anvil_reorg".into(), (1_u64, Vec::<serde_json::Value>::new()))
1784            .await
1785            .unwrap();
1786        assert_eq!(provider.get_transaction_count(sender).number(block_number).await.unwrap(), 0);
1787
1788        match backend.transaction_count(sender).await {
1789            Ok(0) => panic!("the exact lookup fell back to the replacement block"),
1790            Ok(1) | Err(_) => {}
1791            Ok(nonce) => panic!("unexpected nonce: {nonce}"),
1792        }
1793    }
1794
1795    #[test]
1796    fn access_key_signer_takes_precedence_over_same_sender_wallet() {
1797        let root = foundry_wallets::utils::create_private_key_signer(ROOT_PRIVATE_KEY).unwrap();
1798        let root_address = root.address();
1799        let access_key =
1800            foundry_wallets::utils::create_local_signer(ACCESS_KEY_PRIVATE_KEY).unwrap();
1801        let access_key_address = access_key.address();
1802        let mut eth_wallets = AddressHashMap::default();
1803        eth_wallets.insert(root_address, EthereumWallet::new(root));
1804        let mut access_keys = HashMap::default();
1805        access_keys.insert(
1806            SignerScope::new(4217, root_address),
1807            TempoAccountsWallet::from_secp256k1(root_address, access_key, None).with_chain_id(4217),
1808        );
1809        let send_kind =
1810            SendTransactionsKind::<Ethereum>::Raw { eth_wallets, browser: None, access_keys };
1811
1812        let tx = TransactionRequest { from: Some(root_address), ..Default::default() };
1813        let sender = send_kind.for_sender(4217, &root_address, tx).unwrap();
1814
1815        match sender {
1816            SendTransactionKind::AccessKey(_, wallet) => {
1817                assert_eq!(wallet.key_id().unwrap(), access_key_address);
1818                assert_eq!(wallet.account(), root_address);
1819            }
1820            _ => panic!("expected access key signer"),
1821        }
1822    }
1823
1824    #[test]
1825    fn access_key_signer_is_scoped_to_chain() {
1826        let root = foundry_wallets::utils::create_private_key_signer(ROOT_PRIVATE_KEY).unwrap();
1827        let root_address = root.address();
1828        let access_key =
1829            foundry_wallets::utils::create_local_signer(ACCESS_KEY_PRIVATE_KEY).unwrap();
1830        let mut eth_wallets = AddressHashMap::default();
1831        eth_wallets.insert(root_address, EthereumWallet::new(root));
1832        let mut access_keys = HashMap::default();
1833        access_keys.insert(
1834            SignerScope::new(4217, root_address),
1835            TempoAccountsWallet::from_secp256k1(root_address, access_key, None).with_chain_id(4217),
1836        );
1837        let send_kind =
1838            SendTransactionsKind::<Ethereum>::Raw { eth_wallets, browser: None, access_keys };
1839
1840        let tx = TransactionRequest { from: Some(root_address), ..Default::default() };
1841        let sender = send_kind.for_sender(1, &root_address, tx).unwrap();
1842
1843        match sender {
1844            SendTransactionKind::Raw(_, wallet) => {
1845                assert_eq!(wallet.default_signer().address(), root_address);
1846            }
1847            _ => panic!("expected root wallet signer for non-session chain"),
1848        }
1849    }
1850
1851    #[test]
1852    fn recovered_signed_payload_does_not_require_a_signer() {
1853        let dir = tempfile::tempdir().unwrap();
1854        let mut sequence = ScriptSequence::<Ethereum> {
1855            chain: 1,
1856            transactions: [planned_tx(SIGNED_TX)].into(),
1857            ..Default::default()
1858        };
1859        sequence.paths = Some((dir.path().join("broadcast.json"), dir.path().join("cache.json")));
1860        let mut sequence = ScriptSequenceKind::new_single(sequence, false).unwrap();
1861        let payload = Bytes::from_static(SIGNED_TX);
1862        sequence.persist_signed_payload(0, 0, payload).unwrap();
1863
1864        assert!(remaining_unsigned_transactions_for_recovery(&sequence).is_empty());
1865    }
1866
1867    #[test]
1868    fn recovered_batch_attempt_does_not_require_a_signer() {
1869        let dir = tempfile::tempdir().unwrap();
1870        let sender = Address::repeat_byte(0x22);
1871        let mut deployment = ScriptSequence::<Ethereum> {
1872            chain: 1,
1873            transactions: [script_tx(sender)].into(),
1874            ..Default::default()
1875        };
1876        deployment.paths = Some((dir.path().join("broadcast.json"), dir.path().join("cache.json")));
1877        let mut sequence = ScriptSequenceKind::new_single(deployment, true).unwrap();
1878        sequence.persist_batch_delegated_request(0, 0, Default::default()).unwrap();
1879
1880        assert!(remaining_unsigned_transactions_for_recovery(&sequence).is_empty());
1881    }
1882
1883    #[test]
1884    fn batch_retry_clears_the_shared_delegated_attempt() {
1885        let dir = tempfile::tempdir().unwrap();
1886        let recovery_path = dir.path().join("cache.json.recovery.json");
1887        let mut deployment = ScriptSequence::<Ethereum> {
1888            chain: 1,
1889            transactions: [script_tx(Address::ZERO), script_tx(Address::ZERO)].into(),
1890            ..Default::default()
1891        };
1892        deployment.paths = Some((dir.path().join("broadcast.json"), dir.path().join("cache.json")));
1893        let mut sequence = ScriptSequenceKind::new_single(deployment, true).unwrap();
1894        sequence.persist_batch_delegated_request(0, 0, Default::default()).unwrap();
1895        let (_, attempt, _) = sequence.batch_attempt(0).unwrap();
1896
1897        sequence.restore_delegated_pending(Some(attempt), None, true).unwrap();
1898
1899        assert!(sequence.batch_attempt(0).is_none());
1900        drop(sequence);
1901        let recovery: serde_json::Value =
1902            foundry_common::fs::read_json_file(&recovery_path).unwrap();
1903        assert_eq!(recovery["deployments"][0]["operations"].as_array().unwrap().len(), 2);
1904        assert!(recovery["deployments"][0].get("attempts").is_none());
1905    }
1906
1907    #[test]
1908    fn recovered_sender_still_requires_sequential_ordering() {
1909        let dir = tempfile::tempdir().unwrap();
1910        let unsigned = Address::repeat_byte(0x22);
1911        let mut sequence = ScriptSequence::<Ethereum> {
1912            chain: 1,
1913            transactions: [planned_tx(SIGNED_TX), script_tx(unsigned)].into(),
1914            ..Default::default()
1915        };
1916        sequence.paths = Some((dir.path().join("broadcast.json"), dir.path().join("cache.json")));
1917        let mut sequence = ScriptSequenceKind::new_single(sequence, false).unwrap();
1918        sequence.persist_signed_payload(0, 0, Bytes::from_static(SIGNED_TX)).unwrap();
1919
1920        let required = remaining_unsigned_transactions_for_recovery(&sequence);
1921        assert_eq!(required.iter().map(|tx| tx.sender).collect::<Vec<_>>(), [unsigned]);
1922        assert_eq!(remaining_sender_addresses(&sequence).len(), 2);
1923    }
1924
1925    #[test]
1926    fn signed_only_sequences_remain_sequential() {
1927        assert!(should_broadcast_sequentially(false, false, 0, 1, true));
1928        assert!(!should_broadcast_sequentially(false, false, 1, 1, true));
1929    }
1930
1931    #[test]
1932    fn browser_rejections_are_definite_non_submissions() {
1933        let rejected =
1934            eyre::Report::new(alloy_signer::Error::other(BrowserWalletError::Rejected {
1935                operation: "Transaction",
1936                reason: "Rejected by user".to_string(),
1937            }));
1938        assert!(is_definite_browser_non_submission(&rejected));
1939        let send_error = eyre::Report::new(alloy_signer::Error::other(
1940            BrowserWalletError::ServerError("replacement transaction underpriced".to_string()),
1941        ));
1942        assert!(!is_definite_browser_non_submission(&send_error));
1943        let not_connected =
1944            eyre::Report::new(alloy_signer::Error::other(BrowserWalletError::NotConnected));
1945        assert!(is_definite_browser_non_submission(&not_connected));
1946        let timeout = eyre::Report::new(alloy_signer::Error::other(BrowserWalletError::Timeout {
1947            operation: "Transaction",
1948        }));
1949        assert!(!is_definite_browser_non_submission(&timeout));
1950    }
1951
1952    #[test]
1953    fn only_anvil_no_signer_errors_are_definite_non_submissions() {
1954        assert!(is_unlocked_non_submission(-32602, "No Signer available"));
1955        assert!(!is_unlocked_non_submission(-32603, "No Signer available"));
1956        assert!(!is_unlocked_non_submission(-32602, "transaction rejected"));
1957    }
1958
1959    #[test]
1960    fn recovered_completion_is_matched_by_hash() {
1961        let dir = tempfile::tempdir().unwrap();
1962        let mut deployment = ScriptSequence::<Ethereum> {
1963            chain: 1,
1964            transactions: [planned_tx(SIGNED_TX), planned_tx(OTHER_SIGNED_TX)].into(),
1965            ..Default::default()
1966        };
1967        deployment.paths = Some((dir.path().join("broadcast.json"), dir.path().join("cache.json")));
1968        let mut sequence = ScriptSequenceKind::new_single(deployment, false).unwrap();
1969        sequence.persist_signed_payload(0, 0, Bytes::from_static(SIGNED_TX)).unwrap();
1970        let second_hash =
1971            sequence.persist_signed_payload(0, 1, Bytes::from_static(OTHER_SIGNED_TX)).unwrap();
1972        let mut second_receipt = receipt();
1973        second_receipt.transaction_hash = second_hash;
1974        sequence.sequences_mut()[0].receipts.push(second_receipt);
1975
1976        assert_eq!(remaining_operation_indices(&sequence, 0), [0]);
1977    }
1978
1979    #[test]
1980    fn externally_signed_completion_uses_the_persisted_hash() {
1981        let dir = tempfile::tempdir().unwrap();
1982        let sender = Address::repeat_byte(0x11);
1983        let mut deployment = ScriptSequence::<Ethereum> {
1984            chain: 1,
1985            transactions: [script_tx(sender), script_tx(sender)].into(),
1986            receipts: vec![receipt()],
1987            ..Default::default()
1988        };
1989        deployment.transactions[1].hash = Some(deployment.receipts[0].transaction_hash());
1990        deployment.paths = Some((dir.path().join("broadcast.json"), dir.path().join("cache.json")));
1991        let sequence = ScriptSequenceKind::new_single(deployment, false).unwrap();
1992
1993        assert_eq!(remaining_operation_indices(&sequence, 0), [0]);
1994    }
1995
1996    #[test]
1997    fn duplicate_receipts_do_not_complete_an_unsigned_operation() {
1998        let dir = tempfile::tempdir().unwrap();
1999        let mut deployment = ScriptSequence::<Ethereum> {
2000            chain: 1,
2001            transactions: [planned_tx(SIGNED_TX), planned_tx(OTHER_SIGNED_TX)].into(),
2002            ..Default::default()
2003        };
2004        deployment.paths = Some((dir.path().join("broadcast.json"), dir.path().join("cache.json")));
2005        let mut sequence = ScriptSequenceKind::new_single(deployment, false).unwrap();
2006        let first_hash =
2007            sequence.persist_signed_payload(0, 0, Bytes::from_static(SIGNED_TX)).unwrap();
2008        let mut first_receipt = receipt();
2009        first_receipt.transaction_hash = first_hash;
2010        sequence.sequences_mut()[0].receipts = vec![first_receipt.clone(), first_receipt];
2011
2012        assert_eq!(remaining_operation_indices(&sequence, 0), [1]);
2013    }
2014
2015    #[test]
2016    fn completed_prefix_rejects_receipt_holes() {
2017        let hash = B256::repeat_byte(0x44);
2018        let mut later = script_tx(Address::ZERO);
2019        later.hash = Some(hash);
2020        let mut receipt = receipt();
2021        receipt.transaction_hash = hash;
2022        let sequence = ScriptSequence::<Ethereum> {
2023            transactions: [script_tx(Address::ZERO), later].into(),
2024            receipts: vec![receipt],
2025            ..Default::default()
2026        };
2027
2028        assert!(completed_transaction_prefix(&sequence).is_err());
2029    }
2030
2031    #[tokio::test]
2032    async fn access_key_sets_key_id_before_estimation() {
2033        let root_address = Address::repeat_byte(0x11);
2034        let access_key =
2035            foundry_wallets::utils::create_local_signer(ACCESS_KEY_PRIVATE_KEY).unwrap();
2036        let access_key_address = access_key.address();
2037        let access_key_wallet =
2038            TempoAccountsWallet::from_secp256k1(root_address, access_key, None).with_chain_id(4217);
2039        // An explicit fee token keeps preparation from querying the unreachable endpoint.
2040        let mut sender = SendTransactionKind::<TempoNetwork>::AccessKey(
2041            TempoTransactionRequest {
2042                inner: TransactionRequest { from: Some(root_address), ..Default::default() },
2043                fee_token: Some(PATH_USD_ADDRESS),
2044                ..Default::default()
2045            },
2046            Box::new(access_key_wallet),
2047        );
2048        let provider =
2049            RootProvider::<TempoNetwork>::new_http("http://localhost:8545".parse().unwrap());
2050
2051        sender.prepare(&provider, false, true, false, 100, None).await.unwrap();
2052
2053        match sender {
2054            SendTransactionKind::AccessKey(tx, _) => {
2055                assert_eq!(tx.key_id, Some(access_key_address));
2056            }
2057            _ => panic!("expected access key transaction"),
2058        }
2059    }
2060
2061    #[test]
2062    fn tempo_aa_create_moves_deployment_into_calls() {
2063        let mut tx = TempoTransactionRequest {
2064            inner: TransactionRequest { to: Some(TxKind::Create), ..Default::default() },
2065            fee_token: Some(address!("0x20c0000000000000000000000000000000000000")),
2066            ..Default::default()
2067        };
2068
2069        convert_tempo_aa_create::<TempoNetwork>(&mut tx);
2070
2071        assert!(tx.inner.to.is_none());
2072        assert_eq!(tx.calls.len(), 1);
2073        assert!(tx.calls[0].to.is_create());
2074    }
2075
2076    #[tokio::test]
2077    async fn signed_tempo_batch_is_bound_to_its_calls() {
2078        let signer = foundry_wallets::utils::create_private_key_signer(ROOT_PRIVATE_KEY).unwrap();
2079        let sender = signer.address();
2080        let wallet = EthereumWallet::new(signer);
2081        let calls = vec![Call {
2082            to: TxKind::Call(Address::repeat_byte(0x11)),
2083            value: U256::ONE,
2084            input: Bytes::new(),
2085        }];
2086        let request = TempoTransactionRequest {
2087            inner: TransactionRequest {
2088                from: Some(sender),
2089                nonce: Some(0),
2090                chain_id: Some(4217),
2091                gas: Some(21_000),
2092                max_fee_per_gas: Some(1),
2093                max_priority_fee_per_gas: Some(1),
2094                ..Default::default()
2095            },
2096            calls: calls.clone(),
2097            ..Default::default()
2098        };
2099        let payload = request.clone().build(&wallet).await.unwrap().encoded_2718();
2100
2101        assert!(validate_tempo_batch_payload(&request, &payload, sender, 4217, &calls).is_ok());
2102        let other_calls =
2103            vec![Call { to: TxKind::Call(Address::repeat_byte(0x22)), ..calls[0].clone() }];
2104        assert!(
2105            validate_tempo_batch_payload(&request, &payload, sender, 4217, &other_calls).is_err()
2106        );
2107
2108        let other_signer =
2109            foundry_wallets::utils::create_private_key_signer(ACCESS_KEY_PRIVATE_KEY).unwrap();
2110        let mut other_request = request.clone();
2111        other_request.inner.from = Some(other_signer.address());
2112        let other_payload =
2113            other_request.build(&EthereumWallet::new(other_signer)).await.unwrap().encoded_2718();
2114        assert!(
2115            validate_tempo_batch_payload(&request, &other_payload, sender, 4217, &calls).is_err()
2116        );
2117        let other_envelope = TempoTxEnvelope::decode_2718_exact(&other_payload).unwrap();
2118        assert!(validate_tempo_batch_envelope(&other_envelope, sender, 4217, &calls).is_err());
2119    }
2120
2121    #[test]
2122    fn tempo_access_key_create_is_rejected_before_preparation() {
2123        let tx = TempoTransactionRequest {
2124            inner: TransactionRequest { to: Some(TxKind::Create), ..Default::default() },
2125            ..Default::default()
2126        };
2127
2128        let error = reject_access_key_create::<TempoNetwork>(&tx, true).unwrap_err();
2129
2130        assert!(error.to_string().contains("Tempo access-key transactions cannot use CREATE"));
2131    }
2132
2133    #[test]
2134    fn fee_totals_do_not_overflow_u64() {
2135        let gwei = 1_000_000_000;
2136        let receipts = [(30_000_000, 1_000 * gwei), (30_000_000, 2_000 * gwei)];
2137
2138        assert_eq!(
2139            fee_totals(receipts),
2140            (60_000_000, Some(1_500 * gwei), 90_000_000_000_000_000_000)
2141        );
2142    }
2143
2144    #[tokio::test]
2145    async fn batch_receipt_watcher_rejects_foreign_or_incomplete_receipts() {
2146        let hash = B256::repeat_byte(0x42);
2147        let receipt = |field: &str, value: serde_json::Value| {
2148            let mut receipt = serde_json::json!({
2149                "type": "0x02", "status": "0x1", "cumulativeGasUsed": "0x5208", "logs": [],
2150                "transactionHash": hash, "logsBloom": format!("{:#x}", Bloom::ZERO),
2151                "transactionIndex": "0x0", "blockHash": B256::ZERO, "blockNumber": "0x10",
2152                "gasUsed": "0x5208", "effectiveGasPrice": "0x1",
2153                "from": Address::ZERO, "to": Address::ZERO, "contractAddress": null
2154            });
2155            receipt[field] = value;
2156            receipt
2157        };
2158        // Bad receipts fall through to the transaction lookup; valid ones to the block number.
2159        let wait = async |receipt: serde_json::Value, next: serde_json::Value| {
2160            let asserter = Asserter::new();
2161            let provider: RootProvider<Ethereum> =
2162                alloy_provider::ProviderBuilder::default().connect_mocked_client(asserter.clone());
2163            asserter.push_success(&receipt);
2164            asserter.push_success(&next);
2165            wait_for_batch_receipt(&provider, hash, 1).await.unwrap()
2166        };
2167
2168        for (field, value) in [
2169            ("transactionHash", serde_json::json!(B256::repeat_byte(0x99))),
2170            ("blockHash", serde_json::Value::Null),
2171            ("transactionIndex", serde_json::Value::Null),
2172            ("blockNumber", serde_json::Value::Null),
2173        ] {
2174            let result = wait(receipt(field, value), serde_json::Value::Null).await;
2175            assert!(result.is_none(), "accepted receipt with bad {field}");
2176        }
2177        let accepted =
2178            wait(receipt("status", serde_json::json!("0x1")), serde_json::json!("0x10")).await;
2179        let accepted = accepted.unwrap();
2180        assert_eq!(accepted.transaction_hash(), hash);
2181    }
2182
2183    fn script_tx(from: Address) -> TransactionWithMetadata<Ethereum> {
2184        TransactionWithMetadata::from_tx_request(TransactionMaybeSigned::new(TransactionRequest {
2185            from: Some(from),
2186            ..Default::default()
2187        }))
2188    }
2189
2190    fn planned_tx(payload: &[u8]) -> TransactionWithMetadata<Ethereum> {
2191        let envelope = TxEnvelope::decode_2718_exact(payload).unwrap();
2192        let from = envelope.recover_signer().unwrap();
2193        let mut request: TransactionRequest = envelope.into();
2194        request.from = Some(from);
2195        TransactionWithMetadata::from_tx_request(TransactionMaybeSigned::new(request))
2196    }
2197
2198    fn receipt() -> TransactionReceipt {
2199        TransactionReceipt {
2200            inner: ReceiptEnvelope::Legacy(ReceiptWithBloom {
2201                receipt: Receipt {
2202                    status: Eip658Value::success(),
2203                    cumulative_gas_used: 0,
2204                    logs: vec![],
2205                },
2206                logs_bloom: Bloom::ZERO,
2207            }),
2208            transaction_hash: Default::default(),
2209            transaction_index: None,
2210            block_hash: None,
2211            block_number: None,
2212            gas_used: 0,
2213            effective_gas_price: 0,
2214            blob_gas_used: None,
2215            blob_gas_price: None,
2216            from: Address::ZERO,
2217            to: None,
2218            contract_address: None,
2219        }
2220    }
2221
2222    #[tokio::test]
2223    async fn preparation_preserves_signed_payload_without_rpc() {
2224        let envelope = TxEnvelope::decode_2718_exact(SIGNED_TX).unwrap();
2225        let hash = envelope.trie_hash();
2226        let provider = alloy_provider::ProviderBuilder::new()
2227            .disable_recommended_fillers()
2228            .connect_mocked_client(Asserter::new());
2229        for input in [
2230            SendTransactionKind::<Ethereum>::Signed(envelope),
2231            SendTransactionKind::PreparedRaw(Bytes::from_static(SIGNED_TX), hash),
2232        ] {
2233            let prepared = input
2234                .prepare_for_durable_send(&provider, true, false, true, 130, None)
2235                .await
2236                .unwrap();
2237            let PreparedTransaction::Raw(payload, prepared_hash) = prepared else {
2238                panic!("signed transactions must become fixed raw submissions");
2239            };
2240            assert_eq!(payload.as_ref(), SIGNED_TX);
2241            assert_eq!(prepared_hash, hash);
2242        }
2243    }
2244}