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//! Uses a single type 0x76 transaction with multiple calls executed atomically.
5
6use crate::{
7    call_spec::CallSpec,
8    cmd::{
9        auth::{confirm_and_build, confirm_and_build_with_tempo_wallet},
10        send::{SendOptions, cast_send, cast_send_with_tempo_wallet},
11    },
12    tempo,
13    tx::{self, CastTxBuilder, InitState, InputState, SendTxOpts, apply_poll_interval},
14};
15use alloy_network::EthereumWallet;
16use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder};
17use clap::Parser;
18use eyre::Result;
19use foundry_cli::{
20    opts::TransactionOpts,
21    utils::{self, resolve_lane},
22};
23use tempo_alloy::TempoNetwork;
24
25/// CLI arguments for `cast batch-send`.
26///
27/// Sends multiple calls as a single atomic Tempo transaction.
28#[derive(Debug, Parser)]
29pub struct BatchSendArgs {
30    /// Call specifications in format: `to[:<value>][:<sig>[:<args>]]` or `to[:<value>][:<0xdata>]`
31    ///
32    /// Examples:
33    ///   --call "0x1234567890123456789012345678901234567890:0.1ether" (ETH transfer)
34    ///   --call "0x1234567890123456789012345678901234567890::transfer(address,uint256):
35    /// 0x0987654321098765432109876543210987654321,1000" (ERC20 transfer)
36    ///   --call "0x1234567890123456789012345678901234567890::0x123def" (raw calldata)
37    ///   --call "0x1234567890123456789012345678901234567890:1ether:deposit()" (value + function
38    /// 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    /// Skip the EIP-7702 authorization disclosure confirmation.
49    #[arg(long)]
50    pub force: bool,
51
52    /// Send via `eth_sendTransaction` using the `--from` argument or $ETH_FROM as sender
53    #[arg(long, requires = "from")]
54    pub unlocked: bool,
55}
56
57impl BatchSendArgs {
58    pub async fn run(self) -> Result<()> {
59        let Self { calls, send_tx, mut tx, force, unlocked } = self;
60        // Tempo sessions must sign with the session key; these modes route signing through a
61        // node-managed account or browser wallet instead.
62        if tx.tempo.session_id()?.is_some() && unlocked {
63            eyre::bail!("--tempo.session/TEMPO_SESSION_ID cannot be combined with --unlocked");
64        }
65        tempo::ensure_session_not_browser(&tx.tempo, send_tx.browser.browser)?;
66
67        let expires_at = tx.tempo.resolve_expires();
68
69        let (config, provider) = tempo::tempo_provider(&send_tx.eth)?;
70        let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?;
71        let lane = resolved_lane.as_ref();
72
73        apply_poll_interval(&provider, send_tx.poll_interval);
74
75        let chain = utils::get_chain(config.chain, &provider).await?;
76        let (signer, tempo_access_key) =
77            tempo::resolve_session_or_wallet_signer(&tx.tempo, &send_tx.eth.wallet, chain.id())
78                .await?;
79
80        // Preserve key_id for modes that do not call build_with_tempo_wallet, such as unlocked.
81        if let Some(access_key) = &tempo_access_key {
82            tx.tempo.key_id = Some(access_key.key_id()?);
83        }
84
85        let builder = CastTxBuilder::<TempoNetwork, _, _>::new(&provider, tx, &config).await?;
86        let builder = with_batch_calls(&calls, builder, &provider).await?;
87        tempo::print_expires(expires_at)?;
88
89        let send_opts = SendOptions::new(&send_tx, &config).resolving_fee_token(true, &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}