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