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    tx::{self, CastTxBuilder},
9};
10use alloy_consensus::SignableTransaction;
11use alloy_eips::eip2718::Encodable2718;
12use alloy_network::{EthereumWallet, TransactionBuilder};
13use alloy_primitives::{Address, Bytes};
14use alloy_provider::Provider;
15use alloy_signer::Signer;
16use clap::Parser;
17use eyre::{Result, eyre};
18use foundry_cli::{
19    opts::{EthereumOpts, TransactionOpts},
20    utils::{self, LoadConfig, parse_function_args},
21};
22use foundry_common::{FoundryTransactionBuilder, provider::ProviderBuilder};
23use tempo_alloy::TempoNetwork;
24use tempo_primitives::transaction::Call;
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, tx, eth, raw_unsigned, ethsign } = self;
58        let has_nonce = tx.nonce.is_some();
59
60        if calls.is_empty() {
61            return Err(eyre!("No calls specified. Use --call to specify at least one call."));
62        }
63
64        let config = eth.load_config()?;
65        let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
66
67        // Resolve signer to detect keychain mode
68        let (signer, tempo_access_key) = eth.wallet.maybe_signer().await?;
69
70        // Parse all call specs
71        let call_specs: Vec<CallSpec> =
72            calls.iter().map(|s| CallSpec::parse(s)).collect::<Result<Vec<_>>>()?;
73
74        // Get chain for parsing function args
75        let chain = utils::get_chain(config.chain, &provider).await?;
76        let etherscan_api_key = config.get_etherscan_api_key(Some(chain));
77
78        // Build Vec<Call> from specs
79        let mut tempo_calls = Vec::with_capacity(call_specs.len());
80        for (i, spec) in call_specs.iter().enumerate() {
81            let input = if let Some(data) = &spec.data {
82                data.clone()
83            } else if let Some(sig) = &spec.sig {
84                let (encoded, _) = parse_function_args(
85                    sig,
86                    spec.args.clone(),
87                    Some(spec.to),
88                    chain,
89                    &provider,
90                    etherscan_api_key.as_deref(),
91                )
92                .await
93                .map_err(|e| eyre!("Failed to encode call {}: {}", i + 1, e))?;
94                Bytes::from(encoded)
95            } else {
96                Bytes::new()
97            };
98
99            tempo_calls.push(Call { to: spec.to.into(), value: spec.value, input });
100        }
101
102        sh_println!("Building batch transaction with {} call(s)...", tempo_calls.len())?;
103
104        // Build transaction request with calls
105        let mut builder = CastTxBuilder::<TempoNetwork, _, _>::new(&provider, tx, &config).await?;
106
107        // Set key_id for access key transactions
108        if let Some(ref access_key) = tempo_access_key {
109            builder.tx.set_key_id(access_key.key_address);
110        }
111
112        // Set calls on the transaction
113        builder.tx.calls = tempo_calls;
114
115        // Set dummy "to" from first call
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        let tx_builder = builder.with_code_sig_and_args(None, None, vec![]).await?;
119
120        if raw_unsigned {
121            if eth.wallet.from.is_none() && !has_nonce {
122                eyre::bail!(
123                    "Missing required parameters for raw unsigned transaction. When --from is not provided, you must specify: --nonce"
124                );
125            }
126
127            let from = eth.wallet.from.unwrap_or(Address::ZERO);
128            let (tx, _) = tx_builder.build(from).await?;
129            let raw_tx =
130                alloy_primitives::hex::encode_prefixed(tx.build_unsigned()?.encoded_for_signing());
131            sh_println!("{raw_tx}")?;
132            return Ok(());
133        }
134
135        if ethsign {
136            let (tx, _) = tx_builder.build(config.sender).await?;
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        let from = if let Some(ref access_key) = tempo_access_key {
148            access_key.wallet_address
149        } else {
150            Signer::address(&signer)
151        };
152
153        if tempo_access_key.is_none() {
154            tx::validate_from_address(eth.wallet.from, from)?;
155        }
156
157        let (tx, _) = if tempo_access_key.is_some() {
158            tx_builder.build(from).await?
159        } else {
160            tx_builder.build(&signer).await?
161        };
162
163        let signed_tx = if let Some(ref access_key) = tempo_access_key {
164            let raw_tx = tx
165                .sign_with_access_key(
166                    &provider,
167                    &signer,
168                    access_key.wallet_address,
169                    access_key.key_address,
170                    access_key.key_authorization.as_ref(),
171                )
172                .await?;
173            alloy_primitives::hex::encode(raw_tx)
174        } else {
175            let envelope = tx.build(&EthereumWallet::new(signer)).await?;
176            alloy_primitives::hex::encode(envelope.encoded_2718())
177        };
178
179        sh_println!("0x{signed_tx}")?;
180
181        Ok(())
182    }
183}