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#[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 #[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 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 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 convert_tempo_aa_create::<N>(tx);
163
164 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 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#[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
486fn 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
531fn 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
554pub enum SendTransactionsKind<N: Network> {
556 Unlocked(AddressHashSet),
558 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 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
600pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 self.sequence.save(true, false)?;
1138
1139 seq_progress.inner.write().tx_sent(tx_hash);
1140 }
1141
1142 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 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 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 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 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 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 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 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 pub async fn broadcast_batch(mut self) -> Result<BroadcastedState<TempoEvmNetwork>> {
1295 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 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 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 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 let sequence = self.sequence.sequences_mut().get_mut(0).unwrap();
1541
1542 let tempo_sponsor = self.script_config.tempo.sponsor_config().await?;
1543
1544 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 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 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 let nonce = provider.get_transaction_count(sender).await?;
1605
1606 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 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(&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 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 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 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 let remaining_len = sequence.transactions.len() - batch_start;
1777 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 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 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
1933fn 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
1967fn 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
1982fn 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(¬_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 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 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 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 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}