cast/cmd/
send.rs

1use crate::{
2    tx::{self, CastTxBuilder},
3    Cast,
4};
5use alloy_ens::NameOrAddress;
6use alloy_network::{AnyNetwork, EthereumWallet};
7use alloy_provider::{Provider, ProviderBuilder};
8use alloy_rpc_types::TransactionRequest;
9use alloy_serde::WithOtherFields;
10use alloy_signer::Signer;
11use clap::Parser;
12use eyre::{eyre, Result};
13use foundry_cli::{
14    opts::{EthereumOpts, TransactionOpts},
15    utils,
16    utils::LoadConfig,
17};
18use std::{path::PathBuf, str::FromStr};
19
20/// CLI arguments for `cast send`.
21#[derive(Debug, Parser)]
22pub struct SendTxArgs {
23    /// The destination of the transaction.
24    ///
25    /// If not provided, you must use cast send --create.
26    #[arg(value_parser = NameOrAddress::from_str)]
27    to: Option<NameOrAddress>,
28
29    /// The signature of the function to call.
30    sig: Option<String>,
31
32    /// The arguments of the function to call.
33    args: Vec<String>,
34
35    /// Only print the transaction hash and exit immediately.
36    #[arg(id = "async", long = "async", alias = "cast-async", env = "CAST_ASYNC")]
37    cast_async: bool,
38
39    /// The number of confirmations until the receipt is fetched.
40    #[arg(long, default_value = "1")]
41    confirmations: u64,
42
43    #[command(subcommand)]
44    command: Option<SendTxSubcommands>,
45
46    /// Send via `eth_sendTransaction` using the `--from` argument or $ETH_FROM as sender
47    #[arg(long, requires = "from")]
48    unlocked: bool,
49
50    /// Timeout for sending the transaction.
51    #[arg(long, env = "ETH_TIMEOUT")]
52    pub timeout: Option<u64>,
53
54    #[command(flatten)]
55    tx: TransactionOpts,
56
57    #[command(flatten)]
58    eth: EthereumOpts,
59
60    /// The path of blob data to be sent.
61    #[arg(
62        long,
63        value_name = "BLOB_DATA_PATH",
64        conflicts_with = "legacy",
65        requires = "blob",
66        help_heading = "Transaction options"
67    )]
68    path: Option<PathBuf>,
69}
70
71#[derive(Debug, Parser)]
72pub enum SendTxSubcommands {
73    /// Use to deploy raw contract bytecode.
74    #[command(name = "--create")]
75    Create {
76        /// The bytecode of the contract to deploy.
77        code: String,
78
79        /// The signature of the function to call.
80        sig: Option<String>,
81
82        /// The arguments of the function to call.
83        args: Vec<String>,
84    },
85}
86
87impl SendTxArgs {
88    #[expect(dependency_on_unit_never_type_fallback)]
89    pub async fn run(self) -> eyre::Result<()> {
90        let Self {
91            eth,
92            to,
93            mut sig,
94            cast_async,
95            mut args,
96            tx,
97            confirmations,
98            command,
99            unlocked,
100            path,
101            timeout,
102        } = self;
103
104        let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None };
105
106        let code = if let Some(SendTxSubcommands::Create {
107            code,
108            sig: constructor_sig,
109            args: constructor_args,
110        }) = command
111        {
112            // ensure we don't violate settings for transactions that can't be CREATE: 7702 and 4844
113            // which require mandatory target
114            if to.is_none() && tx.auth.is_some() {
115                return Err(eyre!("EIP-7702 transactions can't be CREATE transactions and require a destination address"));
116            }
117            // ensure we don't violate settings for transactions that can't be CREATE: 7702 and 4844
118            // which require mandatory target
119            if to.is_none() && blob_data.is_some() {
120                return Err(eyre!("EIP-4844 transactions can't be CREATE transactions and require a destination address"));
121            }
122
123            sig = constructor_sig;
124            args = constructor_args;
125            Some(code)
126        } else {
127            None
128        };
129
130        let config = eth.load_config()?;
131        let provider = utils::get_provider(&config)?;
132
133        let builder = CastTxBuilder::new(&provider, tx, &config)
134            .await?
135            .with_to(to)
136            .await?
137            .with_code_sig_and_args(code, sig, args)
138            .await?
139            .with_blob_data(blob_data)?;
140
141        let timeout = timeout.unwrap_or(config.transaction_timeout);
142
143        // Case 1:
144        // Default to sending via eth_sendTransaction if the --unlocked flag is passed.
145        // This should be the only way this RPC method is used as it requires a local node
146        // or remote RPC with unlocked accounts.
147        if unlocked {
148            // only check current chain id if it was specified in the config
149            if let Some(config_chain) = config.chain {
150                let current_chain_id = provider.get_chain_id().await?;
151                let config_chain_id = config_chain.id();
152                // switch chain if current chain id is not the same as the one specified in the
153                // config
154                if config_chain_id != current_chain_id {
155                    sh_warn!("Switching to chain {}", config_chain)?;
156                    provider
157                        .raw_request(
158                            "wallet_switchEthereumChain".into(),
159                            [serde_json::json!({
160                                "chainId": format!("0x{:x}", config_chain_id),
161                            })],
162                        )
163                        .await?;
164                }
165            }
166
167            let (tx, _) = builder.build(config.sender).await?;
168
169            cast_send(provider, tx, cast_async, confirmations, timeout).await
170        // Case 2:
171        // An option to use a local signer was provided.
172        // If we cannot successfully instantiate a local signer, then we will assume we don't have
173        // enough information to sign and we must bail.
174        } else {
175            // Retrieve the signer, and bail if it can't be constructed.
176            let signer = eth.wallet.signer().await?;
177            let from = signer.address();
178
179            tx::validate_from_address(eth.wallet.from, from)?;
180
181            let (tx, _) = builder.build(&signer).await?;
182
183            let wallet = EthereumWallet::from(signer);
184            let provider = ProviderBuilder::<_, _, AnyNetwork>::default()
185                .wallet(wallet)
186                .connect_provider(&provider);
187
188            cast_send(provider, tx, cast_async, confirmations, timeout).await
189        }
190    }
191}
192
193async fn cast_send<P: Provider<AnyNetwork>>(
194    provider: P,
195    tx: WithOtherFields<TransactionRequest>,
196    cast_async: bool,
197    confs: u64,
198    timeout: u64,
199) -> Result<()> {
200    let cast = Cast::new(provider);
201    let pending_tx = cast.send(tx).await?;
202
203    let tx_hash = pending_tx.inner().tx_hash();
204
205    if cast_async {
206        sh_println!("{tx_hash:#x}")?;
207    } else {
208        let receipt =
209            cast.receipt(format!("{tx_hash:#x}"), None, confs, Some(timeout), false).await?;
210        sh_println!("{receipt}")?;
211    }
212
213    Ok(())
214}