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::{
10        auth::{confirm_and_build, confirm_and_build_with_tempo_wallet},
11        send::{SendOptions, cast_send, cast_send_with_tempo_wallet},
12    },
13    tempo,
14    tx::{self, CastTxBuilder, InitState, InputState, SendTxOpts, apply_poll_interval},
15};
16use alloy_network::EthereumWallet;
17use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder};
18use clap::Parser;
19use eyre::Result;
20use foundry_cli::{
21    opts::TransactionOpts,
22    utils::{self, resolve_lane},
23};
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    /// Skip the EIP-7702 authorization disclosure confirmation.
48    #[arg(long)]
49    pub force: bool,
50
51    /// Send via `eth_sendTransaction` using the `--from` argument or $ETH_FROM as sender
52    #[arg(long, requires = "from")]
53    pub unlocked: bool,
54}
55
56impl BatchSendArgs {
57    pub async fn run(self) -> Result<()> {
58        let Self { calls, send_tx, mut tx, force, unlocked } = self;
59        // Tempo sessions must sign with the session key; these modes route signing through a
60        // node-managed account or browser wallet instead.
61        if tx.tempo.session_id()?.is_some() && unlocked {
62            eyre::bail!("--tempo.session/TEMPO_SESSION_ID cannot be combined with --unlocked");
63        }
64        tempo::ensure_session_not_browser(&tx.tempo, send_tx.browser.browser)?;
65
66        let expires_at = tx.tempo.resolve_expires();
67
68        let (config, provider) = tempo::tempo_provider(&send_tx.eth)?;
69        let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?;
70        let lane = resolved_lane.as_ref();
71
72        apply_poll_interval(&provider, send_tx.poll_interval);
73
74        let chain = utils::get_chain(config.chain, &provider).await?;
75        let (signer, tempo_access_key) =
76            tempo::resolve_session_or_wallet_signer(&tx.tempo, &send_tx.eth.wallet, chain.id())
77                .await?;
78
79        // Preserve key_id for modes that do not call build_with_tempo_wallet, such as unlocked.
80        if let Some(access_key) = &tempo_access_key {
81            tx.tempo.key_id = Some(access_key.key_id()?);
82        }
83
84        let builder = CastTxBuilder::<TempoNetwork, _, _>::new(&provider, tx, &config).await?;
85        let builder = with_batch_calls(&calls, builder, &provider).await?;
86        tempo::print_expires(expires_at)?;
87
88        let send_opts =
89            SendOptions::new(&send_tx, &config).resolving_fee_token(Some(chain), &config);
90
91        if unlocked {
92            let Some(tx) = confirm_and_build(builder, config.sender, force, lane, false).await?
93            else {
94                return Ok(());
95            };
96            cast_send(provider, tx, &send_opts).await?;
97        } else if let Some(access_key) = &tempo_access_key {
98            let Some((tx_request, prepared)) =
99                confirm_and_build_with_tempo_wallet(builder, access_key, force, lane).await?
100            else {
101                return Ok(());
102            };
103            cast_send_with_tempo_wallet(&provider, tx_request, &prepared, &send_opts).await?;
104        } else {
105            let (signer, _) = tx::resolve_send_signer(signer, &send_tx.eth).await?;
106            let Some(tx_request) = confirm_and_build(builder, &signer, force, lane, false).await?
107            else {
108                return Ok(());
109            };
110            let provider = AlloyProviderBuilder::<_, _, TempoNetwork>::default()
111                .wallet(EthereumWallet::from(signer))
112                .connect_provider(&provider);
113            cast_send(provider, tx_request, &send_opts).await?;
114        }
115
116        Ok(())
117    }
118}
119
120/// Parses the `--call` specs, resolves them against the builder's chain and sets them as the
121/// batch calls of the transaction.
122pub(super) async fn with_batch_calls<P: Provider<TempoNetwork>>(
123    calls: &[String],
124    mut builder: CastTxBuilder<TempoNetwork, P, InitState>,
125    provider: &impl Provider<TempoNetwork>,
126) -> Result<CastTxBuilder<TempoNetwork, P, InputState>> {
127    let specs = calls.iter().map(|s| CallSpec::parse(s)).collect::<Result<Vec<_>>>()?;
128    let (etherscan_api_key, etherscan_api_url) = builder.etherscan_api();
129    let mut tempo_calls = Vec::with_capacity(specs.len());
130    for (i, spec) in specs.iter().enumerate() {
131        tempo_calls.push(
132            spec.resolve(i, builder.chain(), provider, etherscan_api_key, etherscan_api_url)
133                .await?,
134        );
135    }
136    sh_status!("Building batch transaction with {} call(s)...", tempo_calls.len())?;
137    builder.tx_mut().calls = tempo_calls;
138
139    // The builder requires a `to`; `build_aa` uses the calls instead, so point it at the first
140    // call's target.
141    builder
142        .with_to(specs.first().map(|spec| spec.to.into()))
143        .await?
144        .with_code_sig_and_args(None, None, vec![])
145        .await
146}