Skip to main content

cast/cmd/
batch_send.rs

1//! `cast batch-send` command implementation.
2//!
3//! Sends a batch of calls as a single Tempo transaction using native call batching.
4//! Unlike upstream Foundry's sequential transactions, this uses a single type 0x76
5//! transaction with multiple calls executed atomically.
6
7use crate::{
8    call_spec::CallSpec,
9    cmd::send::cast_send,
10    tx::{self, CastTxBuilder, CastTxSender, SendTxOpts},
11};
12use alloy_network::EthereumWallet;
13use alloy_primitives::Bytes;
14use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder};
15use alloy_signer::Signer;
16use clap::Parser;
17use eyre::{Result, eyre};
18use foundry_cli::{
19    opts::TransactionOpts,
20    utils::{self, LoadConfig, parse_function_args},
21};
22use foundry_common::{FoundryTransactionBuilder, provider::ProviderBuilder};
23use std::time::Duration;
24use tempo_alloy::TempoNetwork;
25use tempo_primitives::transaction::Call;
26
27/// CLI arguments for `cast batch-send`.
28///
29/// Sends multiple calls as a single atomic Tempo transaction.
30#[derive(Debug, Parser)]
31pub struct BatchSendArgs {
32    /// Call specifications in format: `to[:<value>][:<sig>[:<args>]]` or `to[:<value>][:<0xdata>]`
33    ///
34    /// Examples:
35    ///   --call "0x123:0.1ether" (ETH transfer)
36    ///   --call "0x456::transfer(address,uint256):0x789,1000" (ERC20 transfer)
37    ///   --call "0xabc::0x123def" (raw calldata)
38    ///   --call "0x123:1ether:deposit()" (value + function call)
39    #[arg(long = "call", value_name = "SPEC", required = true)]
40    pub calls: Vec<String>,
41
42    #[command(flatten)]
43    pub send_tx: SendTxOpts,
44
45    #[command(flatten)]
46    pub tx: TransactionOpts,
47
48    /// Send via `eth_sendTransaction` using the `--from` argument or $ETH_FROM as sender
49    #[arg(long, requires = "from")]
50    pub unlocked: bool,
51}
52
53impl BatchSendArgs {
54    pub async fn run(self) -> Result<()> {
55        let Self { calls, send_tx, tx, unlocked } = self;
56
57        if calls.is_empty() {
58            return Err(eyre!("No calls specified. Use --call to specify at least one call."));
59        }
60
61        let config = send_tx.eth.load_config()?;
62        let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
63
64        if let Some(interval) = send_tx.poll_interval {
65            provider.client().set_poll_interval(Duration::from_secs(interval))
66        }
67
68        // Resolve signer to detect keychain mode
69        let (signer, tempo_access_key) = send_tx.eth.wallet.maybe_signer().await?;
70
71        // Parse all call specs
72        let call_specs: Vec<CallSpec> =
73            calls.iter().map(|s| CallSpec::parse(s)).collect::<Result<Vec<_>>>()?;
74
75        // Get chain for parsing function args
76        let chain = utils::get_chain(config.chain, &provider).await?;
77        let etherscan_api_key = config.get_etherscan_api_key(Some(chain));
78
79        // Build Vec<Call> from specs
80        let mut tempo_calls = Vec::with_capacity(call_specs.len());
81        for (i, spec) in call_specs.iter().enumerate() {
82            let input = if let Some(data) = &spec.data {
83                data.clone()
84            } else if let Some(sig) = &spec.sig {
85                let (encoded, _) = parse_function_args(
86                    sig,
87                    spec.args.clone(),
88                    Some(spec.to),
89                    chain,
90                    &provider,
91                    etherscan_api_key.as_deref(),
92                )
93                .await
94                .map_err(|e| eyre!("Failed to encode call {}: {}", i + 1, e))?;
95                Bytes::from(encoded)
96            } else {
97                Bytes::new()
98            };
99
100            tempo_calls.push(Call { to: spec.to.into(), value: spec.value, input });
101        }
102
103        sh_println!("Building batch transaction with {} call(s)...", tempo_calls.len())?;
104
105        // Build transaction request with calls
106        let mut builder = CastTxBuilder::<TempoNetwork, _, _>::new(&provider, tx, &config).await?;
107
108        // Set key_id for access key transactions
109        if let Some(ref access_key) = tempo_access_key {
110            builder.tx.set_key_id(access_key.key_address);
111        }
112
113        // Access the inner tx and set calls
114        builder.tx.calls = tempo_calls;
115
116        // We need to set a dummy "to" to satisfy the state machine, but the calls field
117        // will be used by build_aa. Set to first call's target.
118        let first_call_to = call_specs.first().map(|s| s.to);
119        let builder = builder.with_to(first_call_to.map(Into::into)).await?;
120
121        // Use empty sig/args since we're using calls directly
122        let builder = builder.with_code_sig_and_args(None, None, vec![]).await?;
123
124        let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout);
125
126        if unlocked {
127            let (tx, _) = builder.build(config.sender).await?;
128            cast_send(
129                provider,
130                tx,
131                send_tx.cast_async,
132                send_tx.sync,
133                send_tx.confirmations,
134                timeout,
135            )
136            .await
137        } else {
138            let signer = match signer {
139                Some(s) => s,
140                None => send_tx.eth.wallet.signer().await?,
141            };
142            let from = if let Some(ref access_key) = tempo_access_key {
143                access_key.wallet_address
144            } else {
145                Signer::address(&signer)
146            };
147
148            if tempo_access_key.is_none() {
149                tx::validate_from_address(send_tx.eth.wallet.from, from)?;
150            }
151
152            let (tx_request, _) = if tempo_access_key.is_some() {
153                builder.build(from).await?
154            } else {
155                builder.build(&signer).await?
156            };
157
158            if let Some(ref access_key) = tempo_access_key {
159                let raw_tx = tx_request
160                    .sign_with_access_key(
161                        &provider,
162                        &signer,
163                        access_key.wallet_address,
164                        access_key.key_address,
165                        access_key.key_authorization.as_ref(),
166                    )
167                    .await?;
168
169                let cast = CastTxSender::new(&provider);
170                let tx_hash = *provider.send_raw_transaction(&raw_tx).await?.tx_hash();
171                cast.print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout)
172                    .await?;
173            } else {
174                let wallet = EthereumWallet::from(signer);
175                let provider = AlloyProviderBuilder::<_, _, TempoNetwork>::default()
176                    .wallet(wallet)
177                    .connect_provider(&provider);
178
179                cast_send(
180                    provider,
181                    tx_request,
182                    send_tx.cast_async,
183                    send_tx.sync,
184                    send_tx.confirmations,
185                    timeout,
186                )
187                .await?;
188            }
189
190            Ok(())
191        }
192    }
193}