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