Skip to main content

cast/cmd/
mktx.rs

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