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