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