Skip to main content

cast/cmd/
mktx.rs

1use crate::tx::{self, CastTxBuilder};
2use alloy_consensus::{SignableTransaction, Signed};
3use alloy_eips::Encodable2718;
4use alloy_ens::NameOrAddress;
5use alloy_network::{
6    Ethereum, EthereumWallet, Network, NetworkTransactionBuilder, TransactionBuilder,
7};
8use alloy_primitives::{Address, hex};
9use alloy_provider::Provider;
10use alloy_signer::{Signature, Signer};
11use clap::Parser;
12use eyre::Result;
13use foundry_cli::{
14    json::print_scalar,
15    opts::{EthereumOpts, TransactionOpts},
16    utils::{LoadConfig, maybe_print_resolved_lane, resolve_lane},
17};
18use foundry_common::{
19    FoundryTransactionBuilder,
20    provider::ProviderBuilder,
21    tempo::{maybe_print_fee_token, resolve_and_set_fee_token},
22};
23use std::{path::PathBuf, str::FromStr};
24use tempo_alloy::TempoNetwork;
25
26/// CLI arguments for `cast mktx`.
27#[derive(Debug, Parser)]
28pub struct MakeTxArgs {
29    /// The destination of the transaction.
30    ///
31    /// If not provided, you must use `cast mktx --create`.
32    #[arg(value_parser = NameOrAddress::from_str)]
33    to: Option<NameOrAddress>,
34
35    /// The signature of the function to call.
36    sig: Option<String>,
37
38    /// The arguments of the function to call.
39    #[arg(allow_negative_numbers = true)]
40    args: Vec<String>,
41
42    #[command(subcommand)]
43    command: Option<MakeTxSubcommands>,
44
45    #[command(flatten)]
46    tx: TransactionOpts,
47
48    /// The path of blob data to be sent.
49    #[arg(
50        long,
51        value_name = "BLOB_DATA_PATH",
52        conflicts_with = "legacy",
53        requires = "blob",
54        help_heading = "Transaction options"
55    )]
56    path: Option<PathBuf>,
57
58    #[command(flatten)]
59    eth: EthereumOpts,
60
61    /// Generate a raw RLP-encoded unsigned transaction.
62    ///
63    /// Relaxes the wallet requirement.
64    #[arg(long)]
65    raw_unsigned: bool,
66
67    /// Call `eth_signTransaction` using the `--from` argument or $ETH_FROM as sender
68    #[arg(long, requires = "from", conflicts_with = "raw_unsigned")]
69    ethsign: bool,
70
71    /// Generate a raw signed transaction using the provided 65-byte signature.
72    #[arg(
73        long,
74        value_name = "SIGNATURE",
75        requires = "from",
76        conflicts_with_all = ["raw_unsigned", "ethsign"]
77    )]
78    signature: Option<Signature>,
79}
80
81#[derive(Debug, Parser)]
82pub enum MakeTxSubcommands {
83    /// Use to deploy raw contract bytecode.
84    #[command(name = "--create")]
85    Create {
86        /// The initialization bytecode of the contract to deploy.
87        code: String,
88
89        /// The signature of the constructor.
90        sig: Option<String>,
91
92        /// The constructor arguments.
93        #[arg(allow_negative_numbers = true)]
94        args: Vec<String>,
95    },
96}
97
98impl MakeTxArgs {
99    pub async fn run(self) -> Result<()> {
100        if self.tx.tempo.is_tempo() {
101            self.run_generic::<TempoNetwork>().await
102        } else {
103            self.run_generic::<Ethereum>().await
104        }
105    }
106
107    pub async fn run_generic<N: Network>(self) -> Result<()>
108    where
109        N::TxEnvelope: From<Signed<N::UnsignedTx>>,
110        N::UnsignedTx: SignableTransaction<Signature>,
111        N::TransactionRequest: FoundryTransactionBuilder<N>,
112    {
113        let Self {
114            to,
115            mut sig,
116            mut args,
117            command,
118            mut tx,
119            path,
120            eth,
121            raw_unsigned,
122            ethsign,
123            signature,
124        } = self;
125
126        let print_sponsor_hash = tx.tempo.print_sponsor_hash;
127        let sponsor_fee_payer = tx.tempo.sponsor;
128        let expires_at = tx.tempo.resolve_expires();
129        let tempo_sponsor =
130            if print_sponsor_hash { None } else { tx.tempo.sponsor_config().await? };
131
132        let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None };
133
134        let code = if let Some(MakeTxSubcommands::Create {
135            code,
136            sig: constructor_sig,
137            args: constructor_args,
138        }) = command
139        {
140            sig = constructor_sig;
141            args = constructor_args;
142            Some(code)
143        } else {
144            None
145        };
146
147        let config = eth.load_config()?;
148
149        let provider = ProviderBuilder::<N>::from_config(&config)?.build()?;
150
151        // Resolve `--tempo.lane <name>` against the lanes file (default
152        // `<root>/tempo.lanes.toml`) and populate `tx.tempo.nonce_key` from the lane.
153        // Must happen before `tx.clone()` so the cloned tx carries the resolved nonce_key.
154        let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?;
155
156        let tx_builder = CastTxBuilder::new(&provider, tx.clone(), &config)
157            .await?
158            .with_to(to)
159            .await?
160            .with_code_sig_and_args(code, sig, args)
161            .await?
162            .with_blob_data(blob_data)?;
163        let chain = tx_builder.chain();
164
165        // If --tempo.print-sponsor-hash was passed, build the tx, print the hash, and exit.
166        if print_sponsor_hash {
167            // Resolve the signer to derive the actual sender address, since the
168            // sponsor hash commits to the sender.
169            let signer = eth.wallet.signer().await?;
170            let from = signer.address();
171            let (mut tx, _) = tx_builder.build(from).await?;
172            if let Some(fee_payer) = sponsor_fee_payer {
173                resolve_and_set_fee_token(
174                    (!config.eth_rpc_curl).then_some(&provider),
175                    Some(chain),
176                    &mut tx,
177                    Some(fee_payer),
178                )
179                .await?;
180            }
181            let hash = tx.compute_sponsor_hash(from).ok_or_else(|| {
182                eyre::eyre!("This network does not support sponsored transactions")
183            })?;
184            print_scalar(format!("{hash:?}"))?;
185            return Ok(());
186        }
187
188        if let Some(ts) = expires_at {
189            sh_status!("Transaction expires at unix timestamp {ts}")?;
190        }
191
192        if raw_unsigned {
193            // Build unsigned raw tx
194            // Check if nonce is provided when --from is not specified
195            // See: <https://github.com/foundry-rs/foundry/issues/11110>
196            if eth.wallet.from.is_none() && tx.nonce.is_none() {
197                eyre::bail!(
198                    "Missing required parameters for raw unsigned transaction. When --from is not provided, you must specify: --nonce"
199                );
200            }
201            if tempo_sponsor.is_some() && eth.wallet.from.is_none() {
202                eyre::bail!(
203                    "--tempo.sponsor requires --from for --raw-unsigned because the sponsor digest commits to the sender"
204                );
205            }
206
207            // Use zero address as placeholder for unsigned transactions
208            let from = eth.wallet.from.unwrap_or(Address::ZERO);
209
210            let (mut tx, _) = tx_builder.build(from).await?;
211            maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
212            if let Some(sponsor) = &tempo_sponsor {
213                sponsor
214                    .resolve_and_set_fee_token(
215                        (!config.eth_rpc_curl).then_some(&provider),
216                        Some(chain),
217                        &mut tx,
218                    )
219                    .await?;
220                sponsor.attach_and_print::<N>(&mut tx, from).await?;
221            } else {
222                let fee_token = resolve_and_set_fee_token(
223                    (!config.eth_rpc_curl).then_some(&provider),
224                    Some(chain),
225                    &mut tx,
226                    Some(from),
227                )
228                .await?;
229                maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token)
230                    .await?;
231            }
232            let raw_tx = hex::encode_prefixed(tx.build_unsigned()?.encoded_for_signing());
233
234            print_scalar(raw_tx)?;
235            return Ok(());
236        }
237
238        if let Some(signature) = signature {
239            let signature = signature.normalized_s();
240            let from = eth.wallet.from.expect("required by clap");
241            let (mut tx, _) = tx_builder.build(from).await?;
242            maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
243            if let Some(sponsor) = &tempo_sponsor {
244                sponsor
245                    .resolve_and_set_fee_token(
246                        (!config.eth_rpc_curl).then_some(&provider),
247                        Some(chain),
248                        &mut tx,
249                    )
250                    .await?;
251                sponsor.attach_and_print::<N>(&mut tx, from).await?;
252            } else {
253                let fee_token = resolve_and_set_fee_token(
254                    (!config.eth_rpc_curl).then_some(&provider),
255                    Some(chain),
256                    &mut tx,
257                    Some(from),
258                )
259                .await?;
260                maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token)
261                    .await?;
262            }
263
264            let tx = tx.build_unsigned()?;
265            let recovered = signature.recover_address_from_prehash(&tx.signature_hash())?;
266            if recovered != from {
267                eyre::bail!(
268                    "The provided signature recovers to {recovered}, which does not match the specified sender {from}"
269                );
270            }
271
272            let tx = N::TxEnvelope::from(tx.into_signed(signature));
273            print_scalar(hex::encode_prefixed(tx.encoded_2718()))?;
274            return Ok(());
275        }
276
277        if ethsign {
278            // Use "eth_signTransaction" to sign the transaction only works if the node/RPC has
279            // unlocked accounts.
280            let (mut tx, _) = tx_builder.build(config.sender).await?;
281            maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
282            if let Some(sponsor) = &tempo_sponsor {
283                sponsor
284                    .resolve_and_set_fee_token(
285                        (!config.eth_rpc_curl).then_some(&provider),
286                        Some(chain),
287                        &mut tx,
288                    )
289                    .await?;
290                sponsor.attach_and_print::<N>(&mut tx, config.sender).await?;
291            } else {
292                let fee_token = resolve_and_set_fee_token(
293                    (!config.eth_rpc_curl).then_some(&provider),
294                    Some(chain),
295                    &mut tx,
296                    Some(config.sender),
297                )
298                .await?;
299                maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token)
300                    .await?;
301            }
302            let signed_tx = provider.sign_transaction(tx).await?;
303
304            print_scalar(signed_tx)?;
305            return Ok(());
306        }
307
308        // Default to using the local signer.
309        // Get the signer from the wallet, and fail if it can't be constructed.
310        let signer = eth.wallet.signer().await?;
311        let from = signer.address();
312
313        tx::validate_from_address(eth.wallet.from, from)?;
314
315        let (mut tx, _) = tx_builder.build(&signer).await?;
316        maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
317        if let Some(sponsor) = &tempo_sponsor {
318            sponsor
319                .resolve_and_set_fee_token(
320                    (!config.eth_rpc_curl).then_some(&provider),
321                    Some(chain),
322                    &mut tx,
323                )
324                .await?;
325            sponsor.attach_and_print::<N>(&mut tx, from).await?;
326        } else {
327            let fee_token = resolve_and_set_fee_token(
328                (!config.eth_rpc_curl).then_some(&provider),
329                Some(chain),
330                &mut tx,
331                Some(from),
332            )
333            .await?;
334            maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token).await?;
335        }
336
337        let tx = tx.build(&EthereumWallet::new(signer)).await?;
338
339        let signed_tx = format!("0x{}", hex::encode(tx.encoded_2718()));
340        print_scalar(signed_tx)?;
341
342        Ok(())
343    }
344}