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    call_spec::CallSpec,
8    tempo,
9    tx::{self, CastTxBuilder},
10};
11use alloy_consensus::SignableTransaction;
12use alloy_eips::eip2718::Encodable2718;
13use alloy_network::{EthereumWallet, NetworkTransactionBuilder, TransactionBuilder};
14use alloy_primitives::Address;
15use alloy_provider::Provider;
16use alloy_signer::Signer;
17use clap::Parser;
18use eyre::{Result, eyre};
19use foundry_cli::{
20    opts::{EthereumOpts, TransactionOpts},
21    utils::{self, LoadConfig, maybe_print_resolved_lane, resolve_lane},
22};
23use foundry_common::{FoundryTransactionBuilder, provider::ProviderBuilder};
24use tempo_alloy::TempoNetwork;
25
26/// CLI arguments for `cast batch-mktx`.
27///
28/// Creates a signed (or unsigned) batch transaction.
29#[derive(Debug, Parser)]
30pub struct BatchMakeTxArgs {
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    #[arg(long = "call", value_name = "SPEC", required = true)]
38    pub calls: Vec<String>,
39
40    #[command(flatten)]
41    pub tx: TransactionOpts,
42
43    #[command(flatten)]
44    pub eth: EthereumOpts,
45
46    /// Generate a raw RLP-encoded unsigned transaction.
47    #[arg(long)]
48    pub raw_unsigned: bool,
49
50    /// Call `eth_signTransaction` using the `--from` argument or $ETH_FROM as sender
51    #[arg(long, requires = "from", conflicts_with = "raw_unsigned")]
52    pub ethsign: bool,
53}
54
55impl BatchMakeTxArgs {
56    pub async fn run(self) -> Result<()> {
57        let Self { calls, mut tx, eth, raw_unsigned, ethsign } = self;
58        let has_nonce = tx.nonce.is_some();
59        let expires_at = tx.tempo.resolve_expires();
60
61        if calls.is_empty() {
62            return Err(eyre!("No calls specified. Use --call to specify at least one call."));
63        }
64
65        let config = eth.load_config()?;
66        let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
67
68        // Resolve `--tempo.lane <name>` against the lanes file (default
69        // `<root>/tempo.lanes.toml`) and populate `tx.tempo.nonce_key` from the lane.
70        let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?;
71
72        // Resolve signer to detect keychain mode
73        let (signer, tempo_access_key) = 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        let mut tempo_calls = Vec::with_capacity(call_specs.len());
86        for (i, spec) in call_specs.iter().enumerate() {
87            tempo_calls.push(
88                spec.resolve(
89                    i,
90                    chain,
91                    &provider,
92                    etherscan_api_key.as_deref(),
93                    etherscan_api_url.as_deref(),
94                )
95                .await?,
96            );
97        }
98
99        sh_println!("Building batch transaction with {} call(s)...", tempo_calls.len())?;
100        tempo::print_expires(expires_at)?;
101
102        // Preserve key_id for modes that do not call build_with_access_key, such as raw unsigned.
103        if let Some(ref access_key) = tempo_access_key {
104            tx.tempo.key_id = Some(access_key.key_address);
105        }
106
107        // Build transaction request with calls
108        let mut builder = CastTxBuilder::<TempoNetwork, _, _>::new(&provider, tx, &config).await?;
109
110        // Set calls on the transaction
111        builder.tx.calls = tempo_calls;
112
113        // Set dummy "to" from first call
114        let first_call_to = call_specs.first().map(|s| s.to);
115        let builder = builder.with_to(first_call_to.map(Into::into)).await?;
116        let tx_builder = builder.with_code_sig_and_args(None, None, vec![]).await?;
117
118        if raw_unsigned {
119            if eth.wallet.from.is_none() && !has_nonce {
120                eyre::bail!(
121                    "Missing required parameters for raw unsigned transaction. When --from is not provided, you must specify: --nonce"
122                );
123            }
124
125            let from = eth.wallet.from.unwrap_or(Address::ZERO);
126            let (tx, _) = tx_builder.build(from).await?;
127            maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
128            let raw_tx =
129                alloy_primitives::hex::encode_prefixed(tx.build_unsigned()?.encoded_for_signing());
130            sh_println!("{raw_tx}")?;
131            return Ok(());
132        }
133
134        if ethsign {
135            let (tx, _) = tx_builder.build(config.sender).await?;
136            maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
137            let signed_tx = provider.sign_transaction(tx).await?;
138            sh_println!("{signed_tx}")?;
139            return Ok(());
140        }
141
142        // Default: use local signer
143        let signer = match signer {
144            Some(s) => s,
145            None => eth.wallet.signer().await?,
146        };
147
148        let signed_tx = if let Some(ref access_key) = tempo_access_key {
149            let (tx, _) =
150                tx_builder.build_with_access_key(access_key.wallet_address, access_key).await?;
151            maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
152            let raw_tx = tx
153                .sign_with_access_key(
154                    &provider,
155                    &signer,
156                    access_key.wallet_address,
157                    access_key.key_address,
158                    access_key.key_authorization.as_ref(),
159                )
160                .await?;
161            alloy_primitives::hex::encode(raw_tx)
162        } else {
163            tx::validate_from_address(eth.wallet.from, Signer::address(&signer))?;
164            let (tx, _) = tx_builder.build(&signer).await?;
165            maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
166            let envelope = tx.build(&EthereumWallet::new(signer)).await?;
167            alloy_primitives::hex::encode(envelope.encoded_2718())
168        };
169
170        sh_println!("0x{signed_tx}")?;
171
172        Ok(())
173    }
174}