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