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    opts::{EthereumOpts, TransactionOpts},
15    utils::{LoadConfig, maybe_print_resolved_lane, resolve_lane},
16};
17use foundry_common::{FoundryTransactionBuilder, provider::ProviderBuilder};
18use std::{path::PathBuf, str::FromStr};
19use tempo_alloy::TempoNetwork;
20
21/// CLI arguments for `cast mktx`.
22#[derive(Debug, Parser)]
23pub struct MakeTxArgs {
24    /// The destination of the transaction.
25    ///
26    /// If not provided, you must use `cast mktx --create`.
27    #[arg(value_parser = NameOrAddress::from_str)]
28    to: Option<NameOrAddress>,
29
30    /// The signature of the function to call.
31    sig: Option<String>,
32
33    /// The arguments of the function to call.
34    #[arg(allow_negative_numbers = true)]
35    args: Vec<String>,
36
37    #[command(subcommand)]
38    command: Option<MakeTxSubcommands>,
39
40    #[command(flatten)]
41    tx: TransactionOpts,
42
43    /// The path of blob data to be sent.
44    #[arg(
45        long,
46        value_name = "BLOB_DATA_PATH",
47        conflicts_with = "legacy",
48        requires = "blob",
49        help_heading = "Transaction options"
50    )]
51    path: Option<PathBuf>,
52
53    #[command(flatten)]
54    eth: EthereumOpts,
55
56    /// Generate a raw RLP-encoded unsigned transaction.
57    ///
58    /// Relaxes the wallet requirement.
59    #[arg(long)]
60    raw_unsigned: bool,
61
62    /// Call `eth_signTransaction` using the `--from` argument or $ETH_FROM as sender
63    #[arg(long, requires = "from", conflicts_with = "raw_unsigned")]
64    ethsign: bool,
65}
66
67#[derive(Debug, Parser)]
68pub enum MakeTxSubcommands {
69    /// Use to deploy raw contract bytecode.
70    #[command(name = "--create")]
71    Create {
72        /// The initialization bytecode of the contract to deploy.
73        code: String,
74
75        /// The signature of the constructor.
76        sig: Option<String>,
77
78        /// The constructor arguments.
79        #[arg(allow_negative_numbers = true)]
80        args: Vec<String>,
81    },
82}
83
84impl MakeTxArgs {
85    pub async fn run(self) -> Result<()> {
86        if self.tx.tempo.is_tempo() {
87            self.run_generic::<TempoNetwork>().await
88        } else {
89            self.run_generic::<Ethereum>().await
90        }
91    }
92
93    pub async fn run_generic<N: Network>(self) -> Result<()>
94    where
95        N::TxEnvelope: From<Signed<N::UnsignedTx>>,
96        N::UnsignedTx: SignableTransaction<Signature>,
97        N::TransactionRequest: FoundryTransactionBuilder<N>,
98    {
99        let Self { to, mut sig, mut args, command, mut tx, path, eth, raw_unsigned, ethsign } =
100            self;
101
102        let print_sponsor_hash = tx.tempo.print_sponsor_hash;
103        let expires_at = tx.tempo.resolve_expires();
104        let tempo_sponsor =
105            if print_sponsor_hash { None } else { tx.tempo.sponsor_config().await? };
106
107        let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None };
108
109        let code = if let Some(MakeTxSubcommands::Create {
110            code,
111            sig: constructor_sig,
112            args: constructor_args,
113        }) = command
114        {
115            sig = constructor_sig;
116            args = constructor_args;
117            Some(code)
118        } else {
119            None
120        };
121
122        let config = eth.load_config()?;
123
124        let provider = ProviderBuilder::<N>::from_config(&config)?.build()?;
125
126        // Resolve `--tempo.lane <name>` against the lanes file (default
127        // `<root>/tempo.lanes.toml`) and populate `tx.tempo.nonce_key` from the lane.
128        // Must happen before `tx.clone()` so the cloned tx carries the resolved nonce_key.
129        let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?;
130
131        let tx_builder = CastTxBuilder::new(&provider, tx.clone(), &config)
132            .await?
133            .with_to(to)
134            .await?
135            .with_code_sig_and_args(code, sig, args)
136            .await?
137            .with_blob_data(blob_data)?;
138
139        // If --tempo.print-sponsor-hash was passed, build the tx, print the hash, and exit.
140        if print_sponsor_hash {
141            // Resolve the signer to derive the actual sender address, since the
142            // sponsor hash commits to the sender.
143            let signer = eth.wallet.signer().await?;
144            let from = signer.address();
145            let (tx, _) = tx_builder.build(from).await?;
146            let hash = tx.compute_sponsor_hash(from).ok_or_else(|| {
147                eyre::eyre!("This network does not support sponsored transactions")
148            })?;
149            sh_println!("{hash:?}")?;
150            return Ok(());
151        }
152
153        if let Some(ts) = expires_at {
154            sh_println!("Transaction expires at unix timestamp {ts}")?;
155        }
156
157        if raw_unsigned {
158            // Build unsigned raw tx
159            // Check if nonce is provided when --from is not specified
160            // See: <https://github.com/foundry-rs/foundry/issues/11110>
161            if eth.wallet.from.is_none() && tx.nonce.is_none() {
162                eyre::bail!(
163                    "Missing required parameters for raw unsigned transaction. When --from is not provided, you must specify: --nonce"
164                );
165            }
166            if tempo_sponsor.is_some() && eth.wallet.from.is_none() {
167                eyre::bail!(
168                    "--tempo.sponsor requires --from for --raw-unsigned because the sponsor digest commits to the sender"
169                );
170            }
171
172            // Use zero address as placeholder for unsigned transactions
173            let from = eth.wallet.from.unwrap_or(Address::ZERO);
174
175            let (mut tx, _) = tx_builder.build(from).await?;
176            maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
177            if let Some(sponsor) = &tempo_sponsor {
178                sponsor.attach_and_print::<N>(&mut tx, from).await?;
179            }
180            let raw_tx = hex::encode_prefixed(tx.build_unsigned()?.encoded_for_signing());
181
182            sh_println!("{raw_tx}")?;
183            return Ok(());
184        }
185
186        if ethsign {
187            // Use "eth_signTransaction" to sign the transaction only works if the node/RPC has
188            // unlocked accounts.
189            let (mut tx, _) = tx_builder.build(config.sender).await?;
190            maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
191            if let Some(sponsor) = &tempo_sponsor {
192                sponsor.attach_and_print::<N>(&mut tx, config.sender).await?;
193            }
194            let signed_tx = provider.sign_transaction(tx).await?;
195
196            sh_println!("{signed_tx}")?;
197            return Ok(());
198        }
199
200        // Default to using the local signer.
201        // Get the signer from the wallet, and fail if it can't be constructed.
202        let signer = eth.wallet.signer().await?;
203        let from = signer.address();
204
205        tx::validate_from_address(eth.wallet.from, from)?;
206
207        let (mut tx, _) = tx_builder.build(&signer).await?;
208        maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
209        if let Some(sponsor) = &tempo_sponsor {
210            sponsor.attach_and_print::<N>(&mut tx, from).await?;
211        }
212
213        let tx = tx.build(&EthereumWallet::new(signer)).await?;
214
215        let signed_tx = hex::encode(tx.encoded_2718());
216        sh_println!("0x{signed_tx}")?;
217
218        Ok(())
219    }
220}