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 = tempo::sponsor_hash(fee_provider, &mut tx, from, sponsor_fee_payer).await?;
223            return print_scalar(format!("{hash:?}"));
224        }
225
226        tempo::print_expires(expires_at)?;
227
228        if raw_unsigned {
229            // Without a sender the nonce cannot be fetched.
230            // See: <https://github.com/foundry-rs/foundry/issues/11110>
231            if eth.wallet.from.is_none() && tx.nonce.is_none() {
232                eyre::bail!(
233                    "Missing required parameters for raw unsigned transaction. When --from is not provided, you must specify: --nonce"
234                );
235            }
236            if tempo_sponsor.is_some() && eth.wallet.from.is_none() {
237                eyre::bail!(
238                    "--tempo.sponsor requires --from for --raw-unsigned because the sponsor digest commits to the sender"
239                );
240            }
241
242            // Use zero address as placeholder for unsigned transactions
243            let from = eth.wallet.from.unwrap_or(Address::ZERO);
244            let Some(mut tx) = confirm_and_build(tx_builder, from, force, lane, false).await?
245            else {
246                return Ok(());
247            };
248            tempo::apply_fee_payment::<N, _>(tempo_sponsor.as_ref(), fee_provider, &mut tx, from)
249                .await?;
250            return print_scalar(hex::encode_prefixed(tx.build_unsigned()?.encoded_for_signing()));
251        }
252
253        if let Some(signature) = signature {
254            let signature = signature.normalized_s();
255            let from = eth.wallet.from.expect("required by clap");
256            let Some(mut tx) = confirm_and_build(tx_builder, from, force, lane, false).await?
257            else {
258                return Ok(());
259            };
260            tempo::apply_fee_payment::<N, _>(tempo_sponsor.as_ref(), fee_provider, &mut tx, from)
261                .await?;
262
263            let tx = tx.build_unsigned()?;
264            let recovered = signature.recover_address_from_prehash(&tx.signature_hash())?;
265            if recovered != from {
266                eyre::bail!(
267                    "The provided signature recovers to {recovered}, which does not match the specified sender {from}"
268                );
269            }
270
271            let tx = N::TxEnvelope::from(tx.into_signed(signature));
272            return print_scalar(hex::encode_prefixed(tx.encoded_2718()));
273        }
274
275        if ethsign {
276            // Use "eth_signTransaction" to sign the transaction only works if the node/RPC has
277            // unlocked accounts.
278            let Some(mut tx) =
279                confirm_and_build(tx_builder, config.sender, force, lane, true).await?
280            else {
281                return Ok(());
282            };
283            tempo::apply_fee_payment::<N, _>(
284                tempo_sponsor.as_ref(),
285                fee_provider,
286                &mut tx,
287                config.sender,
288            )
289            .await?;
290            return print_scalar(provider.sign_transaction(tx).await?);
291        }
292
293        // Default to using the local signer.
294        let signed_tx = if let Some(access_key) = access_key {
295            let Some((mut tx, prepared)) =
296                confirm_and_build_with_tempo_wallet(tx_builder, &access_key, force, lane).await?
297            else {
298                return Ok(());
299            };
300            tempo::apply_fee_payment::<N, _>(
301                tempo_sponsor.as_ref(),
302                fee_provider,
303                &mut tx,
304                prepared.account(),
305            )
306            .await?;
307            tx.sign_with_tempo_wallet(&prepared).await?
308        } else {
309            let (signer, from) = tx::resolve_send_signer(signer, &eth).await?;
310            let Some(mut tx) = confirm_and_build(tx_builder, &signer, force, lane, false).await?
311            else {
312                return Ok(());
313            };
314            tempo::apply_fee_payment::<N, _>(tempo_sponsor.as_ref(), fee_provider, &mut tx, from)
315                .await?;
316            tx.build(&EthereumWallet::new(signer)).await?.encoded_2718()
317        };
318
319        print_scalar(hex::encode_prefixed(signed_tx))
320    }
321}