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, cast_send_with_access_key},
10    tempo,
11    tx::{self, CastTxBuilder, SendTxOpts},
12};
13use alloy_network::{EthereumWallet, TransactionBuilder};
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, maybe_print_resolved_lane, resolve_lane},
21};
22use foundry_common::provider::ProviderBuilder;
23use std::time::Duration;
24use tempo_alloy::TempoNetwork;
25
26/// CLI arguments for `cast batch-send`.
27///
28/// Sends multiple calls as a single atomic Tempo transaction.
29#[derive(Debug, Parser)]
30pub struct BatchSendArgs {
31    /// Call specifications in format: `to[:<value>][:<sig>[:<args>]]` or `to[:<value>][:<0xdata>]`
32    ///
33    /// Examples:
34    ///   --call "0x123:0.1ether" (ETH transfer)
35    ///   --call "0x456::transfer(address,uint256):0x789,1000" (ERC20 transfer)
36    ///   --call "0xabc::0x123def" (raw calldata)
37    ///   --call "0x123:1ether:deposit()" (value + function call)
38    #[arg(long = "call", value_name = "SPEC", required = true)]
39    pub calls: Vec<String>,
40
41    #[command(flatten)]
42    pub send_tx: SendTxOpts,
43
44    #[command(flatten)]
45    pub tx: TransactionOpts,
46
47    /// Send via `eth_sendTransaction` using the `--from` argument or $ETH_FROM as sender
48    #[arg(long, requires = "from")]
49    pub unlocked: bool,
50}
51
52impl BatchSendArgs {
53    pub async fn run(self) -> Result<()> {
54        let Self { calls, send_tx, mut tx, unlocked } = self;
55        let expires_at = tx.tempo.resolve_expires();
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        // Resolve `--tempo.lane <name>` against the lanes file (default
65        // `<root>/tempo.lanes.toml`) and populate `tx.tempo.nonce_key` from the lane.
66        let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?;
67
68        if let Some(interval) = send_tx.poll_interval {
69            provider.client().set_poll_interval(Duration::from_secs(interval))
70        }
71
72        // Resolve signer to detect keychain mode
73        let (signer, tempo_access_key) = send_tx.eth.wallet.maybe_signer().await?;
74
75        // Parse all call specs
76        let call_specs: Vec<CallSpec> =
77            calls.iter().map(|s| CallSpec::parse(s)).collect::<Result<Vec<_>>>()?;
78
79        // Get chain for parsing function args
80        let chain = utils::get_chain(config.chain, &provider).await?;
81        let etherscan_config = config.get_etherscan_config_with_chain(Some(chain)).ok().flatten();
82        let etherscan_api_key = etherscan_config.as_ref().map(|c| c.key.clone());
83        let etherscan_api_url = etherscan_config.map(|c| c.api_url);
84
85        // Build Vec<Call> from specs
86        let mut tempo_calls = Vec::with_capacity(call_specs.len());
87        for (i, spec) in call_specs.iter().enumerate() {
88            tempo_calls.push(
89                spec.resolve(
90                    i,
91                    chain,
92                    &provider,
93                    etherscan_api_key.as_deref(),
94                    etherscan_api_url.as_deref(),
95                )
96                .await?,
97            );
98        }
99
100        sh_println!("Building batch transaction with {} call(s)...", tempo_calls.len())?;
101        tempo::print_expires(expires_at)?;
102
103        // Preserve key_id for modes that do not call build_with_access_key, such as unlocked.
104        if let Some(ref access_key) = tempo_access_key {
105            tx.tempo.key_id = Some(access_key.key_address);
106        }
107
108        // Build transaction request with calls
109        let mut builder = CastTxBuilder::<TempoNetwork, _, _>::new(&provider, tx, &config).await?;
110
111        // Access the inner tx and set calls
112        builder.tx.calls = tempo_calls;
113
114        // We need to set a dummy "to" to satisfy the state machine, but the calls field
115        // will be used by build_aa. Set to first call's target.
116        let first_call_to = call_specs.first().map(|s| s.to);
117        let builder = builder.with_to(first_call_to.map(Into::into)).await?;
118
119        // Use empty sig/args since we're using calls directly
120        let builder = builder.with_code_sig_and_args(None, None, vec![]).await?;
121
122        let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout);
123
124        if unlocked {
125            let (tx, _) = builder.build(config.sender).await?;
126            maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
127            cast_send(
128                provider,
129                tx,
130                send_tx.cast_async,
131                send_tx.sync,
132                send_tx.confirmations,
133                timeout,
134            )
135            .await
136        } else {
137            let signer = match signer {
138                Some(s) => s,
139                None => send_tx.eth.wallet.signer().await?,
140            };
141
142            if let Some(ref access_key) = tempo_access_key {
143                let (tx_request, _) =
144                    builder.build_with_access_key(access_key.wallet_address, access_key).await?;
145                maybe_print_resolved_lane(
146                    resolved_lane.as_ref(),
147                    tx_request.nonce().unwrap_or_default(),
148                )?;
149                cast_send_with_access_key(
150                    &provider,
151                    tx_request,
152                    &signer,
153                    access_key,
154                    send_tx.cast_async,
155                    send_tx.confirmations,
156                    timeout,
157                )
158                .await?;
159            } else {
160                tx::validate_from_address(send_tx.eth.wallet.from, Signer::address(&signer))?;
161                let (tx_request, _) = builder.build(&signer).await?;
162                maybe_print_resolved_lane(
163                    resolved_lane.as_ref(),
164                    tx_request.nonce().unwrap_or_default(),
165                )?;
166                let wallet = EthereumWallet::from(signer);
167                let provider = AlloyProviderBuilder::<_, _, TempoNetwork>::default()
168                    .wallet(wallet)
169                    .connect_provider(&provider);
170
171                cast_send(
172                    provider,
173                    tx_request,
174                    send_tx.cast_async,
175                    send_tx.sync,
176                    send_tx.confirmations,
177                    timeout,
178                )
179                .await?;
180            }
181
182            Ok(())
183        }
184    }
185}