1use std::{cmp::Ordering, num::NonZeroU64, sync::Arc, time::Duration};
2
3use crate::{
4 ScriptArgs, ScriptConfig, build::LinkedBuildData, progress::ScriptProgress,
5 sequence::ScriptSequenceKind, verify::BroadcastedState,
6};
7use alloy_chains::{Chain, NamedChain};
8use alloy_consensus::{SignableTransaction, Signed};
9use alloy_eips::{BlockId, eip2718::Encodable2718};
10use alloy_network::{
11 EthereumWallet, Network, NetworkTransactionBuilder, ReceiptResponse, TransactionBuilder,
12};
13use alloy_primitives::{
14 Address, TxHash, TxKind, U256,
15 map::{AddressHashMap, AddressHashSet},
16 utils::format_units,
17};
18use alloy_provider::{Provider, RootProvider, utils::Eip1559Estimation};
19use alloy_rpc_types::TransactionRequest;
20use alloy_signer::Signature;
21use eyre::{Context, Result, bail};
22use forge_verify::provider::VerificationProviderType;
23use foundry_cheatcodes::Wallets;
24use foundry_cli::utils::{has_batch_support, has_different_gas_calc};
25use foundry_common::{
26 FoundryTransactionBuilder, TransactionMaybeSigned,
27 provider::{ProviderBuilder, try_get_http_provider},
28 shell,
29 tempo::TempoSponsor,
30};
31use foundry_config::Config;
32use foundry_evm::core::evm::{FoundryEvmNetwork, TempoEvmNetwork};
33use foundry_wallets::{
34 TempoAccessKeyConfig, WalletSigner,
35 tempo::{TempoLookup, lookup_signer},
36 wallet_browser::signer::BrowserSigner,
37};
38use futures::{FutureExt, StreamExt, future::join_all, stream::FuturesUnordered};
39use itertools::Itertools;
40use tempo_alloy::{TempoNetwork, rpc::TempoTransactionRequest};
41use tempo_primitives::transaction::Call;
42
43pub async fn estimate_gas<N: Network, P: Provider<N>>(
44 tx: &mut N::TransactionRequest,
45 provider: &P,
46 estimate_multiplier: u64,
47) -> Result<()>
48where
49 N::TransactionRequest: FoundryTransactionBuilder<N>,
50{
51 tx.reset_gas_limit();
54
55 tx.set_gas_limit(
56 provider.estimate_gas(tx.clone()).await.wrap_err("Failed to estimate gas for tx")?
57 * estimate_multiplier
58 / 100,
59 );
60 Ok(())
61}
62
63pub async fn next_nonce(
64 caller: Address,
65 provider_url: &str,
66 block_number: Option<u64>,
67) -> eyre::Result<u64> {
68 let provider = try_get_http_provider(provider_url)
69 .wrap_err_with(|| format!("bad fork_url provider: {provider_url}"))?;
70
71 let block_id = block_number.map_or(BlockId::latest(), BlockId::number);
72 Ok(provider.get_transaction_count(caller).block_id(block_id).await?)
73}
74
75#[derive(Clone)]
77pub enum SendTransactionKind<'a, N: Network> {
78 Unlocked(N::TransactionRequest),
79 Raw(N::TransactionRequest, &'a EthereumWallet),
80 Browser(N::TransactionRequest, &'a BrowserSigner<N>),
81 Signed(N::TxEnvelope),
82 AccessKey(N::TransactionRequest, &'a WalletSigner, &'a TempoAccessKeyConfig),
83}
84
85impl<'a, N: Network> SendTransactionKind<'a, N>
86where
87 N::TxEnvelope: From<Signed<N::UnsignedTx>>,
88 N::UnsignedTx: SignableTransaction<Signature>,
89 N::TransactionRequest: FoundryTransactionBuilder<N>,
90{
91 pub async fn prepare(
98 &mut self,
99 provider: &RootProvider<N>,
100 sequential_broadcast: bool,
101 is_fixed_gas_limit: bool,
102 estimate_via_rpc: bool,
103 estimate_multiplier: u64,
104 tempo_sponsor: Option<&TempoSponsor>,
105 ) -> Result<()> {
106 let access_key_authorization = match self {
107 Self::AccessKey(_, _, access_key) => Some((
108 access_key.wallet_address,
109 access_key.key_address,
110 access_key.key_authorization.clone(),
111 )),
112 _ => None,
113 };
114
115 if let Self::Raw(tx, _)
116 | Self::Unlocked(tx)
117 | Self::Browser(tx, _)
118 | Self::AccessKey(tx, _, _) = self
119 {
120 if sequential_broadcast {
121 let from = tx.from().expect("no sender");
122
123 let tx_nonce = tx.nonce().expect("no nonce");
124 for attempt in 0..5 {
125 let nonce = provider.get_transaction_count(from).await?;
126 match nonce.cmp(&tx_nonce) {
127 Ordering::Greater => {
128 bail!(
129 "EOA nonce changed unexpectedly while sending transactions. Expected {tx_nonce} got {nonce} from provider."
130 )
131 }
132 Ordering::Less => {
133 if attempt == 4 {
134 bail!(
135 "After 5 attempts, provider nonce ({nonce}) is still behind expected nonce ({tx_nonce})."
136 )
137 }
138 warn!(
139 "Expected nonce ({tx_nonce}) is ahead of provider nonce ({nonce}). Retrying in 1 second..."
140 );
141 tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
142 }
143 Ordering::Equal => {
144 break;
146 }
147 }
148 }
149 }
150
151 if let Some((wallet_address, key_address, key_authorization)) =
152 access_key_authorization.as_ref()
153 {
154 tx.prepare_access_key_authorization(
155 provider,
156 *wallet_address,
157 *key_address,
158 key_authorization.as_ref(),
159 )
160 .await?;
161 }
162
163 if !is_fixed_gas_limit && estimate_via_rpc {
166 estimate_gas(tx, provider, estimate_multiplier).await?;
167 }
168
169 if let Some(sponsor) = tempo_sponsor {
170 let from = tx.from().expect("no sender");
171 sponsor.attach_and_print::<N>(tx, from).await?;
172 }
173 }
174
175 Ok(())
176 }
177
178 pub async fn send(self, provider: Arc<RootProvider<N>>) -> Result<TxHash> {
185 match self {
186 Self::Unlocked(tx) => {
187 debug!("sending transaction from unlocked account {:?}", tx);
188
189 let pending = provider.send_transaction(tx).await?;
191 Ok(*pending.tx_hash())
192 }
193 Self::Raw(tx, signer) => {
194 debug!("sending transaction: {:?}", tx);
195 let signed = tx.build(signer).await?;
196
197 let pending = provider.send_raw_transaction(signed.encoded_2718().as_ref()).await?;
199 Ok(*pending.tx_hash())
200 }
201 Self::Signed(tx) => {
202 debug!("sending transaction: {:?}", tx);
203 let pending = provider.send_raw_transaction(tx.encoded_2718().as_ref()).await?;
204 Ok(*pending.tx_hash())
205 }
206 Self::Browser(tx, signer) => {
207 debug!("sending transaction: {:?}", tx);
208
209 Ok(signer.send_transaction_via_browser(tx).await?)
211 }
212 Self::AccessKey(tx, signer, access_key) => {
213 debug!("sending transaction via tempo access key: {:?}", tx);
214
215 let raw_tx = tx
216 .sign_with_access_key(
217 provider.as_ref(),
218 signer,
219 access_key.wallet_address,
220 access_key.key_address,
221 access_key.key_authorization.as_ref(),
222 )
223 .await?;
224
225 let pending = provider.send_raw_transaction(&raw_tx).await?;
226 Ok(*pending.tx_hash())
227 }
228 }
229 }
230
231 pub async fn prepare_and_send(
236 mut self,
237 provider: Arc<RootProvider<N>>,
238 sequential_broadcast: bool,
239 is_fixed_gas_limit: bool,
240 estimate_via_rpc: bool,
241 estimate_multiplier: u64,
242 tempo_sponsor: Option<&TempoSponsor>,
243 ) -> Result<TxHash> {
244 self.prepare(
245 &provider,
246 sequential_broadcast,
247 is_fixed_gas_limit,
248 estimate_via_rpc,
249 estimate_multiplier,
250 tempo_sponsor,
251 )
252 .await?;
253
254 self.send(provider).await
255 }
256}
257
258pub enum SendTransactionsKind<N: Network> {
260 Unlocked(AddressHashSet),
262 Raw {
264 eth_wallets: AddressHashMap<EthereumWallet>,
265 browser: Option<BrowserSigner<N>>,
266 access_keys: AddressHashMap<(WalletSigner, TempoAccessKeyConfig)>,
267 },
268}
269
270impl<N: Network> SendTransactionsKind<N> {
271 pub fn for_sender(
275 &self,
276 addr: &Address,
277 tx: N::TransactionRequest,
278 ) -> Result<SendTransactionKind<'_, N>> {
279 match self {
280 Self::Unlocked(unlocked) => {
281 if !unlocked.contains(addr) {
282 bail!("Sender address {:?} is not unlocked", addr)
283 }
284 Ok(SendTransactionKind::Unlocked(tx))
285 }
286 Self::Raw { eth_wallets, browser, access_keys } => {
287 if let Some((signer, config)) = access_keys.get(addr) {
288 Ok(SendTransactionKind::AccessKey(tx, signer, config))
289 } else if let Some(wallet) = eth_wallets.get(addr) {
290 Ok(SendTransactionKind::Raw(tx, wallet))
291 } else if let Some(b) = browser
292 && b.address() == *addr
293 {
294 Ok(SendTransactionKind::Browser(tx, b))
295 } else {
296 bail!("No matching signer for {:?} found", addr)
297 }
298 }
299 }
300 }
301}
302
303pub struct BundledState<FEN: FoundryEvmNetwork> {
307 pub args: ScriptArgs,
308 pub script_config: ScriptConfig<FEN>,
309 pub script_wallets: Wallets,
310 pub browser_wallet: Option<BrowserSigner<FEN::Network>>,
311 pub build_data: LinkedBuildData,
312 pub sequence: ScriptSequenceKind<FEN::Network>,
313}
314
315impl<FEN: FoundryEvmNetwork> BundledState<FEN> {
316 pub async fn wait_for_pending(mut self) -> Result<Self> {
317 let progress = ScriptProgress::default();
318 let progress_ref = &progress;
319 let futs = self
320 .sequence
321 .sequences_mut()
322 .iter_mut()
323 .enumerate()
324 .map(|(sequence_idx, sequence)| async move {
325 let rpc_url = sequence.rpc_url();
326 let provider = Arc::new(ProviderBuilder::new(rpc_url).build()?);
327 progress_ref
328 .wait_for_pending(
329 sequence_idx,
330 sequence,
331 &provider,
332 self.script_config.config.transaction_timeout,
333 )
334 .await
335 })
336 .collect::<Vec<_>>();
337
338 let errors = join_all(futs).await.into_iter().filter_map(Result::err).collect::<Vec<_>>();
339
340 self.sequence.save(true, false)?;
341
342 if !errors.is_empty() {
343 return Err(eyre::eyre!("{}", errors.iter().format("\n")));
344 }
345
346 Ok(self)
347 }
348
349 pub async fn broadcast(mut self) -> Result<BroadcastedState<FEN>> {
351 let required_addresses = self
352 .sequence
353 .sequences()
354 .iter()
355 .flat_map(|sequence| {
356 sequence
357 .transactions()
358 .filter(|tx| tx.is_unsigned())
359 .map(|tx| tx.from().expect("missing from"))
360 })
361 .collect::<AddressHashSet>();
362
363 if required_addresses.contains(&Config::DEFAULT_SENDER) {
364 eyre::bail!(
365 "You seem to be using Foundry's default sender. Be sure to set your own --sender."
366 );
367 }
368
369 let send_kind = if self.args.unlocked {
370 SendTransactionsKind::Unlocked(required_addresses.clone())
371 } else {
372 let signers: Vec<Address> = self
373 .script_wallets
374 .signers()
375 .map_err(|e| eyre::eyre!("{e}"))?
376 .into_iter()
377 .chain(self.browser_wallet.as_ref().map(|b| b.address()))
378 .collect();
379
380 let mut access_keys: AddressHashMap<(WalletSigner, TempoAccessKeyConfig)> =
382 AddressHashMap::default();
383 let mut direct_signers: AddressHashMap<WalletSigner> = AddressHashMap::default();
384 let mut missing_addresses = Vec::new();
385
386 for addr in &required_addresses {
387 if !signers.contains(addr) {
388 match lookup_signer(*addr) {
389 Ok(TempoLookup::Direct(signer)) => {
390 direct_signers.insert(*addr, signer);
391 }
392 Ok(TempoLookup::Keychain(signer, config)) => {
393 access_keys.insert(*addr, (signer, *config));
394 }
395 _ => {
396 missing_addresses.push(addr);
397 }
398 }
399 }
400 }
401
402 if !missing_addresses.is_empty() {
403 eyre::bail!(
404 "No associated wallet for addresses: {:?}. Unlocked wallets: {:?}",
405 missing_addresses,
406 signers
407 );
408 }
409
410 let signers = self.script_wallets.into_multi_wallet().into_signers()?;
411 let mut eth_wallets: AddressHashMap<EthereumWallet> =
412 signers.into_iter().map(|(addr, signer)| (addr, signer.into())).collect();
413 for (addr, signer) in direct_signers {
414 eth_wallets.insert(addr, signer.into());
415 }
416
417 SendTransactionsKind::Raw { eth_wallets, browser: self.browser_wallet, access_keys }
418 };
419
420 let tempo_sponsor = self.script_config.tempo.sponsor_config().await?.map(Arc::new);
421 if tempo_sponsor.is_some() && self.script_config.tempo.sponsor_sig.is_some() {
422 let remaining = self
423 .sequence
424 .sequences()
425 .iter()
426 .map(|sequence| {
427 sequence
428 .transactions()
429 .skip(sequence.receipts.len())
430 .filter(|tx| tx.is_unsigned())
431 .count()
432 })
433 .sum::<usize>();
434 if remaining > 1 {
435 eyre::bail!(
436 "--tempo.sponsor-sig can only sponsor one remaining script transaction; use --tempo.sponsor-signer for multi-transaction scripts"
437 );
438 }
439 }
440
441 let progress = ScriptProgress::default();
442
443 for i in 0..self.sequence.sequences().len() {
444 let mut sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
445
446 let provider = Arc::new(ProviderBuilder::new(sequence.rpc_url()).build()?);
447 let already_broadcasted = sequence.receipts.len();
448
449 let seq_progress = progress.get_sequence_progress(i, sequence);
450
451 if already_broadcasted < sequence.transactions.len() {
452 let is_legacy = Chain::from(sequence.chain).is_legacy() || self.args.legacy;
453 let (gas_price, eip1559_fees) = match (
455 is_legacy,
456 self.args.with_gas_price,
457 self.args.priority_gas_price,
458 ) {
459 (true, Some(gas_price), _) => (Some(gas_price.to()), None),
460 (true, None, _) => (Some(provider.get_gas_price().await?), None),
461 (false, Some(max_fee_per_gas), Some(max_priority_fee_per_gas)) => {
462 let max_fee: u128 = max_fee_per_gas.to();
463 let max_priority: u128 = max_priority_fee_per_gas.to();
464 if max_priority > max_fee {
465 eyre::bail!(
466 "--priority-gas-price ({max_priority}) cannot be higher than --with-gas-price ({max_fee})"
467 );
468 }
469 (
470 None,
471 Some(Eip1559Estimation {
472 max_fee_per_gas: max_fee,
473 max_priority_fee_per_gas: max_priority,
474 }),
475 )
476 }
477 (false, _, _) => {
478 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.")?;
479
480 if matches!(&send_kind, SendTransactionsKind::Raw { browser: Some(_), .. })
488 && let Ok(suggested_tip) = provider.get_max_priority_fee_per_gas().await
489 && suggested_tip > fees.max_priority_fee_per_gas
490 {
491 fees.max_fee_per_gas += suggested_tip - fees.max_priority_fee_per_gas;
492 fees.max_priority_fee_per_gas = suggested_tip;
493 }
494
495 if let Some(gas_price) = self.args.with_gas_price {
496 fees.max_fee_per_gas = gas_price.to();
497 }
498
499 if let Some(priority_gas_price) = self.args.priority_gas_price {
500 fees.max_priority_fee_per_gas = priority_gas_price.to();
501 }
502
503 (None, Some(fees))
504 }
505 };
506
507 let transactions = sequence
510 .transactions
511 .iter()
512 .skip(already_broadcasted)
513 .map(|tx_with_metadata| {
514 let is_fixed_gas_limit = tx_with_metadata.is_fixed_gas_limit;
515
516 let kind = match tx_with_metadata.tx().clone() {
517 TransactionMaybeSigned::Signed { tx, .. } => {
518 if tempo_sponsor.is_some() {
519 eyre::bail!(
520 "cannot attach Tempo sponsor signature to an already signed script transaction"
521 );
522 }
523 SendTransactionKind::Signed(tx)
524 }
525 TransactionMaybeSigned::Unsigned(mut tx) => {
526 let from = tx.from().expect("No sender for onchain transaction!");
527
528 tx.set_chain_id(sequence.chain);
529
530 if tx.kind().is_none() {
533 tx.set_create();
534 }
535
536 if let Some(gas_price) = gas_price {
537 tx.set_gas_price(gas_price);
538 } else {
539 let eip1559_fees = eip1559_fees.expect("was set above");
540 tx.set_max_priority_fee_per_gas(
541 eip1559_fees.max_priority_fee_per_gas,
542 );
543 tx.set_max_fee_per_gas(eip1559_fees.max_fee_per_gas);
544 }
545
546 self.script_config.tempo.apply::<FEN::Network>(&mut tx, None);
547
548 send_kind.for_sender(&from, tx)?
549 }
550 };
551
552 Ok((kind, is_fixed_gas_limit))
553 })
554 .collect::<Result<Vec<_>>>()?;
555
556 let estimate_via_rpc = has_different_gas_calc(sequence.chain)
557 || self.script_config.evm_opts.networks.is_tempo()
558 || self.args.skip_simulation;
559
560 let sequential_broadcast = estimate_via_rpc
566 || self.args.slow
567 || required_addresses.len() != 1
568 || !has_batch_support(sequence.chain);
569
570 let batch_size = if sequential_broadcast { 1 } else { 100 };
573 let mut index = already_broadcasted;
574
575 for (batch_number, batch) in transactions.chunks(batch_size).enumerate() {
576 seq_progress.inner.write().set_status(&format!(
577 "Sending transactions [{} - {}]",
578 batch_number * batch_size,
579 batch_number * batch_size + std::cmp::min(batch_size, batch.len()) - 1
580 ));
581
582 if !batch.is_empty() {
583 let pending_transactions =
584 batch.iter().map(|(kind, is_fixed_gas_limit)| {
585 let provider = provider.clone();
586 let tempo_sponsor = tempo_sponsor.clone();
587 async move {
588 let res = kind
589 .clone()
590 .prepare_and_send(
591 provider,
592 sequential_broadcast,
593 *is_fixed_gas_limit,
594 estimate_via_rpc,
595 self.args.gas_estimate_multiplier,
596 tempo_sponsor.as_deref(),
597 )
598 .await;
599 (res, kind, *is_fixed_gas_limit, 0, None)
600 }
601 .boxed()
602 });
603
604 let mut buffer = pending_transactions.collect::<FuturesUnordered<_>>();
605
606 'send: while let Some((
607 res,
608 kind,
609 is_fixed_gas_limit,
610 attempt,
611 original_res,
612 )) = buffer.next().await
613 {
614 if res.is_err()
615 && self.script_config.tempo.sponsor_sig.is_some()
616 && attempt == 0
617 {
618 debug!(
619 "not retrying transaction because --tempo.sponsor-sig is a static signature"
620 );
621 } else if res.is_err() && attempt <= 3 {
622 let provider = provider.clone();
624 let progress = seq_progress.inner.clone();
625 let tempo_sponsor = tempo_sponsor.clone();
626 buffer.push(Box::pin(async move {
627 debug!(err=?res, ?attempt, "retrying transaction ");
628 let attempt = attempt + 1;
629 progress.write().set_status(&format!(
630 "retrying transaction {res:?} (attempt {attempt})"
631 ));
632 tokio::time::sleep(Duration::from_millis(1000 * attempt)).await;
633 let r = kind
634 .clone()
635 .prepare_and_send(
636 provider,
637 sequential_broadcast,
638 is_fixed_gas_limit,
639 estimate_via_rpc,
640 self.args.gas_estimate_multiplier,
641 tempo_sponsor.as_deref(),
642 )
643 .await;
644 (
645 r,
646 kind,
647 is_fixed_gas_limit,
648 attempt,
649 original_res.or(Some(res)),
650 )
651 }));
652
653 continue 'send;
654 }
655
656 let tx_hash = res.wrap_err_with(|| {
658 if let Some(original_res) = original_res {
659 format!(
660 "Failed to send transaction after {attempt} attempts {original_res:?}"
661 )
662 } else {
663 "Failed to send transaction".to_string()
664 }
665 })?;
666 sequence.add_pending(index, tx_hash);
667
668 self.sequence.save(true, false)?;
670 sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
671
672 seq_progress.inner.write().tx_sent(tx_hash);
673 index += 1;
674 }
675
676 self.sequence.save(true, false)?;
678 sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
679
680 progress
681 .wait_for_pending(
682 i,
683 sequence,
684 &provider,
685 self.script_config.config.transaction_timeout,
686 )
687 .await?
688 }
689 self.sequence.save(true, false)?;
691 sequence = self.sequence.sequences_mut().get_mut(i).unwrap();
692 }
693 }
694
695 let (total_gas, total_gas_price, total_paid) =
696 sequence.receipts.iter().fold((0, 0, 0), |acc, receipt| {
697 let gas_used = receipt.gas_used();
698 let gas_price = receipt.effective_gas_price() as u64;
699 (acc.0 + gas_used, acc.1 + gas_price, acc.2 + gas_used * gas_price)
700 });
701 let paid = format_units(total_paid, 18).unwrap_or_else(|_| "N/A".to_string());
702 let avg_gas_price = total_gas_price
703 .checked_div(sequence.receipts.len() as u64)
704 .and_then(|avg| format_units(avg, 9).ok())
705 .unwrap_or_else(|| "N/A".to_string());
706
707 let token_symbol = NamedChain::try_from(sequence.chain)
708 .unwrap_or_default()
709 .native_currency_symbol()
710 .unwrap_or("ETH");
711 seq_progress.inner.write().set_status(&format!(
712 "Total Paid: {} {} ({} gas * avg {} gwei)\n",
713 paid.trim_end_matches('0'),
714 token_symbol,
715 total_gas,
716 avg_gas_price.trim_end_matches('0').trim_end_matches('.')
717 ));
718 seq_progress.inner.write().finish();
719 }
720
721 if !shell::is_json() {
722 sh_println!("\n\n==========================")?;
723 sh_println!("\nONCHAIN EXECUTION COMPLETE & SUCCESSFUL.")?;
724 }
725
726 Ok(BroadcastedState {
727 args: self.args,
728 script_config: self.script_config,
729 build_data: self.build_data,
730 sequence: self.sequence,
731 })
732 }
733
734 pub fn verify_preflight_check(&self) -> Result<()> {
735 for sequence in self.sequence.sequences() {
736 if self.args.verifier.verifier == VerificationProviderType::Etherscan
737 && self
738 .script_config
739 .config
740 .get_etherscan_api_key(Some(sequence.chain.into()))
741 .is_none()
742 {
743 eyre::bail!("Missing etherscan key for chain {}", sequence.chain);
744 }
745 }
746
747 Ok(())
748 }
749}
750
751impl BundledState<TempoEvmNetwork> {
752 pub async fn broadcast_batch(mut self) -> Result<BroadcastedState<TempoEvmNetwork>> {
757 if self.sequence.sequences().len() != 1 {
759 bail!(
760 "--batch mode only supports single-chain scripts. \
761 Use --multi without --batch for multi-chain."
762 );
763 }
764
765 let sequence = self.sequence.sequences_mut().get_mut(0).unwrap();
766 let provider = Arc::new(ProviderBuilder::<TempoNetwork>::new(sequence.rpc_url()).build()?);
767 let tempo_sponsor = self.script_config.tempo.sponsor_config().await?;
768
769 let senders: AddressHashSet = sequence
771 .transactions()
772 .filter(|tx| tx.is_unsigned())
773 .filter_map(|tx| tx.from())
774 .collect();
775
776 if senders.len() != 1 {
777 bail!(
778 "--batch mode requires all transactions to have the same sender. \
779 Found {} unique senders: {:?}",
780 senders.len(),
781 senders
782 );
783 }
784
785 let sender = *senders.iter().next().unwrap();
786
787 if sender == Config::DEFAULT_SENDER {
788 bail!(
789 "You seem to be using Foundry's default sender. Be sure to set your own --sender."
790 );
791 }
792
793 enum BatchSigner {
795 Unlocked,
796 Wallet(EthereumWallet),
797 TempoKeychain(Box<WalletSigner>, Box<TempoAccessKeyConfig>),
798 }
799
800 let batch_signer = if self.args.unlocked {
801 BatchSigner::Unlocked
802 } else {
803 let mut signers = self.script_wallets.into_multi_wallet().into_signers()?;
804 if let Some(signer) = signers.remove(&sender) {
805 BatchSigner::Wallet(EthereumWallet::new(signer))
806 } else {
807 match lookup_signer(sender)? {
809 TempoLookup::Direct(signer) => BatchSigner::Wallet(EthereumWallet::new(signer)),
810 TempoLookup::Keychain(signer, config) => {
811 BatchSigner::TempoKeychain(Box::new(signer), config)
812 }
813 TempoLookup::NotFound => {
814 bail!("No wallet found for sender {}", sender);
815 }
816 }
817 }
818 };
819
820 let mut calls: Vec<Call> = Vec::new();
823 let mut has_create = false;
824 for (idx, tx) in sequence.transactions().enumerate() {
825 let to = match tx.to() {
826 Some(addr) => TxKind::Call(addr),
827 None => {
828 if idx > 0 {
829 bail!(
830 "Contract creation must be the first transaction in --batch mode. \
831 Found CREATE at position {}. Reorder your script or deploy separately.",
832 idx + 1
833 );
834 }
835 if has_create {
836 bail!("Only one contract creation is allowed per --batch transaction.");
837 }
838 has_create = true;
839 TxKind::Create
840 }
841 };
842 let value = tx.value().unwrap_or(U256::ZERO);
843 let input = tx.input().cloned().unwrap_or_default();
844
845 calls.push(Call { to, value, input });
846 }
847
848 if calls.is_empty() {
849 sh_println!("No transactions to broadcast in batch mode.")?;
850 return 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 sh_println!(
859 "\n## Broadcasting batch transaction with {} call(s) to chain {}...",
860 calls.len(),
861 sequence.chain
862 )?;
863
864 let nonce = provider.get_transaction_count(sender).await?;
866 let chain_id = sequence.chain;
867
868 let fees = provider.estimate_eip1559_fees().await?;
870 let max_fee_per_gas =
871 self.args.with_gas_price.map(|p| p.to()).unwrap_or(fees.max_fee_per_gas);
872 let max_priority_fee_per_gas =
873 self.args.priority_gas_price.map(|p| p.to()).unwrap_or(fees.max_priority_fee_per_gas);
874
875 let mut batch_tx = TempoTransactionRequest {
876 inner: TransactionRequest {
877 from: Some(sender),
878 to: None,
879 value: None,
880 input: Default::default(),
881 nonce: Some(nonce),
882 chain_id: Some(chain_id),
883 max_fee_per_gas: Some(max_fee_per_gas),
884 max_priority_fee_per_gas: Some(max_priority_fee_per_gas),
885 ..Default::default()
886 },
887 fee_token: self.script_config.tempo.fee_token,
888 calls: calls.clone(),
889 nonce_key: self.script_config.tempo.expiring_nonce.then_some(U256::MAX),
890 valid_before: self.script_config.tempo.valid_before.and_then(NonZeroU64::new),
891 ..Default::default()
892 };
893 self.script_config.tempo.apply::<TempoNetwork>(&mut batch_tx, None);
894
895 if let BatchSigner::TempoKeychain(_, ak) = &batch_signer {
896 batch_tx.key_id = Some(ak.key_address);
897 batch_tx
898 .prepare_access_key_authorization(
899 provider.as_ref(),
900 ak.wallet_address,
901 ak.key_address,
902 ak.key_authorization.as_ref(),
903 )
904 .await?;
905 }
906
907 estimate_gas(&mut batch_tx, provider.as_ref(), self.args.gas_estimate_multiplier).await?;
909
910 sh_println!("Estimated gas: {}", batch_tx.inner.gas.unwrap_or(0))?;
911
912 if let Some(sponsor) = &tempo_sponsor {
913 sponsor.attach_and_print::<TempoNetwork>(&mut batch_tx, sender).await?;
914 }
915
916 let tx_hash = match batch_signer {
918 BatchSigner::Wallet(wallet) => {
919 let provider_with_wallet =
920 alloy_provider::ProviderBuilder::<_, _, TempoNetwork>::default()
921 .wallet(wallet)
922 .connect_provider(provider.as_ref());
923
924 let pending = provider_with_wallet.send_transaction(batch_tx).await?;
925 *pending.tx_hash()
926 }
927 BatchSigner::TempoKeychain(signer, access_key) => {
928 let raw_tx = batch_tx
929 .sign_with_access_key(
930 provider.as_ref(),
931 &*signer,
932 access_key.wallet_address,
933 access_key.key_address,
934 access_key.key_authorization.as_ref(),
935 )
936 .await?;
937
938 let pending = provider.send_raw_transaction(&raw_tx).await?;
939 *pending.tx_hash()
940 }
941 BatchSigner::Unlocked => {
942 let pending = provider.send_transaction(batch_tx).await?;
943 *pending.tx_hash()
944 }
945 };
946
947 sh_println!("Batch transaction sent: {:#x}", tx_hash)?;
948
949 let timeout = self.script_config.config.transaction_timeout;
951 let receipt = tokio::time::timeout(Duration::from_secs(timeout), async {
952 loop {
953 if let Some(receipt) = provider.get_transaction_receipt(tx_hash).await? {
954 return Ok::<_, eyre::Error>(receipt);
955 }
956 tokio::time::sleep(Duration::from_millis(500)).await;
957 }
958 })
959 .await
960 .map_err(|_| eyre::eyre!("Timeout waiting for batch transaction receipt"))??;
961
962 let success = receipt.status();
963 if success {
964 sh_println!(
965 "Batch transaction confirmed in block {}",
966 receipt.block_number.unwrap_or(0)
967 )?;
968 } else {
969 bail!("Batch transaction failed (reverted)");
970 }
971
972 let created_address = if has_create {
974 let deployed_addr = sender.create(nonce);
975 sh_println!("Contract deployed at: {:#x}", deployed_addr)?;
976 Some(deployed_addr)
977 } else {
978 None
979 };
980
981 for idx in 0..calls.len() {
986 let mut tx_receipt = receipt.clone();
987 if idx == 0 && has_create {
988 tx_receipt.contract_address = created_address;
989 } else {
990 tx_receipt.contract_address = None;
991 }
992 sequence.receipts.push(tx_receipt);
993 }
994
995 for i in 0..sequence.transactions.len() {
997 sequence.add_pending(i, tx_hash);
998 }
999
1000 let chain = sequence.chain;
1001 let _ = sequence;
1002
1003 self.sequence.save(true, false)?;
1004
1005 let total_gas = receipt.gas_used();
1006 let gas_price = receipt.effective_gas_price() as u64;
1007 let total_paid = total_gas * gas_price;
1008 let paid = format_units(total_paid, 18).unwrap_or_else(|_| "N/A".to_string());
1009 let gas_price_gwei = format_units(gas_price, 9).unwrap_or_else(|_| "N/A".to_string());
1010
1011 let token_symbol = NamedChain::try_from(chain)
1012 .unwrap_or_default()
1013 .native_currency_symbol()
1014 .unwrap_or("ETH");
1015 sh_println!(
1016 "\nTotal Paid: {} {} ({} gas * {} gwei)",
1017 paid.trim_end_matches('0'),
1018 token_symbol,
1019 total_gas,
1020 gas_price_gwei.trim_end_matches('0').trim_end_matches('.')
1021 )?;
1022
1023 if !shell::is_json() {
1024 sh_println!("\n\n==========================")?;
1025 sh_println!("\nBATCH EXECUTION COMPLETE & SUCCESSFUL.")?;
1026 sh_println!("All {} calls executed atomically in a single transaction.", calls.len())?;
1027 }
1028
1029 Ok(BroadcastedState {
1030 args: self.args,
1031 script_config: self.script_config,
1032 build_data: self.build_data,
1033 sequence: self.sequence,
1034 })
1035 }
1036}