1use std::{cmp::Ordering, num::NonZeroU64, sync::Arc, time::Duration};
2
3use crate::{
4 ScriptArgs, ScriptConfig,
5 build::LinkedBuildData,
6 progress::ScriptProgress,
7 sequence::ScriptSequenceKind,
8 session::{
9 RemainingScriptTransaction, SignerScope,
10 insert_session_access_key_for_remaining_transactions,
11 script_session_expected_sender_if_configured,
12 },
13 verify::BroadcastedState,
14};
15use alloy_chains::{Chain, NamedChain};
16use alloy_consensus::{SignableTransaction, Signed};
17use alloy_eips::eip2718::Encodable2718;
18use alloy_network::{
19 EthereumWallet, Network, NetworkTransactionBuilder, ReceiptResponse, TransactionBuilder,
20};
21use alloy_primitives::{
22 Address, TxHash, TxKind, U256, keccak256,
23 map::{AddressHashMap, AddressHashSet, HashMap},
24 utils::format_units,
25};
26use alloy_provider::{Provider, RootProvider, utils::Eip1559Estimation};
27use alloy_rpc_types::TransactionRequest;
28use alloy_signer::Signature;
29use eyre::{Context, Result, bail};
30use forge_script_sequence::ScriptSequence;
31use foundry_cheatcodes::Wallets;
32use foundry_cli::utils::{has_batch_support, has_different_gas_calc};
33use foundry_common::{
34 FoundryTransactionBuilder, TransactionMaybeSigned,
35 provider::{
36 ProviderBuilder,
37 fee::{estimate_eip1559_fees, resolve_broadcast_eip1559_fees},
38 },
39 shell,
40 tempo::{TempoSponsor, maybe_print_fee_token, resolve_and_set_fee_token},
41};
42use foundry_config::Config;
43use foundry_evm::core::{
44 constants::DEFAULT_CREATE2_DEPLOYER_CODEHASH,
45 evm::{FoundryEvmNetwork, TempoEvmNetwork},
46 fork::ResolvedFork,
47 opts::EvmOpts,
48};
49use foundry_wallets::{TempoAccountsWallet, wallet_browser::signer::BrowserSigner};
50use futures::{FutureExt, StreamExt, future::join_all, stream::FuturesUnordered};
51use itertools::Itertools;
52use revm_inspectors::tracing::types::CallKind;
53use tempo_alloy::{TempoNetwork, rpc::TempoTransactionRequest};
54use tempo_primitives::transaction::Call;
55
56#[derive(Clone)]
58pub enum SendTransactionKind<'a, N: Network> {
59 Unlocked(N::TransactionRequest),
60 Raw(N::TransactionRequest, &'a EthereumWallet),
61 Browser(N::TransactionRequest, &'a BrowserSigner<N>),
62 Signed(N::TxEnvelope),
63 AccessKey(N::TransactionRequest, Box<TempoAccountsWallet>),
64}
65
66impl<'a, N: Network> SendTransactionKind<'a, N>
67where
68 N::TxEnvelope: From<Signed<N::UnsignedTx>>,
69 N::UnsignedTx: SignableTransaction<Signature>,
70 N::TransactionRequest: FoundryTransactionBuilder<N>,
71{
72 #[allow(clippy::too_many_arguments)]
79 pub async fn prepare(
80 &mut self,
81 provider: &RootProvider<N>,
82 sequential_broadcast: bool,
83 is_fixed_gas_limit: bool,
84 estimate_via_rpc: bool,
85 estimate_multiplier: u64,
86 tempo_sponsor: Option<&TempoSponsor>,
87 chain: Option<Chain>,
88 ) -> Result<()> {
89 let tempo_browser = matches!(self, Self::Browser(..)) && chain.is_some_and(Chain::is_tempo);
90 let (tx, tempo_wallet) = match self {
91 Self::Raw(tx, _) | Self::Unlocked(tx) | Self::Browser(tx, _) => (tx, None),
92 Self::AccessKey(tx, wallet) => (tx, Some(wallet)),
93 Self::Signed(_) => return Ok(()),
94 };
95
96 reject_access_key_create::<N>(tx, tempo_wallet.is_some())?;
97
98 if sequential_broadcast {
99 let from = tx.from().expect("no sender");
100
101 let tx_nonce = tx.nonce().expect("no nonce");
102 for attempt in 0..5 {
103 let nonce = provider.get_transaction_count(from).await?;
104 match nonce.cmp(&tx_nonce) {
105 Ordering::Greater => {
106 bail!(
107 "EOA nonce changed unexpectedly while sending transactions. Expected {tx_nonce} got {nonce} from provider."
108 );
109 }
110 Ordering::Less => {
111 if attempt == 4 {
112 bail!(
113 "After 5 attempts, provider nonce ({nonce}) is still behind expected nonce ({tx_nonce})."
114 );
115 }
116 warn!(
117 "Expected nonce ({tx_nonce}) is ahead of provider nonce ({nonce}). Retrying in 1 second..."
118 );
119 tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
120 }
121 Ordering::Equal => {
122 break;
124 }
125 }
126 }
127 }
128
129 if let Some(wallet) = tempo_wallet {
130 **wallet = tx.prepare_with_tempo_wallet(provider, wallet).await?;
131 }
132
133 let fee_token = if let Some(sponsor) = tempo_sponsor {
134 sponsor.resolve_and_set_fee_token(Some(provider), chain, tx).await?;
135 None
136 } else {
137 resolve_and_set_fee_token(Some(provider), chain, tx, tx.from()).await?
138 };
139
140 convert_tempo_aa_create::<N>(tx);
144
145 if !is_fixed_gas_limit && estimate_via_rpc {
148 estimate_gas(tx, provider, estimate_multiplier, tempo_browser).await?;
149 }
150
151 if let Some(sponsor) = tempo_sponsor {
152 let from = tx.from().expect("no sender");
153 sponsor.attach_and_print::<N>(tx, from).await?;
154 } else {
155 maybe_print_fee_token(Some(provider), fee_token).await?;
156 }
157
158 Ok(())
159 }
160
161 pub async fn send(self, provider: Arc<RootProvider<N>>) -> Result<TxHash> {
168 match self {
169 Self::Unlocked(tx) => {
170 debug!("sending transaction from unlocked account {:?}", tx);
171
172 let pending = provider.send_transaction(tx).await?;
174 Ok(*pending.tx_hash())
175 }
176 Self::Raw(tx, signer) => {
177 debug!("sending transaction: {:?}", tx);
178 let signed = tx.build(signer).await?;
179
180 let pending = provider.send_raw_transaction(signed.encoded_2718().as_ref()).await?;
182 Ok(*pending.tx_hash())
183 }
184 Self::Signed(tx) => {
185 debug!("sending transaction: {:?}", tx);
186 let pending = provider.send_raw_transaction(tx.encoded_2718().as_ref()).await?;
187 Ok(*pending.tx_hash())
188 }
189 Self::Browser(tx, signer) => {
190 debug!("sending transaction: {:?}", tx);
191
192 Ok(signer.send_transaction_via_browser(tx).await?)
194 }
195 Self::AccessKey(tx, wallet) => {
196 debug!("sending transaction via tempo access key: {:?}", tx);
197
198 let raw_tx = tx.sign_with_tempo_wallet(&wallet).await?;
199
200 let pending = provider.send_raw_transaction(&raw_tx).await?;
201 Ok(*pending.tx_hash())
202 }
203 }
204 }
205
206 #[allow(clippy::too_many_arguments)]
211 pub async fn prepare_and_send(
212 mut self,
213 provider: Arc<RootProvider<N>>,
214 sequential_broadcast: bool,
215 is_fixed_gas_limit: bool,
216 estimate_via_rpc: bool,
217 estimate_multiplier: u64,
218 tempo_sponsor: Option<&TempoSponsor>,
219 chain: Option<Chain>,
220 ) -> Result<TxHash> {
221 self.prepare(
222 &provider,
223 sequential_broadcast,
224 is_fixed_gas_limit,
225 estimate_via_rpc,
226 estimate_multiplier,
227 tempo_sponsor,
228 chain,
229 )
230 .await?;
231
232 self.send(provider).await
233 }
234}
235
236pub(crate) fn remaining_unsigned_transactions<N: Network>(
237 sequences: &[ScriptSequence<N>],
238) -> impl Iterator<Item = RemainingScriptTransaction> + '_ {
239 sequences.iter().flat_map(|sequence| {
240 remaining_transactions(sequence).filter(|tx| tx.is_unsigned()).map(|tx| {
241 RemainingScriptTransaction {
242 chain: sequence.chain,
243 from: tx.from().expect("missing from"),
244 }
245 })
246 })
247}
248
249fn remaining_transaction_start<N: Network>(sequence: &ScriptSequence<N>) -> usize {
250 sequence.receipts.len().min(sequence.transactions.len())
251}
252
253fn remaining_transactions<N: Network>(
254 sequence: &ScriptSequence<N>,
255) -> impl Iterator<Item = &TransactionMaybeSigned<N>> + '_ {
256 sequence.transactions().skip(remaining_transaction_start(sequence))
257}
258
259pub enum SendTransactionsKind<N: Network> {
261 Unlocked(AddressHashSet),
263 Raw {
265 eth_wallets: AddressHashMap<EthereumWallet>,
266 browser: Option<BrowserSigner<N>>,
267 access_keys: HashMap<SignerScope, TempoAccountsWallet>,
268 },
269}
270
271impl<N: Network> SendTransactionsKind<N> {
272 pub fn for_sender(
276 &self,
277 chain: u64,
278 addr: &Address,
279 tx: N::TransactionRequest,
280 ) -> Result<SendTransactionKind<'_, N>> {
281 match self {
282 Self::Unlocked(unlocked) => {
283 if !unlocked.contains(addr) {
284 bail!("Sender address {:?} is not unlocked", addr);
285 }
286 Ok(SendTransactionKind::Unlocked(tx))
287 }
288 Self::Raw { eth_wallets, browser, access_keys } => {
289 if let Some(wallet) = access_keys.get(&SignerScope::new(chain, *addr)) {
290 Ok(SendTransactionKind::AccessKey(tx, Box::new(wallet.clone())))
291 } else if let Some(wallet) = eth_wallets.get(addr) {
292 Ok(SendTransactionKind::Raw(tx, wallet))
293 } else if let Some(b) = browser
294 && b.address() == *addr
295 {
296 Ok(SendTransactionKind::Browser(tx, b))
297 } else {
298 bail!("No matching signer for {:?} found", addr);
299 }
300 }
301 }
302 }
303}
304
305pub struct BundledState<FEN: FoundryEvmNetwork> {
309 pub args: ScriptArgs,
310 pub script_config: ScriptConfig<FEN>,
311 pub script_wallets: Wallets,
312 pub browser_wallet: Option<BrowserSigner<FEN::Network>>,
313 pub build_data: LinkedBuildData,
314 pub sequence: ScriptSequenceKind<FEN::Network>,
315}
316
317impl<FEN: FoundryEvmNetwork> BundledState<FEN> {
318 pub async fn wait_for_pending(mut self) -> Result<Self> {
319 let progress = ScriptProgress::default();
320 let progress_ref = &progress;
321 let config = &self.script_config.config;
322 let futs = self
323 .sequence
324 .sequences_mut()
325 .iter_mut()
326 .enumerate()
327 .map(|(sequence_idx, sequence)| async move {
328 let rpc_url = sequence.rpc_url();
329 let provider =
330 Arc::new(ProviderBuilder::from_config_with_url(config, rpc_url)?.build()?);
331 progress_ref
332 .wait_for_pending(
333 sequence_idx,
334 sequence,
335 &provider,
336 self.script_config.config.transaction_timeout,
337 self.args.confirmations,
338 )
339 .await
340 })
341 .collect::<Vec<_>>();
342
343 let errors = join_all(futs).await.into_iter().filter_map(Result::err).collect::<Vec<_>>();
344
345 self.sequence.save(true, false)?;
346
347 if !errors.is_empty() {
348 return Err(eyre::eyre!("{}", errors.iter().format("\n")));
349 }
350
351 Ok(self)
352 }
353
354 pub async fn broadcast(mut self) -> Result<BroadcastedState<FEN>> {
356 let remaining_transactions =
357 remaining_unsigned_transactions(self.sequence.sequences()).collect::<Vec<_>>();
358 let required_addresses =
359 remaining_transactions.iter().map(|tx| tx.from).collect::<AddressHashSet>();
360
361 if required_addresses.contains(&Config::DEFAULT_SENDER) {
362 eyre::bail!(
363 "You seem to be using Foundry's default sender. Be sure to set your own --sender."
364 );
365 }
366
367 let send_kind = if self.args.unlocked {
368 SendTransactionsKind::Unlocked(required_addresses.clone())
369 } else {
370 let expected_session_sender = script_session_expected_sender_if_configured(
371 &self.script_config.tempo,
372 &required_addresses,
373 )?;
374
375 let mut access_keys: HashMap<SignerScope, TempoAccountsWallet> = HashMap::default();
377 if let Some(expected_session_sender) = expected_session_sender
378 && let Some(session) =
379 self.script_config.tempo.session_signer_for_multi_wallet_any_chain(
380 &self.args.wallets,
381 Some(expected_session_sender),
382 )?
383 {
384 insert_session_access_key_for_remaining_transactions(
385 &mut access_keys,
386 session,
387 &remaining_transactions,
388 )?;
389 }
390
391 let signers: Vec<Address> = self
392 .script_wallets
393 .signers()
394 .map_err(|e| eyre::eyre!("{e}"))?
395 .into_iter()
396 .chain(self.browser_wallet.as_ref().map(|b| b.address()))
397 .collect();
398
399 let mut missing_addresses = Vec::new();
400 let accounts_wallet = self
401 .script_config
402 .evm_opts
403 .networks
404 .is_tempo()
405 .then(TempoAccountsWallet::try_from_default_store)
406 .transpose()?
407 .flatten();
408
409 for tx in &remaining_transactions {
410 let scope = tx.scope();
411 if !signers.contains(&tx.from) && !access_keys.contains_key(&scope) {
412 if let Some(wallet) = accounts_wallet.as_ref()
413 && wallet.has_account(tx.from)?
414 {
415 access_keys.insert(scope, wallet.clone().with_chain_id(tx.chain));
416 } else {
417 missing_addresses.push(tx.from);
418 }
419 }
420 }
421
422 missing_addresses.sort_unstable();
423 missing_addresses.dedup();
424
425 if !missing_addresses.is_empty() {
426 eyre::bail!(
427 "No associated wallet for addresses: {:?}. Unlocked wallets: {:?}",
428 missing_addresses,
429 signers
430 );
431 }
432
433 let signers = self.script_wallets.into_multi_wallet().into_signers()?;
434 let eth_wallets: AddressHashMap<EthereumWallet> =
435 signers.into_iter().map(|(addr, signer)| (addr, signer.into())).collect();
436
437 SendTransactionsKind::Raw { eth_wallets, browser: self.browser_wallet, access_keys }
438 };
439
440 let tempo_sponsor = self.script_config.tempo.sponsor_config().await?.map(Arc::new);
441 if tempo_sponsor.is_some()
442 && self.script_config.tempo.sponsor_sig.is_some()
443 && remaining_transactions.len() > 1
444 {
445 eyre::bail!(
446 "--tempo.sponsor-sig can only sponsor one remaining script transaction; use --tempo.sponsor-signer for multi-transaction scripts"
447 );
448 }
449
450 let progress = ScriptProgress::default();
451
452 for i in 0..self.sequence.sequences().len() {
453 let mut sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
454
455 let provider = Arc::new(
456 ProviderBuilder::from_config_with_url(
457 &self.script_config.config,
458 sequence.rpc_url(),
459 )?
460 .build()?,
461 );
462 let already_broadcasted = sequence.receipts.len();
463
464 let seq_progress = progress.get_sequence_progress(i, sequence);
465
466 if already_broadcasted < sequence.transactions.len() {
467 let is_legacy = Chain::from(sequence.chain).is_legacy() || self.args.legacy;
468 let (gas_price, eip1559_fees) = match (
470 is_legacy,
471 self.args.with_gas_price,
472 self.args.priority_gas_price,
473 ) {
474 (true, Some(gas_price), _) => (Some(gas_price.to()), None),
475 (true, None, _) => (Some(provider.get_gas_price().await?), None),
476 (false, Some(max_fee_per_gas), Some(max_priority_fee_per_gas)) => {
477 let max_fee: u128 = max_fee_per_gas.to();
478 let max_priority: u128 = max_priority_fee_per_gas.to();
479 if max_priority > max_fee {
480 eyre::bail!(
481 "--priority-gas-price ({max_priority}) cannot be higher than --with-gas-price ({max_fee})"
482 );
483 }
484 (
485 None,
486 Some(Eip1559Estimation {
487 max_fee_per_gas: max_fee,
488 max_priority_fee_per_gas: max_priority,
489 }),
490 )
491 }
492 (false, _, _) => {
493 let fees = estimate_eip1559_fees(
494 &provider,
495 self.script_config.config.eip1559_fee_estimate,
496 )
497 .await
498 .wrap_err("Failed to estimate EIP1559 fees. This chain might not support EIP1559, try adding --legacy to your command.")?;
499
500 let browser_suggested_tip = if matches!(
502 &send_kind,
503 SendTransactionsKind::Raw { browser: Some(_), .. }
504 ) {
505 provider.get_max_priority_fee_per_gas().await.ok()
506 } else {
507 None
508 };
509
510 let fees = resolve_broadcast_eip1559_fees(
511 fees,
512 self.args.with_gas_price.map(|p| p.to()),
513 self.args.priority_gas_price.map(|p| p.to()),
514 browser_suggested_tip,
515 )?;
516
517 (None, Some(fees.estimation()))
518 }
519 };
520
521 let sequence_chain = sequence.chain;
524 let mut transactions = Vec::with_capacity(
525 sequence.transactions.len().saturating_sub(already_broadcasted),
526 );
527 for tx_with_metadata in sequence.transactions.iter().skip(already_broadcasted) {
528 let is_fixed_gas_limit = tx_with_metadata.is_fixed_gas_limit;
529
530 let kind = match tx_with_metadata.tx().clone() {
531 TransactionMaybeSigned::Signed { tx, .. } => {
532 if tempo_sponsor.is_some() {
533 eyre::bail!(
534 "cannot attach Tempo sponsor signature to an already signed script transaction"
535 );
536 }
537 SendTransactionKind::Signed(tx)
538 }
539 TransactionMaybeSigned::Unsigned(mut tx) => {
540 let from = tx.from().expect("No sender for onchain transaction!");
541
542 tx.set_chain_id(sequence_chain);
543
544 if tx.kind().is_none() {
547 tx.set_create();
548 }
549
550 if let Some(gas_price) = gas_price {
551 tx.set_gas_price(gas_price);
552 } else {
553 let eip1559_fees = eip1559_fees.expect("was set above");
554 tx.set_max_priority_fee_per_gas(
555 eip1559_fees.max_priority_fee_per_gas,
556 );
557 tx.set_max_fee_per_gas(eip1559_fees.max_fee_per_gas);
558 }
559
560 self.script_config.tempo.apply::<FEN::Network>(&mut tx, None);
561
562 send_kind.for_sender(sequence_chain, &from, tx)?
563 }
564 };
565
566 transactions.push((kind, is_fixed_gas_limit));
567 }
568
569 let estimate_via_rpc = has_different_gas_calc(sequence.chain)
570 || self.script_config.evm_opts.networks.is_tempo()
571 || self.args.skip_simulation;
572
573 let sequential_broadcast = estimate_via_rpc
579 || self.args.slow
580 || required_addresses.len() != 1
581 || !has_batch_support(sequence.chain);
582
583 let batch_size = if sequential_broadcast { 1 } else { 100 };
586 let sequence_chain = sequence.chain;
587
588 for (batch_number, batch) in transactions.chunks(batch_size).enumerate() {
589 seq_progress.inner.write().set_status(&format!(
590 "Sending transactions [{} - {}]",
591 batch_number * batch_size,
592 batch_number * batch_size + std::cmp::min(batch_size, batch.len()) - 1
593 ));
594
595 if !batch.is_empty() {
596 let pending_transactions = batch.iter().enumerate().map(
597 |(position, (kind, is_fixed_gas_limit))| {
598 let index =
599 already_broadcasted + batch_number * batch_size + position;
600 let provider = provider.clone();
601 let tempo_sponsor = tempo_sponsor.clone();
602 async move {
603 let res = kind
604 .clone()
605 .prepare_and_send(
606 provider,
607 sequential_broadcast,
608 *is_fixed_gas_limit,
609 estimate_via_rpc,
610 self.args.gas_estimate_multiplier,
611 tempo_sponsor.as_deref(),
612 Some(sequence_chain.into()),
613 )
614 .await;
615 (res, kind, *is_fixed_gas_limit, 0, None, index)
616 }
617 .boxed()
618 },
619 );
620
621 let mut buffer = pending_transactions.collect::<FuturesUnordered<_>>();
622
623 'send: while let Some((
624 res,
625 kind,
626 is_fixed_gas_limit,
627 attempt,
628 original_res,
629 index,
630 )) = buffer.next().await
631 {
632 if res.is_err()
633 && self.script_config.tempo.sponsor_sig.is_some()
634 && attempt == 0
635 {
636 debug!(
637 "not retrying transaction because --tempo.sponsor-sig is a static signature"
638 );
639 } else if res.is_err() && attempt <= 3 {
640 let provider = provider.clone();
642 let progress = seq_progress.inner.clone();
643 let tempo_sponsor = tempo_sponsor.clone();
644 buffer.push(Box::pin(async move {
645 debug!(err=?res, ?attempt, "retrying transaction ");
646 let attempt = attempt + 1;
647 progress.write().set_status(&format!(
648 "retrying transaction {res:?} (attempt {attempt})"
649 ));
650 tokio::time::sleep(Duration::from_millis(1000 * attempt)).await;
651 let r = kind
652 .clone()
653 .prepare_and_send(
654 provider,
655 sequential_broadcast,
656 is_fixed_gas_limit,
657 estimate_via_rpc,
658 self.args.gas_estimate_multiplier,
659 tempo_sponsor.as_deref(),
660 Some(sequence_chain.into()),
661 )
662 .await;
663 (
664 r,
665 kind,
666 is_fixed_gas_limit,
667 attempt,
668 original_res.or(Some(res)),
669 index,
670 )
671 }));
672
673 continue 'send;
674 }
675
676 let tx_hash = res.wrap_err_with(|| {
678 if let Some(original_res) = original_res {
679 format!(
680 "Failed to send transaction after {attempt} attempts {original_res:?}"
681 )
682 } else {
683 "Failed to send transaction".to_string()
684 }
685 })?;
686 sequence.add_pending(index, tx_hash);
687
688 self.sequence.save(true, false)?;
690 sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
691
692 seq_progress.inner.write().tx_sent(tx_hash);
693 }
694
695 self.sequence.save(true, false)?;
697 sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
698
699 progress
700 .wait_for_pending(
701 i,
702 sequence,
703 &provider,
704 self.script_config.config.transaction_timeout,
705 self.args.confirmations,
706 )
707 .await?
708 }
709 self.sequence.save(true, false)?;
711 sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
712 }
713 }
714
715 let (total_gas, total_gas_price, total_paid) =
716 sequence.receipts.iter().fold((0, 0, 0), |acc, receipt| {
717 let gas_used = receipt.gas_used();
718 let gas_price = receipt.effective_gas_price() as u64;
719 (acc.0 + gas_used, acc.1 + gas_price, acc.2 + gas_used * gas_price)
720 });
721 let paid = format_units(total_paid, 18).unwrap_or_else(|_| "N/A".to_string());
722 let avg_gas_price = total_gas_price
723 .checked_div(sequence.receipts.len() as u64)
724 .and_then(|avg| format_units(avg, 9).ok())
725 .unwrap_or_else(|| "N/A".to_string());
726
727 let token_symbol = NamedChain::try_from(sequence.chain)
728 .unwrap_or_default()
729 .native_currency_symbol()
730 .unwrap_or("ETH");
731 seq_progress.inner.write().set_status(&format!(
732 "Total Paid: {} {} ({} gas * avg {} gwei)\n",
733 paid.trim_end_matches('0'),
734 token_symbol,
735 total_gas,
736 avg_gas_price.trim_end_matches('0').trim_end_matches('.')
737 ));
738 seq_progress.inner.write().finish();
739 }
740
741 if !shell::is_json() {
742 sh_println!("\n\n==========================")?;
743 sh_println!("\nONCHAIN EXECUTION COMPLETE & SUCCESSFUL.")?;
744 }
745
746 Ok(BroadcastedState {
747 args: self.args,
748 script_config: self.script_config,
749 build_data: self.build_data,
750 sequence: self.sequence,
751 })
752 }
753
754 pub async fn verify_preflight_check(&self) -> Result<()> {
755 if self.args.verify_external && self.script_config.config.offline {
756 bail!("External contract verification is unavailable in offline mode");
757 }
758
759 for sequence in self.sequence.sequences() {
760 let chain: Chain = sequence.chain.into();
761 let etherscan_key = self
763 .script_config
764 .config
765 .get_etherscan_api_key(Some(chain))
766 .or_else(|| self.script_config.config.etherscan_api_key.clone());
767 let api_key =
768 self.args.verifier.resolve_api_key(etherscan_key.as_deref()).map(str::to_owned);
769 let has_url = self.args.verifier.verifier_url.is_some();
770 let is_explicit = self.args.verifier.is_explicitly_set();
771 self.args
774 .verifier
775 .resolve(api_key.as_deref(), Some(chain))
776 .client(api_key.as_deref(), Some(chain), has_url, is_explicit)
777 .wrap_err_with(|| {
778 format!("Verification preflight check failed for chain {}", sequence.chain)
779 })?;
780 self.args
782 .verifier
783 .check_credentials(api_key.as_deref(), chain, &self.script_config.config)
784 .await
785 .wrap_err_with(|| {
786 format!("Verification preflight check failed for chain {}", sequence.chain)
787 })?;
788 }
789
790 Ok(())
791 }
792}
793
794impl BundledState<TempoEvmNetwork> {
795 pub async fn broadcast_batch(mut self) -> Result<BroadcastedState<TempoEvmNetwork>> {
800 if self.sequence.sequences().len() != 1 {
802 bail!(
803 "--batch mode only supports single-chain scripts. \
804 Use --multi without --batch for multi-chain."
805 );
806 }
807
808 let sequence = self.sequence.sequences_mut().get_mut(0).unwrap();
809 let total_transactions = sequence.transactions.len();
810 let remaining_start = remaining_transaction_start(sequence);
811
812 if remaining_start == total_transactions {
813 sh_println!("No transactions to broadcast in batch mode.")?;
814 return Ok(BroadcastedState {
815 args: self.args,
816 script_config: self.script_config,
817 build_data: self.build_data,
818 sequence: self.sequence,
819 });
820 }
821
822 if let Some((idx, _)) =
825 sequence.transactions().enumerate().find(|(_, tx)| !tx.is_unsigned())
826 {
827 bail!(
828 "--batch cannot include pre-signed transactions (found at position {}); \
829 batch mode signs a single atomic transaction from one sender.",
830 idx + 1
831 );
832 }
833
834 let senders: AddressHashSet = remaining_transactions(sequence)
836 .filter(|tx| tx.is_unsigned())
837 .filter_map(|tx| tx.from())
838 .collect();
839
840 if senders.len() != 1 {
841 bail!(
842 "--batch mode requires all transactions to have the same sender. \
843 Found {} unique senders: {:?}",
844 senders.len(),
845 senders
846 );
847 }
848
849 let sender = *senders.iter().next().unwrap();
850 let chain_id = sequence.chain;
851
852 if sender == Config::DEFAULT_SENDER {
853 bail!(
854 "You seem to be using Foundry's default sender. Be sure to set your own --sender."
855 );
856 }
857
858 let provider = Arc::new(
859 ProviderBuilder::<TempoNetwork>::from_config_with_url(
860 &self.script_config.config,
861 sequence.rpc_url(),
862 )?
863 .build()?,
864 );
865
866 let pending_batch_hash: Option<TxHash> =
874 sequence.transactions.iter().skip(remaining_start).find_map(|tx| tx.hash);
875
876 if let Some(tx_hash) = pending_batch_hash {
877 sh_println!(
878 "Resuming batch: tx {tx_hash:#x} already submitted, waiting for receipt..."
879 )?;
880
881 let timeout = self.script_config.config.transaction_timeout;
882 let receipt_result = tokio::time::timeout(
883 Duration::from_secs(timeout),
884 wait_for_batch_receipt(provider.as_ref(), tx_hash, self.args.confirmations),
885 )
886 .await;
887
888 match receipt_result {
889 Ok(Ok(Some(receipt))) => {
890 let success = receipt.status();
892 if success {
893 sh_println!(
894 "Batch transaction confirmed in block {}",
895 receipt.block_number.unwrap_or(0)
896 )?;
897 } else {
898 bail!("Batch transaction failed (reverted)");
899 }
900
901 let sequence = self.sequence.sequences_mut().get_mut(0).unwrap();
902 let remaining_len = sequence.transactions.len() - remaining_start;
903 let per_tx_addresses: Vec<Option<Address>> = sequence
904 .transactions
905 .iter()
906 .skip(remaining_start)
907 .map(|tx| match tx.call_kind {
908 CallKind::Create | CallKind::Create2 => tx.contract_address,
909 _ => None,
910 })
911 .collect();
912
913 for (idx, addr) in per_tx_addresses.iter().enumerate() {
914 if let Some(addr) = addr {
915 sh_println!(" call[{idx}] deployed at: {addr:#x}")?;
916 }
917 }
918
919 for addr in &per_tx_addresses {
920 let mut tx_receipt = receipt.clone();
921 tx_receipt.contract_address = *addr;
922 sequence.receipts.push(tx_receipt);
923 }
924 sequence.remove_pending(tx_hash);
926
927 let chain = sequence.chain;
928 let _ = sequence;
929 self.sequence.save(true, false)?;
930
931 let total_gas = receipt.gas_used();
932 let gas_price = receipt.effective_gas_price() as u64;
933 let total_paid = total_gas * gas_price;
934 let paid = format_units(total_paid, 18).unwrap_or_else(|_| "N/A".to_string());
935 let gas_price_gwei =
936 format_units(gas_price, 9).unwrap_or_else(|_| "N/A".to_string());
937 let token_symbol = NamedChain::try_from(chain)
938 .unwrap_or_default()
939 .native_currency_symbol()
940 .unwrap_or("ETH");
941 sh_println!(
942 "\nTotal Paid: {} {} ({} gas * {} gwei)\n(resumed from previous run, {} tx(s))",
943 paid.trim_end_matches('0'),
944 token_symbol,
945 total_gas,
946 gas_price_gwei.trim_end_matches('0').trim_end_matches('.'),
947 remaining_len,
948 )?;
949
950 if !shell::is_json() {
951 sh_println!("\n\n==========================")?;
952 sh_println!("\nBATCH EXECUTION COMPLETE & SUCCESSFUL.")?;
953 sh_println!(
954 "All {} calls executed atomically in a single transaction.",
955 remaining_len
956 )?;
957 }
958
959 return Ok(BroadcastedState {
960 args: self.args,
961 script_config: self.script_config,
962 build_data: self.build_data,
963 sequence: self.sequence,
964 });
965 }
966 Ok(Ok(None)) => {
967 sh_println!(
969 "Batch tx {tx_hash:#x} was dropped from the mempool; will re-send..."
970 )?;
971 let sequence = self.sequence.sequences_mut().get_mut(0).unwrap();
972 sequence.remove_pending(tx_hash);
973 for tx in sequence.transactions.iter_mut().skip(remaining_start) {
974 tx.hash = None;
975 }
976 self.sequence.save(true, false)?;
977 }
979 Ok(Err(e)) => return Err(e),
980 Err(_) => {
981 sh_println!(
984 "Timeout waiting for batch tx {tx_hash:#x}; clearing checkpoint so \
985 --resume can re-send a replacement."
986 )?;
987 let sequence = self.sequence.sequences_mut().get_mut(0).unwrap();
988 sequence.remove_pending(tx_hash);
989 for tx in sequence.transactions.iter_mut().skip(remaining_start) {
990 tx.hash = None;
991 }
992 self.sequence.save(true, false)?;
993 return Err(eyre::eyre!(
994 "Timeout waiting for batch transaction receipt (tx: {tx_hash:#x}). \
995 The transaction hash has been cleared; run with --resume to retry."
996 ));
997 }
998 }
999 }
1000
1001 let sequence = self.sequence.sequences_mut().get_mut(0).unwrap();
1003
1004 let tempo_sponsor = self.script_config.tempo.sponsor_config().await?;
1005
1006 enum BatchSigner {
1008 Unlocked,
1009 Wallet(EthereumWallet),
1010 TempoKeychain(Box<TempoAccountsWallet>),
1011 }
1012
1013 let mut batch_signer = if self.args.unlocked {
1014 BatchSigner::Unlocked
1015 } else if let Some(session) = self.script_config.tempo.session_signer_for_multi_wallet(
1016 &self.args.wallets,
1017 Some(sender),
1018 chain_id,
1019 )? {
1020 BatchSigner::TempoKeychain(Box::new(session.access_key))
1021 } else {
1022 let mut signers = self.script_wallets.into_multi_wallet().into_signers()?;
1023 if let Some(signer) = signers.remove(&sender) {
1024 BatchSigner::Wallet(EthereumWallet::new(signer))
1025 } else {
1026 if self.script_config.evm_opts.networks.is_tempo()
1028 && let Some(wallet) = TempoAccountsWallet::try_from_default_store()?
1029 && wallet.has_account(sender)?
1030 {
1031 BatchSigner::TempoKeychain(Box::new(wallet.with_chain_id(chain_id)))
1032 } else {
1033 bail!("No wallet found for sender {}", sender);
1034 }
1035 }
1036 };
1037
1038 let create2_deployer = self.script_config.evm_opts.create2_deployer;
1039 let mut calls: Vec<Call> = Vec::new();
1040 for (call_index, tx) in remaining_transactions(sequence).enumerate() {
1041 if tx.authorization_list().is_some_and(|l| !l.is_empty()) {
1044 bail!(
1045 "--batch does not support EIP-7702 authorization lists \
1046 (found at transaction {}); use regular broadcast instead.",
1047 call_index + 1
1048 );
1049 }
1050 if let TransactionMaybeSigned::Unsigned(inner) = tx
1052 && inner.blob_sidecar().is_some()
1053 {
1054 bail!(
1055 "--batch does not support blob (EIP-4844) transactions \
1056 (found at transaction {}); use regular broadcast instead.",
1057 call_index + 1
1058 );
1059 }
1060
1061 let to = match tx.to() {
1064 Some(addr) => TxKind::Call(addr),
1065 None => {
1066 bail!(
1067 "Unexpected raw CREATE in --batch mode at position {} — \
1068 this is a bug; CREATEs should have been rewritten by the inspector.",
1069 call_index + 1
1070 );
1071 }
1072 };
1073 let value = tx.value().unwrap_or(U256::ZERO);
1074 let input = tx.input().cloned().unwrap_or_default();
1075
1076 calls.push(Call { to, value, input });
1077 }
1078
1079 if calls.is_empty() {
1080 sh_println!("No transactions to broadcast in batch mode.")?;
1081 return Ok(BroadcastedState {
1082 args: self.args,
1083 script_config: self.script_config,
1084 build_data: self.build_data,
1085 sequence: self.sequence,
1086 });
1087 }
1088
1089 let needs_factory = sequence
1091 .transactions
1092 .iter()
1093 .skip(remaining_start)
1094 .any(|tx| matches!(tx.call_kind, CallKind::Create | CallKind::Create2));
1095 if needs_factory {
1096 let code = provider.get_code_at(create2_deployer).await?;
1097 if keccak256(&code) != DEFAULT_CREATE2_DEPLOYER_CODEHASH {
1098 bail!(
1099 "CREATE2 deployer {create2_deployer:#x} is not deployed on this Tempo network; \
1100 --batch requires it. Deploy it first and retry."
1101 );
1102 }
1103 }
1104
1105 sh_println!(
1106 "\n## Broadcasting batch transaction with {} call(s) to chain {}...",
1107 calls.len(),
1108 sequence.chain
1109 )?;
1110
1111 let nonce = provider.get_transaction_count(sender).await?;
1113
1114 let fees = estimate_eip1559_fees(&provider, self.script_config.config.eip1559_fee_estimate)
1116 .await?;
1117 let fees = resolve_broadcast_eip1559_fees(
1118 fees,
1119 self.args.with_gas_price.map(|p| p.to()),
1120 self.args.priority_gas_price.map(|p| p.to()),
1121 None,
1122 )?;
1123 let max_fee_per_gas = fees.max_fee_per_gas;
1124 let max_priority_fee_per_gas = fees.max_priority_fee_per_gas;
1125
1126 let mut batch_tx = TempoTransactionRequest {
1127 inner: TransactionRequest {
1128 from: Some(sender),
1129 to: None,
1130 value: None,
1131 input: Default::default(),
1132 nonce: Some(nonce),
1133 chain_id: Some(chain_id),
1134 max_fee_per_gas: Some(max_fee_per_gas),
1135 max_priority_fee_per_gas: Some(max_priority_fee_per_gas),
1136 ..Default::default()
1137 },
1138 fee_token: self.script_config.tempo.fee_token,
1139 calls: calls.clone(),
1140 nonce_key: self.script_config.tempo.expiring_nonce.then_some(U256::MAX),
1141 valid_before: self.script_config.tempo.valid_before.and_then(NonZeroU64::new),
1142 ..Default::default()
1143 };
1144 self.script_config.tempo.apply::<TempoNetwork>(&mut batch_tx, None);
1145 let fee_token = if let Some(sponsor) = &tempo_sponsor {
1146 sponsor
1147 .resolve_and_set_fee_token(
1148 Some(provider.as_ref()),
1149 Some(Chain::from_named(NamedChain::Tempo)),
1150 &mut batch_tx,
1151 )
1152 .await?;
1153 None
1154 } else {
1155 resolve_and_set_fee_token(
1156 Some(provider.as_ref()),
1157 Some(Chain::from_named(NamedChain::Tempo)),
1158 &mut batch_tx,
1159 Some(sender),
1160 )
1161 .await?
1162 };
1163
1164 if let BatchSigner::TempoKeychain(wallet) = &mut batch_signer {
1165 **wallet = batch_tx.prepare_with_tempo_wallet(provider.as_ref(), wallet).await?;
1166 }
1167
1168 estimate_gas(&mut batch_tx, provider.as_ref(), self.args.gas_estimate_multiplier, false)
1170 .await?;
1171
1172 sh_println!("Estimated gas: {}", batch_tx.inner.gas.unwrap_or(0))?;
1173
1174 if let Some(sponsor) = &tempo_sponsor {
1175 sponsor.attach_and_print::<TempoNetwork>(&mut batch_tx, sender).await?;
1176 } else {
1177 maybe_print_fee_token(Some(provider.as_ref()), fee_token).await?;
1178 }
1179
1180 let tx_hash = match batch_signer {
1182 BatchSigner::Wallet(wallet) => {
1183 let provider_with_wallet =
1184 alloy_provider::ProviderBuilder::<_, _, TempoNetwork>::default()
1185 .wallet(wallet)
1186 .connect_provider(provider.as_ref());
1187
1188 let pending = provider_with_wallet.send_transaction(batch_tx).await?;
1189 *pending.tx_hash()
1190 }
1191 BatchSigner::TempoKeychain(wallet) => {
1192 let raw_tx = batch_tx.sign_with_tempo_wallet(&wallet).await?;
1193
1194 let pending = provider.send_raw_transaction(&raw_tx).await?;
1195 *pending.tx_hash()
1196 }
1197 BatchSigner::Unlocked => {
1198 let pending = provider.send_transaction(batch_tx).await?;
1199 *pending.tx_hash()
1200 }
1201 };
1202
1203 sh_println!("Batch transaction sent: {:#x}", tx_hash)?;
1204
1205 for tx in sequence.transactions.iter_mut().skip(remaining_start) {
1209 tx.hash = Some(tx_hash);
1210 }
1211 if !sequence.pending.contains(&tx_hash) {
1212 sequence.pending.push(tx_hash);
1213 }
1214 self.sequence.save(true, false)?;
1215
1216 let timeout = self.script_config.config.transaction_timeout;
1218 let receipt = tokio::time::timeout(
1219 Duration::from_secs(timeout),
1220 wait_for_batch_receipt(provider.as_ref(), tx_hash, self.args.confirmations),
1221 )
1222 .await
1223 .map_err(|_| eyre::eyre!("Timeout waiting for batch transaction receipt (tx: {tx_hash:#x}). Run with --resume to retry."))??
1224 .ok_or_else(|| eyre::eyre!("Batch transaction {tx_hash:#x} was dropped from the mempool. Run with --resume to retry."))?;
1225
1226 let success = receipt.status();
1227 if success {
1228 sh_println!(
1229 "Batch transaction confirmed in block {}",
1230 receipt.block_number.unwrap_or(0)
1231 )?;
1232 } else {
1233 bail!("Batch transaction failed (reverted)");
1234 }
1235
1236 let sequence = self.sequence.sequences_mut().get_mut(0).unwrap();
1237 sequence.remove_pending(tx_hash);
1238
1239 let remaining_len = sequence.transactions.len() - remaining_start;
1241 if calls.len() != remaining_len {
1242 bail!(
1243 "batch call count ({}) does not match remaining transactions ({}); \
1244 refusing to push misaligned receipts",
1245 calls.len(),
1246 remaining_len
1247 );
1248 }
1249 let per_tx_addresses: Vec<Option<Address>> = sequence
1254 .transactions
1255 .iter()
1256 .skip(remaining_start)
1257 .map(|tx| match tx.call_kind {
1258 CallKind::Create | CallKind::Create2 => tx.contract_address,
1259 _ => None,
1260 })
1261 .collect();
1262
1263 for (idx, addr) in per_tx_addresses.iter().enumerate() {
1264 if let Some(addr) = addr {
1265 sh_println!(" call[{idx}] deployed at: {addr:#x}")?;
1266 }
1267 }
1268
1269 for addr in &per_tx_addresses {
1271 let mut tx_receipt = receipt.clone();
1272 tx_receipt.contract_address = *addr;
1273 sequence.receipts.push(tx_receipt);
1274 }
1275
1276 let chain = sequence.chain;
1277 let _ = sequence;
1278
1279 self.sequence.save(true, false)?;
1280
1281 let total_gas = receipt.gas_used();
1282 let gas_price = receipt.effective_gas_price() as u64;
1283 let total_paid = total_gas * gas_price;
1284 let paid = format_units(total_paid, 18).unwrap_or_else(|_| "N/A".to_string());
1285 let gas_price_gwei = format_units(gas_price, 9).unwrap_or_else(|_| "N/A".to_string());
1286
1287 let token_symbol = NamedChain::try_from(chain)
1288 .unwrap_or_default()
1289 .native_currency_symbol()
1290 .unwrap_or("ETH");
1291 sh_println!(
1292 "\nTotal Paid: {} {} ({} gas * {} gwei)",
1293 paid.trim_end_matches('0'),
1294 token_symbol,
1295 total_gas,
1296 gas_price_gwei.trim_end_matches('0').trim_end_matches('.')
1297 )?;
1298
1299 if !shell::is_json() {
1300 sh_println!("\n\n==========================")?;
1301 sh_println!("\nBATCH EXECUTION COMPLETE & SUCCESSFUL.")?;
1302 sh_println!("All {} calls executed atomically in a single transaction.", calls.len())?;
1303 }
1304
1305 Ok(BroadcastedState {
1306 args: self.args,
1307 script_config: self.script_config,
1308 build_data: self.build_data,
1309 sequence: self.sequence,
1310 })
1311 }
1312}
1313
1314async fn wait_for_batch_receipt<N: Network>(
1315 provider: &RootProvider<N>,
1316 tx_hash: TxHash,
1317 confirmations: u64,
1318) -> Result<Option<N::ReceiptResponse>> {
1319 loop {
1320 if let Some(receipt) = provider.get_transaction_receipt(tx_hash).await?
1321 && let Some(receipt_block) = receipt.block_number()
1322 {
1323 let latest_block = provider.get_block_number().await?;
1324 if latest_block >= receipt_block.saturating_add(confirmations.saturating_sub(1)) {
1325 return Ok(Some(receipt));
1326 }
1327 }
1328
1329 if provider.get_transaction_by_hash(tx_hash).await?.is_none() {
1330 return Ok(None);
1331 }
1332
1333 tokio::time::sleep(Duration::from_millis(500)).await;
1334 }
1335}
1336
1337pub async fn estimate_gas<N: Network, P: Provider<N>>(
1338 tx: &mut N::TransactionRequest,
1339 provider: &P,
1340 estimate_multiplier: u64,
1341 tempo_browser: bool,
1342) -> Result<()>
1343where
1344 N::TransactionRequest: FoundryTransactionBuilder<N>,
1345{
1346 tx.reset_gas_limit();
1349
1350 let request =
1351 if tempo_browser { tx.browser_wallet_gas_estimation_request() } else { tx.clone() };
1352 tx.set_gas_limit(
1353 provider.estimate_gas(request).await.wrap_err("Failed to estimate gas for tx")?
1354 * estimate_multiplier
1355 / 100,
1356 );
1357 Ok(())
1358}
1359
1360pub(super) async fn next_nonce_resolved(
1362 caller: Address,
1363 evm_opts: &EvmOpts,
1364 fork: &ResolvedFork,
1365) -> eyre::Result<u64> {
1366 evm_opts.transaction_count_at_resolved_fork(caller, fork).await
1367}
1368
1369fn reject_access_key_create<N: Network>(
1370 tx: &N::TransactionRequest,
1371 uses_access_key: bool,
1372) -> Result<()>
1373where
1374 N::TransactionRequest: FoundryTransactionBuilder<N>,
1375{
1376 if uses_access_key && tx.tempo_calls().iter().any(|(to, _)| to.is_create()) {
1377 bail!("Tempo access-key transactions cannot use CREATE");
1378 }
1379 Ok(())
1380}
1381
1382fn convert_tempo_aa_create<N: Network>(tx: &mut N::TransactionRequest)
1383where
1384 N::TransactionRequest: FoundryTransactionBuilder<N>,
1385{
1386 if tx.is_tempo_aa() {
1387 tx.convert_create_to_call();
1388 }
1389}
1390
1391#[cfg(test)]
1392mod tests {
1393 use super::*;
1394 use alloy_consensus::{Eip658Value, Receipt, ReceiptEnvelope, ReceiptWithBloom};
1395 use alloy_eips::BlockId;
1396 use alloy_network::Ethereum;
1397 use alloy_primitives::{Bloom, address};
1398 use alloy_rpc_types::TransactionReceipt;
1399 use alloy_signer::Signer;
1400 use forge_script_sequence::TransactionWithMetadata;
1401
1402 const ROOT_PRIVATE_KEY: &str =
1403 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
1404 const ACCESS_KEY_PRIVATE_KEY: &str =
1405 "0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0";
1406
1407 #[tokio::test(flavor = "multi_thread")]
1408 async fn next_nonce_uses_exact_fork_hash() {
1409 let (_api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
1410 let provider = handle.http_provider();
1411 let sender = handle.dev_accounts().next().unwrap();
1412 let recipient = Address::with_last_byte(1);
1413
1414 let receipt = provider
1415 .send_transaction(
1416 TransactionRequest::default()
1417 .from(sender)
1418 .to(recipient)
1419 .value(U256::from(1))
1420 .into(),
1421 )
1422 .await
1423 .unwrap()
1424 .get_receipt()
1425 .await
1426 .unwrap();
1427 let block_number = receipt.block_number.unwrap();
1428 let evm_opts = EvmOpts {
1429 fork_url: Some(handle.http_endpoint()),
1430 fork_block_number: Some(block_number),
1431 ..Default::default()
1432 };
1433 let fork = evm_opts.resolve_fork().await.unwrap().unwrap();
1434 assert_eq!(next_nonce_resolved(sender, &evm_opts, &fork).await.unwrap(), 1);
1435
1436 provider
1437 .raw_request::<_, ()>("anvil_reorg".into(), (1_u64, Vec::<serde_json::Value>::new()))
1438 .await
1439 .unwrap();
1440 assert_eq!(
1441 provider
1442 .get_transaction_count(sender)
1443 .block_id(BlockId::number(block_number))
1444 .await
1445 .unwrap(),
1446 0
1447 );
1448
1449 match next_nonce_resolved(sender, &evm_opts, &fork).await {
1450 Ok(0) => panic!("the exact lookup fell back to the replacement block"),
1451 Ok(1) | Err(_) => {}
1452 Ok(nonce) => panic!("unexpected nonce: {nonce}"),
1453 }
1454 }
1455
1456 #[test]
1457 fn access_key_signer_takes_precedence_over_same_sender_wallet() {
1458 let root = foundry_wallets::utils::create_private_key_signer(ROOT_PRIVATE_KEY).unwrap();
1459 let root_address = root.address();
1460 let access_key =
1461 foundry_wallets::utils::create_local_signer(ACCESS_KEY_PRIVATE_KEY).unwrap();
1462 let access_key_address = access_key.address();
1463 let mut eth_wallets = AddressHashMap::default();
1464 eth_wallets.insert(root_address, EthereumWallet::new(root));
1465 let mut access_keys = HashMap::default();
1466 access_keys.insert(
1467 SignerScope::new(4217, root_address),
1468 TempoAccountsWallet::from_secp256k1(root_address, access_key, None).with_chain_id(4217),
1469 );
1470 let send_kind =
1471 SendTransactionsKind::<Ethereum>::Raw { eth_wallets, browser: None, access_keys };
1472
1473 let tx = TransactionRequest { from: Some(root_address), ..Default::default() };
1474 let sender = send_kind.for_sender(4217, &root_address, tx).unwrap();
1475
1476 match sender {
1477 SendTransactionKind::AccessKey(_, wallet) => {
1478 assert_eq!(wallet.key_id().unwrap(), access_key_address);
1479 assert_eq!(wallet.account(), root_address);
1480 }
1481 _ => panic!("expected access key signer"),
1482 }
1483 }
1484
1485 #[test]
1486 fn access_key_signer_is_scoped_to_chain() {
1487 let root = foundry_wallets::utils::create_private_key_signer(ROOT_PRIVATE_KEY).unwrap();
1488 let root_address = root.address();
1489 let access_key =
1490 foundry_wallets::utils::create_local_signer(ACCESS_KEY_PRIVATE_KEY).unwrap();
1491 let mut eth_wallets = AddressHashMap::default();
1492 eth_wallets.insert(root_address, EthereumWallet::new(root));
1493 let mut access_keys = HashMap::default();
1494 access_keys.insert(
1495 SignerScope::new(4217, root_address),
1496 TempoAccountsWallet::from_secp256k1(root_address, access_key, None).with_chain_id(4217),
1497 );
1498 let send_kind =
1499 SendTransactionsKind::<Ethereum>::Raw { eth_wallets, browser: None, access_keys };
1500
1501 let tx = TransactionRequest { from: Some(root_address), ..Default::default() };
1502 let sender = send_kind.for_sender(1, &root_address, tx).unwrap();
1503
1504 match sender {
1505 SendTransactionKind::Raw(_, wallet) => {
1506 assert_eq!(wallet.default_signer().address(), root_address);
1507 }
1508 _ => panic!("expected root wallet signer for non-session chain"),
1509 }
1510 }
1511
1512 #[test]
1513 fn remaining_unsigned_transactions_skip_completed_transactions() {
1514 let completed = address!("0x1111111111111111111111111111111111111111");
1515 let remaining_sender = address!("0x2222222222222222222222222222222222222222");
1516 let mut sequence = ScriptSequence::<Ethereum> {
1517 chain: 4217,
1518 transactions: [script_tx(completed), script_tx(remaining_sender)].into(),
1519 receipts: vec![receipt()],
1520 ..Default::default()
1521 };
1522
1523 let remaining =
1524 remaining_unsigned_transactions(std::slice::from_ref(&sequence)).collect::<Vec<_>>();
1525 assert_eq!(remaining.len(), 1);
1526 assert_eq!(remaining[0].from, remaining_sender);
1527 assert_eq!(remaining[0].chain, 4217);
1528
1529 sequence.receipts.push(receipt());
1530 let remaining =
1531 remaining_unsigned_transactions(std::slice::from_ref(&sequence)).collect::<Vec<_>>();
1532 assert!(remaining.is_empty());
1533
1534 let completed_sequence = ScriptSequence::<Ethereum> {
1535 chain: 1,
1536 transactions: [script_tx(completed)].into(),
1537 receipts: vec![receipt()],
1538 ..Default::default()
1539 };
1540 let remaining_sequence = ScriptSequence::<Ethereum> {
1541 chain: 4217,
1542 transactions: [script_tx(remaining_sender)].into(),
1543 ..Default::default()
1544 };
1545
1546 let remaining = remaining_unsigned_transactions(&[completed_sequence, remaining_sequence])
1547 .collect::<Vec<_>>();
1548 assert_eq!(remaining.len(), 1);
1549 assert_eq!(remaining[0].chain, 4217);
1550 }
1551
1552 #[test]
1553 fn remaining_transactions_skip_receipt_prefix() {
1554 let completed = address!("0x1111111111111111111111111111111111111111");
1555 let second = address!("0x2222222222222222222222222222222222222222");
1556 let third = address!("0x3333333333333333333333333333333333333333");
1557 let mut sequence = ScriptSequence::<Ethereum> {
1558 chain: 4217,
1559 transactions: [script_tx(completed), script_tx(second), script_tx(third)].into(),
1560 receipts: vec![receipt()],
1561 ..Default::default()
1562 };
1563
1564 let remaining =
1565 remaining_transactions(&sequence).map(|tx| tx.from().unwrap()).collect::<Vec<_>>();
1566
1567 assert_eq!(remaining, vec![second, third]);
1568
1569 sequence.receipts = (0..4).map(|_| receipt()).collect();
1570 assert!(remaining_transactions(&sequence).next().is_none());
1571 }
1572
1573 #[tokio::test]
1574 async fn access_key_sets_key_id_before_estimation() {
1575 let root_address = address!("0x1111111111111111111111111111111111111111");
1576 let access_key =
1577 foundry_wallets::utils::create_local_signer(ACCESS_KEY_PRIVATE_KEY).unwrap();
1578 let access_key_address = access_key.address();
1579 let access_key_wallet =
1580 TempoAccountsWallet::from_secp256k1(root_address, access_key, None).with_chain_id(4217);
1581 let mut sender = SendTransactionKind::<TempoNetwork>::AccessKey(
1582 TempoTransactionRequest {
1583 inner: TransactionRequest { from: Some(root_address), ..Default::default() },
1584 ..Default::default()
1585 },
1586 Box::new(access_key_wallet),
1587 );
1588 let provider =
1589 RootProvider::<TempoNetwork>::new_http("http://localhost:8545".parse().unwrap());
1590
1591 sender
1592 .prepare(
1593 &provider,
1594 false,
1595 true,
1596 false,
1597 100,
1598 None,
1599 Some(Chain::from_named(NamedChain::Mainnet)),
1600 )
1601 .await
1602 .unwrap();
1603
1604 match sender {
1605 SendTransactionKind::AccessKey(tx, _) => {
1606 assert_eq!(tx.key_id, Some(access_key_address));
1607 }
1608 _ => panic!("expected access key transaction"),
1609 }
1610 }
1611
1612 #[test]
1613 fn tempo_aa_create_moves_deployment_into_calls() {
1614 let mut tx = TempoTransactionRequest {
1615 inner: TransactionRequest { to: Some(TxKind::Create), ..Default::default() },
1616 fee_token: Some(address!("0x20c0000000000000000000000000000000000000")),
1617 ..Default::default()
1618 };
1619
1620 convert_tempo_aa_create::<TempoNetwork>(&mut tx);
1621
1622 assert!(tx.inner.to.is_none());
1623 assert_eq!(tx.calls.len(), 1);
1624 assert!(tx.calls[0].to.is_create());
1625 }
1626
1627 #[test]
1628 fn tempo_access_key_create_is_rejected_before_preparation() {
1629 let tx = TempoTransactionRequest {
1630 inner: TransactionRequest { to: Some(TxKind::Create), ..Default::default() },
1631 ..Default::default()
1632 };
1633
1634 let error = reject_access_key_create::<TempoNetwork>(&tx, true).unwrap_err();
1635
1636 assert!(error.to_string().contains("Tempo access-key transactions cannot use CREATE"));
1637 }
1638
1639 fn script_tx(from: Address) -> TransactionWithMetadata<Ethereum> {
1640 TransactionWithMetadata::from_tx_request(TransactionMaybeSigned::new(TransactionRequest {
1641 from: Some(from),
1642 ..Default::default()
1643 }))
1644 }
1645
1646 fn receipt() -> TransactionReceipt {
1647 TransactionReceipt {
1648 inner: ReceiptEnvelope::Legacy(ReceiptWithBloom {
1649 receipt: Receipt {
1650 status: Eip658Value::success(),
1651 cumulative_gas_used: 0,
1652 logs: vec![],
1653 },
1654 logs_bloom: Bloom::ZERO,
1655 }),
1656 transaction_hash: Default::default(),
1657 transaction_index: None,
1658 block_hash: None,
1659 block_number: None,
1660 gas_used: 0,
1661 effective_gas_price: 0,
1662 blob_gas_used: None,
1663 blob_gas_price: None,
1664 from: Address::ZERO,
1665 to: None,
1666 contract_address: None,
1667 }
1668 }
1669}