@@ -9,11 +9,12 @@ use alloy_consensus::{SignableTransaction, Signed};
99use alloy_eips:: { BlockId , eip2718:: Encodable2718 } ;
1010use alloy_network:: { EthereumWallet , Network , ReceiptResponse , TransactionBuilder } ;
1111use alloy_primitives:: {
12- Address , TxHash ,
12+ Address , TxHash , TxKind , U256 ,
1313 map:: { AddressHashMap , AddressHashSet } ,
1414 utils:: format_units,
1515} ;
1616use alloy_provider:: { Provider , RootProvider , utils:: Eip1559Estimation } ;
17+ use alloy_rpc_types:: TransactionRequest ;
1718use alloy_signer:: Signature ;
1819use eyre:: { Context , Result , bail} ;
1920use forge_verify:: provider:: VerificationProviderType ;
@@ -25,10 +26,16 @@ use foundry_common::{
2526 shell,
2627} ;
2728use foundry_config:: Config ;
28- use foundry_evm:: core:: evm:: FoundryEvmNetwork ;
29- use foundry_wallets:: { TempoAccessKeyConfig , WalletSigner , wallet_browser:: signer:: BrowserSigner } ;
29+ use foundry_evm:: core:: evm:: { FoundryEvmNetwork , TempoEvmNetwork } ;
30+ use foundry_wallets:: {
31+ TempoAccessKeyConfig , WalletSigner ,
32+ tempo:: { TempoLookup , lookup_signer} ,
33+ wallet_browser:: signer:: BrowserSigner ,
34+ } ;
3035use futures:: { FutureExt , StreamExt , future:: join_all, stream:: FuturesUnordered } ;
3136use itertools:: Itertools ;
37+ use tempo_alloy:: { TempoNetwork , rpc:: TempoTransactionRequest } ;
38+ use tempo_primitives:: transaction:: Call ;
3239
3340pub async fn estimate_gas < N : Network , P : Provider < N > > (
3441 tx : & mut N :: TransactionRequest ,
@@ -345,8 +352,8 @@ impl<FEN: FoundryEvmNetwork> BundledState<FEN> {
345352
346353 for addr in & required_addresses {
347354 if !signers. contains ( addr) {
348- match foundry_wallets :: tempo :: lookup_signer ( * addr) {
349- Ok ( foundry_wallets :: tempo :: TempoLookup :: Keychain ( signer, config) ) => {
355+ match lookup_signer ( * addr) {
356+ Ok ( TempoLookup :: Keychain ( signer, config) ) => {
350357 access_keys. insert ( * addr, ( signer, * config) ) ;
351358 }
352359 _ => {
@@ -492,8 +499,9 @@ impl<FEN: FoundryEvmNetwork> BundledState<FEN> {
492499 || required_addresses. len ( ) != 1
493500 || !has_batch_support ( sequence. chain ) ;
494501
495- // We send transactions and wait for receipts in batches.
496- let batch_size = if sequential_broadcast { 1 } else { self . args . batch_size } ;
502+ // We send transactions and wait for receipts in batches of 100, since some networks
503+ // cannot handle more than that.
504+ let batch_size = if sequential_broadcast { 1 } else { 100 } ;
497505 let mut index = already_broadcasted;
498506
499507 for ( batch_number, batch) in transactions. chunks ( batch_size) . enumerate ( ) {
@@ -640,3 +648,279 @@ impl<FEN: FoundryEvmNetwork> BundledState<FEN> {
640648 Ok ( ( ) )
641649 }
642650}
651+
652+ impl BundledState < TempoEvmNetwork > {
653+ /// Broadcasts all transactions as a single Tempo batch transaction (type 0x76).
654+ ///
655+ /// This method collects all individual transactions from the script and combines them
656+ /// into a single batch transaction for atomic execution on Tempo.
657+ pub async fn broadcast_batch ( mut self ) -> Result < BroadcastedState < TempoEvmNetwork > > {
658+ // Batch mode only supports single chain for now
659+ if self . sequence . sequences ( ) . len ( ) != 1 {
660+ bail ! (
661+ "--batch mode only supports single-chain scripts. \
662+ Use --multi without --batch for multi-chain."
663+ ) ;
664+ }
665+
666+ let sequence = self . sequence . sequences_mut ( ) . get_mut ( 0 ) . unwrap ( ) ;
667+ let provider = Arc :: new ( ProviderBuilder :: < TempoNetwork > :: new ( sequence. rpc_url ( ) ) . build ( ) ?) ;
668+
669+ // Collect sender addresses - batch mode requires single sender
670+ let senders: AddressHashSet = sequence
671+ . transactions ( )
672+ . filter ( |tx| tx. is_unsigned ( ) )
673+ . filter_map ( |tx| tx. from ( ) )
674+ . collect ( ) ;
675+
676+ if senders. len ( ) != 1 {
677+ bail ! (
678+ "--batch mode requires all transactions to have the same sender. \
679+ Found {} unique senders: {:?}",
680+ senders. len( ) ,
681+ senders
682+ ) ;
683+ }
684+
685+ let sender = * senders. iter ( ) . next ( ) . unwrap ( ) ;
686+
687+ if sender == Config :: DEFAULT_SENDER {
688+ bail ! (
689+ "You seem to be using Foundry's default sender. Be sure to set your own --sender."
690+ ) ;
691+ }
692+
693+ // Get wallet for signing
694+ enum BatchSigner {
695+ Unlocked ,
696+ Wallet ( EthereumWallet ) ,
697+ TempoKeychain ( Box < WalletSigner > , Box < TempoAccessKeyConfig > ) ,
698+ }
699+
700+ let batch_signer = if self . args . unlocked {
701+ BatchSigner :: Unlocked
702+ } else {
703+ let mut signers = self . script_wallets . into_multi_wallet ( ) . into_signers ( ) ?;
704+ if let Some ( signer) = signers. remove ( & sender) {
705+ BatchSigner :: Wallet ( EthereumWallet :: new ( signer) )
706+ } else {
707+ // Try Tempo keys.toml fallback
708+ match lookup_signer ( sender) ? {
709+ TempoLookup :: Direct ( signer) => BatchSigner :: Wallet ( EthereumWallet :: new ( signer) ) ,
710+ TempoLookup :: Keychain ( signer, config) => {
711+ BatchSigner :: TempoKeychain ( Box :: new ( signer) , config)
712+ }
713+ TempoLookup :: NotFound => {
714+ bail ! ( "No wallet found for sender {}" , sender) ;
715+ }
716+ }
717+ }
718+ } ;
719+
720+ // Collect all transactions into Call structs
721+ // Tempo batch transactions support CREATE only as the first call
722+ let mut calls: Vec < Call > = Vec :: new ( ) ;
723+ let mut has_create = false ;
724+ for ( idx, tx) in sequence. transactions ( ) . enumerate ( ) {
725+ let to = match tx. to ( ) {
726+ Some ( addr) => TxKind :: Call ( addr) ,
727+ None => {
728+ if idx > 0 {
729+ bail ! (
730+ "Contract creation must be the first transaction in --batch mode. \
731+ Found CREATE at position {}. Reorder your script or deploy separately.",
732+ idx + 1
733+ ) ;
734+ }
735+ if has_create {
736+ bail ! ( "Only one contract creation is allowed per --batch transaction." ) ;
737+ }
738+ has_create = true ;
739+ TxKind :: Create
740+ }
741+ } ;
742+ let value = tx. value ( ) . unwrap_or ( U256 :: ZERO ) ;
743+ let input = tx. input ( ) . cloned ( ) . unwrap_or_default ( ) ;
744+
745+ calls. push ( Call { to, value, input } ) ;
746+ }
747+
748+ if calls. is_empty ( ) {
749+ sh_println ! ( "No transactions to broadcast in batch mode." ) ?;
750+ return Ok ( BroadcastedState {
751+ args : self . args ,
752+ script_config : self . script_config ,
753+ build_data : self . build_data ,
754+ sequence : self . sequence ,
755+ } ) ;
756+ }
757+
758+ sh_println ! (
759+ "\n ## Broadcasting batch transaction with {} call(s) to chain {}..." ,
760+ calls. len( ) ,
761+ sequence. chain
762+ ) ?;
763+
764+ // Build the batch transaction request
765+ let nonce = provider. get_transaction_count ( sender) . await ?;
766+ let chain_id = sequence. chain ;
767+
768+ // Get gas prices - batch transactions are Tempo-only, always use EIP-1559 style fees
769+ let fees = provider. estimate_eip1559_fees ( ) . await ?;
770+ let max_fee_per_gas =
771+ self . args . with_gas_price . map ( |p| p. to ( ) ) . unwrap_or ( fees. max_fee_per_gas ) ;
772+ let max_priority_fee_per_gas =
773+ self . args . priority_gas_price . map ( |p| p. to ( ) ) . unwrap_or ( fees. max_priority_fee_per_gas ) ;
774+
775+ let mut batch_tx = TempoTransactionRequest {
776+ inner : TransactionRequest {
777+ from : Some ( sender) ,
778+ to : None ,
779+ value : None ,
780+ input : Default :: default ( ) ,
781+ nonce : Some ( nonce) ,
782+ chain_id : Some ( chain_id) ,
783+ max_fee_per_gas : Some ( max_fee_per_gas) ,
784+ max_priority_fee_per_gas : Some ( max_priority_fee_per_gas) ,
785+ ..Default :: default ( )
786+ } ,
787+ calls : calls. clone ( ) ,
788+ ..Default :: default ( )
789+ } ;
790+
791+ // Estimate gas for the batch transaction
792+ estimate_gas ( & mut batch_tx, provider. as_ref ( ) , self . args . gas_estimate_multiplier ) . await ?;
793+
794+ sh_println ! ( "Estimated gas: {}" , batch_tx. inner. gas. unwrap_or( 0 ) ) ?;
795+
796+ // Sign and send
797+ let tx_hash = match batch_signer {
798+ BatchSigner :: Wallet ( wallet) => {
799+ let provider_with_wallet =
800+ alloy_provider:: ProviderBuilder :: < _ , _ , TempoNetwork > :: default ( )
801+ . wallet ( wallet)
802+ . connect_provider ( provider. as_ref ( ) ) ;
803+
804+ let pending = provider_with_wallet. send_transaction ( batch_tx) . await ?;
805+ * pending. tx_hash ( )
806+ }
807+ BatchSigner :: TempoKeychain ( signer, access_key) => {
808+ batch_tx. key_id = Some ( access_key. key_address ) ;
809+
810+ if let Some ( ref auth) = access_key. key_authorization {
811+ batch_tx. key_authorization = Some ( auth. clone ( ) ) ;
812+ }
813+
814+ // Strip key_authorization if the key is already provisioned (saves gas)
815+ if batch_tx. key_authorization . is_some ( ) {
816+ use tempo_alloy:: provider:: TempoProviderExt ;
817+ let key_info = provider
818+ . get_keychain_key ( access_key. wallet_address , access_key. key_address )
819+ . await ;
820+ if key_info. map ( |info| info. keyId != Address :: ZERO ) . unwrap_or ( false ) {
821+ batch_tx. key_authorization = None ;
822+ }
823+ }
824+
825+ let raw_tx =
826+ batch_tx. sign_with_access_key ( & * signer, access_key. wallet_address ) . 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+ // Wait for receipt
840+ 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+ // For CREATE transactions, compute the deployed contract address
863+ 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+ // Add receipt to sequence for each original transaction.
872+ // In batch mode, all calls share the same receipt. Set contract_address
873+ // only for index 0 if CREATE, clear for the rest to prevent the verifier
874+ // from attempting to verify the same address multiple times.
875+ 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+ // Mark all transactions as pending with the batch tx hash
886+ 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+ "\n Total 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 ! ( "\n BATCH 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+ }
0 commit comments