cast/cmd/
mktx.rs

1use crate::tx::{self, CastTxBuilder};
2use alloy_eips::Encodable2718;
3use alloy_ens::NameOrAddress;
4use alloy_network::{EthereumWallet, NetworkWallet, TransactionBuilder};
5use alloy_primitives::{Address, hex};
6use alloy_provider::Provider;
7use alloy_signer::Signer;
8use clap::Parser;
9use eyre::Result;
10use foundry_cli::{
11    opts::{EthereumOpts, TransactionOpts},
12    utils::{LoadConfig, get_provider},
13};
14use std::{path::PathBuf, str::FromStr};
15
16/// CLI arguments for `cast mktx`.
17#[derive(Debug, Parser)]
18pub struct MakeTxArgs {
19    /// The destination of the transaction.
20    ///
21    /// If not provided, you must use `cast mktx --create`.
22    #[arg(value_parser = NameOrAddress::from_str)]
23    to: Option<NameOrAddress>,
24
25    /// The signature of the function to call.
26    sig: Option<String>,
27
28    /// The arguments of the function to call.
29    #[arg(allow_negative_numbers = true)]
30    args: Vec<String>,
31
32    #[command(subcommand)]
33    command: Option<MakeTxSubcommands>,
34
35    #[command(flatten)]
36    tx: TransactionOpts,
37
38    /// The path of blob data to be sent.
39    #[arg(
40        long,
41        value_name = "BLOB_DATA_PATH",
42        conflicts_with = "legacy",
43        requires = "blob",
44        help_heading = "Transaction options"
45    )]
46    path: Option<PathBuf>,
47
48    #[command(flatten)]
49    eth: EthereumOpts,
50
51    /// Generate a raw RLP-encoded unsigned transaction.
52    ///
53    /// Relaxes the wallet requirement.
54    #[arg(long)]
55    raw_unsigned: bool,
56
57    /// Call `eth_signTransaction` using the `--from` argument or $ETH_FROM as sender
58    #[arg(long, requires = "from", conflicts_with = "raw_unsigned")]
59    ethsign: bool,
60}
61
62#[derive(Debug, Parser)]
63pub enum MakeTxSubcommands {
64    /// Use to deploy raw contract bytecode.
65    #[command(name = "--create")]
66    Create {
67        /// The initialization bytecode of the contract to deploy.
68        code: String,
69
70        /// The signature of the constructor.
71        sig: Option<String>,
72
73        /// The constructor arguments.
74        #[arg(allow_negative_numbers = true)]
75        args: Vec<String>,
76    },
77}
78
79impl MakeTxArgs {
80    pub async fn run(self) -> Result<()> {
81        let Self { to, mut sig, mut args, command, tx, path, eth, raw_unsigned, ethsign } = self;
82
83        let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None };
84
85        let code = if let Some(MakeTxSubcommands::Create {
86            code,
87            sig: constructor_sig,
88            args: constructor_args,
89        }) = command
90        {
91            sig = constructor_sig;
92            args = constructor_args;
93            Some(code)
94        } else {
95            None
96        };
97
98        let config = eth.load_config()?;
99
100        let provider = get_provider(&config)?;
101
102        let tx_builder = CastTxBuilder::new(&provider, tx.clone(), &config)
103            .await?
104            .with_to(to)
105            .await?
106            .with_code_sig_and_args(code, sig, args)
107            .await?
108            .with_blob_data(blob_data)?;
109
110        if raw_unsigned {
111            // Build unsigned raw tx
112            // Check if nonce is provided when --from is not specified
113            // See: <https://github.com/foundry-rs/foundry/issues/11110>
114            if eth.wallet.from.is_none() && tx.nonce.is_none() {
115                eyre::bail!(
116                    "Missing required parameters for raw unsigned transaction. When --from is not provided, you must specify: --nonce"
117                );
118            }
119
120            // Use zero address as placeholder for unsigned transactions
121            let from = eth.wallet.from.unwrap_or(Address::ZERO);
122
123            let raw_tx = tx_builder.build_unsigned_raw(from).await?;
124
125            sh_println!("{raw_tx}")?;
126            return Ok(());
127        }
128
129        let is_tempo = tx_builder.is_tempo();
130
131        if ethsign {
132            // Use "eth_signTransaction" to sign the transaction only works if the node/RPC has
133            // unlocked accounts.
134            let (tx, _) = tx_builder.build(config.sender).await?;
135            let signed_tx = provider.sign_transaction(tx.into_inner()).await?;
136
137            sh_println!("{signed_tx}")?;
138            return Ok(());
139        }
140
141        // Default to using the local signer.
142        // Get the signer from the wallet, and fail if it can't be constructed.
143        let signer = eth.wallet.signer().await?;
144        let from = signer.address();
145
146        tx::validate_from_address(eth.wallet.from, from)?;
147
148        // Handle Tempo transactions separately
149        // TODO(onbjerg): All of this is a side effect of a few things, most notably that we do
150        // not use `FoundryNetwork` and `FoundryTransactionRequest` everywhere, which is
151        // downstream of the fact that we use `EthereumWallet` everywhere.
152        if is_tempo {
153            let (ftx, _) = tx_builder.build(&signer).await?;
154
155            // Sign using NetworkWallet<FoundryNetwork>
156            let signed_tx = signer.sign_request(ftx).await?;
157
158            // Encode as 2718
159            let mut raw_tx = Vec::with_capacity(signed_tx.encode_2718_len());
160            signed_tx.encode_2718(&mut raw_tx);
161
162            let signed_tx_hex = hex::encode(&raw_tx);
163            sh_println!("0x{signed_tx_hex}")?;
164
165            return Ok(());
166        }
167
168        let (tx, _) = tx_builder.build(&signer).await?;
169
170        let tx = tx.into_inner().build(&EthereumWallet::new(signer)).await?;
171
172        let signed_tx = hex::encode(tx.encoded_2718());
173        sh_println!("0x{signed_tx}")?;
174
175        Ok(())
176    }
177}