Skip to main content

cast/cmd/
mktx.rs

1use super::auth::{confirm_and_build, confirm_and_build_with_tempo_wallet};
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::{Ethereum, EthereumWallet, Network, NetworkTransactionBuilder};
10use alloy_primitives::{Address, hex};
11use alloy_provider::Provider;
12use alloy_signer::{Signature, Signer};
13use clap::Parser;
14use eyre::Result;
15use foundry_cli::{
16    json::print_scalar,
17    opts::{EthereumOpts, TransactionOpts},
18    utils::{LoadConfig, resolve_lane},
19};
20use foundry_common::{FoundryTransactionBuilder, provider::ProviderBuilder};
21use foundry_wallets::{TempoAccountsWallet, WalletSigner};
22use std::{path::PathBuf, str::FromStr};
23use tempo_alloy::TempoNetwork;
24
25#[cfg(feature = "base")]
26use base_common_network::Base;
27
28/// CLI arguments for `cast mktx`.
29#[derive(Debug, Parser)]
30pub struct MakeTxArgs {
31    /// The destination of the transaction.
32    ///
33    /// If not provided, you must use `cast mktx --create`.
34    #[arg(value_parser = NameOrAddress::from_str)]
35    to: Option<NameOrAddress>,
36
37    /// The signature of the function to call.
38    sig: Option<String>,
39
40    /// The arguments of the function to call.
41    #[arg(allow_negative_numbers = true)]
42    args: Vec<String>,
43
44    #[command(subcommand)]
45    command: Option<MakeTxSubcommands>,
46
47    #[command(flatten)]
48    tx: TransactionOpts,
49
50    /// Skip the EIP-7702 authorization disclosure confirmation.
51    #[arg(long)]
52    force: bool,
53
54    /// The path of blob data to be sent.
55    #[arg(
56        long,
57        value_name = "BLOB_DATA_PATH",
58        conflicts_with = "legacy",
59        requires = "blob",
60        help_heading = "Transaction options"
61    )]
62    path: Option<PathBuf>,
63
64    #[command(flatten)]
65    eth: EthereumOpts,
66
67    /// Generate a raw RLP-encoded unsigned transaction.
68    ///
69    /// Relaxes the wallet requirement.
70    #[arg(long)]
71    raw_unsigned: bool,
72
73    /// Call `eth_signTransaction` using the `--from` argument or $ETH_FROM as sender
74    #[arg(long, requires = "from", conflicts_with = "raw_unsigned")]
75    ethsign: bool,
76
77    /// Generate a raw signed transaction using the provided 65-byte signature.
78    #[arg(
79        long,
80        value_name = "SIGNATURE",
81        requires = "from",
82        conflicts_with_all = ["raw_unsigned", "ethsign"]
83    )]
84    signature: Option<Signature>,
85}
86
87#[derive(Debug, Parser)]
88pub enum MakeTxSubcommands {
89    /// Use to deploy raw contract bytecode.
90    #[command(name = "--create")]
91    Create {
92        /// The initialization bytecode of the contract to deploy.
93        code: String,
94
95        /// The signature of the constructor.
96        sig: Option<String>,
97
98        /// The constructor arguments.
99        #[arg(allow_negative_numbers = true)]
100        args: Vec<String>,
101    },
102}
103
104impl MakeTxArgs {
105    pub async fn run(self) -> Result<()> {
106        if self.tx.tempo.sponsor_url.is_some() {
107            eyre::bail!(
108                "--sponsor-url is not supported by cast mktx; use --tempo.sponsor with \
109                 --tempo.sponsor-signer or --tempo.sponsor-sig"
110            );
111        }
112
113        let (network, signer, access_key) =
114            tempo::resolve_transaction_network_and_signer(&self.tx.tempo, &self.eth).await?;
115        if network.is_tempo() {
116            return self.run_generic::<TempoNetwork>(signer, access_key).await;
117        }
118
119        #[cfg(feature = "base")]
120        if network.is_base() {
121            super::validate_base_transaction_options(&self.tx)?;
122            return self.run_generic::<Base>(signer, None).await;
123        }
124
125        self.run_generic::<Ethereum>(signer, None).await
126    }
127
128    async fn run_generic<N: Network>(
129        self,
130        pre_resolved_signer: Option<WalletSigner>,
131        pre_resolved_access_key: Option<TempoAccountsWallet>,
132    ) -> Result<()>
133    where
134        N::TxEnvelope: From<Signed<N::UnsignedTx>>,
135        N::UnsignedTx: SignableTransaction<Signature>,
136        N::TransactionRequest: FoundryTransactionBuilder<N>,
137    {
138        let Self {
139            to,
140            mut sig,
141            mut args,
142            command,
143            mut tx,
144            force,
145            path,
146            eth,
147            raw_unsigned,
148            ethsign,
149            signature,
150        } = self;
151        let has_session = tx.tempo.session_id()?.is_some();
152
153        let print_sponsor_hash = tx.tempo.print_sponsor_hash;
154        let sponsor_fee_payer = tx.tempo.sponsor;
155        let expires_at = tx.tempo.resolve_expires();
156        let tempo_sponsor =
157            if print_sponsor_hash { None } else { tx.tempo.sponsor_config().await? };
158
159        let blob_data = path.map(std::fs::read).transpose()?;
160
161        let code = if let Some(MakeTxSubcommands::Create {
162            code,
163            sig: constructor_sig,
164            args: constructor_args,
165        }) = command
166        {
167            sig = constructor_sig;
168            args = constructor_args;
169            Some(code)
170        } else {
171            None
172        };
173
174        let config = eth.load_config()?;
175        let provider = ProviderBuilder::<N>::from_config(&config)?.build()?;
176        // The provider is not consulted for fee tokens in `--curl` mode.
177        let fee_provider = (!config.eth_rpc_curl).then_some(&provider);
178
179        // Populate `tx.tempo.nonce_key` from `--tempo.lane` before the options are cloned into
180        // the builder.
181        let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?;
182        let lane = resolved_lane.as_ref();
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                let Some((tx, prepared)) =
205                    confirm_and_build_with_tempo_wallet(tx_builder, &access_key, force, None)
206                        .await?
207                else {
208                    return Ok(());
209                };
210                (tx, prepared.account())
211            } else {
212                let signer = match signer {
213                    Some(signer) => signer,
214                    None => eth.wallet.signer().await?,
215                };
216                let Some(tx) = confirm_and_build(tx_builder, &signer, force, None, false).await?
217                else {
218                    return Ok(());
219                };
220                (tx, signer.address())
221            };
222            let hash =
223                tempo::sponsor_hash(fee_provider, chain, &mut tx, from, sponsor_fee_payer).await?;
224            return print_scalar(format!("{hash:?}"));
225        }
226
227        tempo::print_expires(expires_at)?;
228
229        if raw_unsigned {
230            // Without a sender the nonce cannot be fetched.
231            // See: <https://github.com/foundry-rs/foundry/issues/11110>
232            if eth.wallet.from.is_none() && tx.nonce.is_none() {
233                eyre::bail!(
234                    "Missing required parameters for raw unsigned transaction. When --from is not provided, you must specify: --nonce"
235                );
236            }
237            if tempo_sponsor.is_some() && eth.wallet.from.is_none() {
238                eyre::bail!(
239                    "--tempo.sponsor requires --from for --raw-unsigned because the sponsor digest commits to the sender"
240                );
241            }
242
243            // Use zero address as placeholder for unsigned transactions
244            let from = eth.wallet.from.unwrap_or(Address::ZERO);
245            let Some(mut tx) = confirm_and_build(tx_builder, from, force, lane, false).await?
246            else {
247                return Ok(());
248            };
249            tempo::apply_fee_payment::<N, _>(
250                tempo_sponsor.as_ref(),
251                fee_provider,
252                chain,
253                &mut tx,
254                from,
255            )
256            .await?;
257            return print_scalar(hex::encode_prefixed(tx.build_unsigned()?.encoded_for_signing()));
258        }
259
260        if let Some(signature) = signature {
261            let signature = signature.normalized_s();
262            let from = eth.wallet.from.expect("required by clap");
263            let Some(mut tx) = confirm_and_build(tx_builder, from, force, lane, false).await?
264            else {
265                return Ok(());
266            };
267            tempo::apply_fee_payment::<N, _>(
268                tempo_sponsor.as_ref(),
269                fee_provider,
270                chain,
271                &mut tx,
272                from,
273            )
274            .await?;
275
276            let tx = tx.build_unsigned()?;
277            let recovered = signature.recover_address_from_prehash(&tx.signature_hash())?;
278            if recovered != from {
279                eyre::bail!(
280                    "The provided signature recovers to {recovered}, which does not match the specified sender {from}"
281                );
282            }
283
284            let tx = N::TxEnvelope::from(tx.into_signed(signature));
285            return print_scalar(hex::encode_prefixed(tx.encoded_2718()));
286        }
287
288        if ethsign {
289            // Use "eth_signTransaction" to sign the transaction only works if the node/RPC has
290            // unlocked accounts.
291            let Some(mut tx) =
292                confirm_and_build(tx_builder, config.sender, force, lane, true).await?
293            else {
294                return Ok(());
295            };
296            tempo::apply_fee_payment::<N, _>(
297                tempo_sponsor.as_ref(),
298                fee_provider,
299                chain,
300                &mut tx,
301                config.sender,
302            )
303            .await?;
304            return print_scalar(provider.sign_transaction(tx).await?);
305        }
306
307        // Default to using the local signer.
308        let signed_tx = if let Some(access_key) = access_key {
309            let Some((mut tx, prepared)) =
310                confirm_and_build_with_tempo_wallet(tx_builder, &access_key, force, lane).await?
311            else {
312                return Ok(());
313            };
314            tempo::apply_fee_payment::<N, _>(
315                tempo_sponsor.as_ref(),
316                fee_provider,
317                chain,
318                &mut tx,
319                prepared.account(),
320            )
321            .await?;
322            tx.sign_with_tempo_wallet(&prepared).await?
323        } else {
324            let (signer, from) = tx::resolve_send_signer(signer, &eth).await?;
325            let Some(mut tx) = confirm_and_build(tx_builder, &signer, force, lane, false).await?
326            else {
327                return Ok(());
328            };
329            tempo::apply_fee_payment::<N, _>(
330                tempo_sponsor.as_ref(),
331                fee_provider,
332                chain,
333                &mut tx,
334                from,
335            )
336            .await?;
337            tx.build(&EthereumWallet::new(signer)).await?.encoded_2718()
338        };
339
340        print_scalar(hex::encode_prefixed(signed_tx))
341    }
342}