Skip to main content

cast/cmd/
batch_mktx.rs

1//! `cast batch-mktx` command implementation.
2//!
3//! Creates a signed or unsigned batch transaction using Tempo's native call batching.
4//! Outputs the RLP-encoded transaction hex.
5
6use crate::{
7    cmd::{
8        auth::{confirm_and_build, confirm_and_build_with_tempo_wallet},
9        batch_send::with_batch_calls,
10    },
11    tempo,
12    tx::{self, CastTxBuilder},
13};
14use alloy_consensus::SignableTransaction;
15use alloy_eips::eip2718::Encodable2718;
16use alloy_network::{EthereumWallet, NetworkTransactionBuilder};
17use alloy_primitives::{Address, hex};
18use alloy_provider::Provider;
19use clap::Parser;
20use eyre::Result;
21use foundry_cli::{
22    opts::{EthereumOpts, TransactionOpts},
23    utils::{self, resolve_lane},
24};
25use foundry_common::FoundryTransactionBuilder;
26use tempo_alloy::TempoNetwork;
27
28/// CLI arguments for `cast batch-mktx`.
29///
30/// Creates a signed (or unsigned) batch transaction.
31#[derive(Debug, Parser)]
32pub struct BatchMakeTxArgs {
33    /// Call specifications in format: `to[:<value>][:<sig>[:<args>]]` or `to[:<value>][:<0xdata>]`
34    ///
35    /// Examples:
36    ///   --call "0x1234567890123456789012345678901234567890:0.1ether" (ETH transfer)
37    ///   --call "0x1234567890123456789012345678901234567890::transfer(address,uint256):
38    /// 0x0987654321098765432109876543210987654321,1000" (ERC20 transfer)
39    ///   --call "0x1234567890123456789012345678901234567890::0x123def" (raw calldata)
40    ///   --call "0x1234567890123456789012345678901234567890:1ether:deposit()" (value + function
41    /// call)
42    #[arg(long = "call", value_name = "SPEC", required = true)]
43    pub calls: Vec<String>,
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    #[command(flatten)]
53    pub eth: EthereumOpts,
54
55    /// Generate a raw RLP-encoded unsigned transaction.
56    #[arg(long)]
57    pub raw_unsigned: bool,
58
59    /// Call `eth_signTransaction` using the `--from` argument or $ETH_FROM as sender
60    #[arg(long, requires = "from", conflicts_with = "raw_unsigned")]
61    pub ethsign: bool,
62}
63
64impl BatchMakeTxArgs {
65    pub async fn run(self) -> Result<()> {
66        let Self { calls, mut tx, force, eth, raw_unsigned, ethsign } = self;
67        let has_nonce = tx.nonce.is_some();
68        let has_session = tx.tempo.session_id()?.is_some();
69        let expires_at = tx.tempo.resolve_expires();
70
71        if has_session && raw_unsigned {
72            eyre::bail!("--tempo.session/TEMPO_SESSION_ID cannot be combined with --raw-unsigned");
73        }
74        if has_session && ethsign {
75            eyre::bail!("--tempo.session/TEMPO_SESSION_ID cannot be combined with --ethsign");
76        }
77
78        let (config, provider) = tempo::tempo_provider(&eth)?;
79        // The provider is not consulted for fee tokens in `--curl` mode.
80        let fee_provider = (!config.eth_rpc_curl).then_some(&provider);
81        let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?;
82        let lane = resolved_lane.as_ref();
83
84        let chain = utils::get_chain(config.chain, &provider).await?;
85        // A raw unsigned transaction needs no signer, but the access-key metadata still shapes
86        // the request.
87        let (signer, tempo_access_key) = if raw_unsigned {
88            (None, eth.wallet.maybe_signer_for_chain(chain.id()).await?.1)
89        } else {
90            tempo::resolve_session_or_wallet_signer(&tx.tempo, &eth.wallet, chain.id()).await?
91        };
92
93        // Preserve key_id for modes that do not call build_with_tempo_wallet, such as raw unsigned.
94        if let Some(access_key) = &tempo_access_key {
95            tx.tempo.key_id = Some(access_key.key_id()?);
96        }
97
98        let builder = CastTxBuilder::<TempoNetwork, _, _>::new(&provider, tx, &config).await?;
99        let tx_builder = with_batch_calls(&calls, builder, &provider).await?;
100        tempo::print_expires(expires_at)?;
101
102        if raw_unsigned {
103            if eth.wallet.from.is_none() && !has_nonce {
104                eyre::bail!(
105                    "Missing required parameters for raw unsigned transaction. When --from is not provided, you must specify: --nonce"
106                );
107            }
108
109            let from = eth.wallet.from.unwrap_or(Address::ZERO);
110            let Some(mut tx) = confirm_and_build(tx_builder, from, force, lane, false).await?
111            else {
112                return Ok(());
113            };
114            tempo::resolve_and_print_fee_token(fee_provider, &mut tx, Some(from)).await?;
115            let raw_tx = hex::encode_prefixed(tx.build_unsigned()?.encoded_for_signing());
116            sh_println!("{raw_tx}")?;
117            return Ok(());
118        }
119
120        if ethsign {
121            let Some(mut tx) =
122                confirm_and_build(tx_builder, config.sender, force, lane, true).await?
123            else {
124                return Ok(());
125            };
126            tempo::resolve_and_print_fee_token(fee_provider, &mut tx, Some(config.sender)).await?;
127            let signed_tx = provider.sign_transaction(tx).await?;
128            sh_println!("{signed_tx}")?;
129            return Ok(());
130        }
131
132        let signed_tx = if let Some(access_key) = &tempo_access_key {
133            let Some((mut tx, prepared)) =
134                confirm_and_build_with_tempo_wallet(tx_builder, access_key, force, lane).await?
135            else {
136                return Ok(());
137            };
138            tempo::resolve_and_print_fee_token(fee_provider, &mut tx, Some(prepared.account()))
139                .await?;
140            tx.sign_with_tempo_wallet(&prepared).await?
141        } else {
142            let (signer, from) = tx::resolve_send_signer(signer, &eth).await?;
143            let Some(mut tx) = confirm_and_build(tx_builder, &signer, force, lane, false).await?
144            else {
145                return Ok(());
146            };
147            tempo::resolve_and_print_fee_token(fee_provider, &mut tx, Some(from)).await?;
148            tx.build(&EthereumWallet::new(signer)).await?.encoded_2718()
149        };
150
151        sh_println!("{}", hex::encode_prefixed(signed_tx))?;
152
153        Ok(())
154    }
155}