cast/cmd/
send.rs

1use std::{path::PathBuf, str::FromStr, time::Duration};
2
3use alloy_ens::NameOrAddress;
4use alloy_network::{AnyNetwork, EthereumWallet};
5use alloy_provider::{Provider, ProviderBuilder};
6use alloy_rpc_types::TransactionRequest;
7use alloy_serde::WithOtherFields;
8use alloy_signer::Signer;
9use clap::Parser;
10use eyre::{Result, eyre};
11use foundry_cli::{opts::TransactionOpts, utils, utils::LoadConfig};
12use foundry_wallets::WalletSigner;
13
14use crate::tx::{self, CastTxBuilder, CastTxSender, SendTxOpts};
15
16/// CLI arguments for `cast send`.
17#[derive(Debug, Parser)]
18pub struct SendTxArgs {
19    /// The destination of the transaction.
20    ///
21    /// If not provided, you must use cast send --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    /// Raw hex-encoded data for the transaction. Used instead of \[SIG\] and \[ARGS\].
33    #[arg(
34        long,
35        conflicts_with_all = &["sig", "args"]
36    )]
37    data: Option<String>,
38
39    #[command(flatten)]
40    send_tx: SendTxOpts,
41
42    #[command(subcommand)]
43    command: Option<SendTxSubcommands>,
44
45    /// Send via `eth_sendTransaction` using the `--from` argument or $ETH_FROM as sender
46    #[arg(long, requires = "from")]
47    unlocked: bool,
48
49    #[command(flatten)]
50    tx: TransactionOpts,
51
52    /// The path of blob data to be sent.
53    #[arg(
54        long,
55        value_name = "BLOB_DATA_PATH",
56        conflicts_with = "legacy",
57        requires = "blob",
58        help_heading = "Transaction options"
59    )]
60    path: Option<PathBuf>,
61}
62
63#[derive(Debug, Parser)]
64pub enum SendTxSubcommands {
65    /// Use to deploy raw contract bytecode.
66    #[command(name = "--create")]
67    Create {
68        /// The bytecode of the contract to deploy.
69        code: String,
70
71        /// The signature of the function to call.
72        sig: Option<String>,
73
74        /// The arguments of the function to call.
75        #[arg(allow_negative_numbers = true)]
76        args: Vec<String>,
77    },
78}
79
80impl SendTxArgs {
81    pub async fn run(self) -> eyre::Result<()> {
82        let Self { to, mut sig, mut args, data, send_tx, tx, command, unlocked, path } = self;
83
84        let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None };
85
86        if let Some(data) = data {
87            sig = Some(data);
88        }
89
90        let code = if let Some(SendTxSubcommands::Create {
91            code,
92            sig: constructor_sig,
93            args: constructor_args,
94        }) = command
95        {
96            // ensure we don't violate settings for transactions that can't be CREATE: 7702 and 4844
97            // which require mandatory target
98            if to.is_none() && !tx.auth.is_empty() {
99                return Err(eyre!(
100                    "EIP-7702 transactions can't be CREATE transactions and require a destination address"
101                ));
102            }
103            // ensure we don't violate settings for transactions that can't be CREATE: 7702 and 4844
104            // which require mandatory target
105            if to.is_none() && blob_data.is_some() {
106                return Err(eyre!(
107                    "EIP-4844 transactions can't be CREATE transactions and require a destination address"
108                ));
109            }
110
111            sig = constructor_sig;
112            args = constructor_args;
113            Some(code)
114        } else {
115            None
116        };
117
118        let config = send_tx.eth.load_config()?;
119        let provider = utils::get_provider(&config)?;
120
121        if let Some(interval) = send_tx.poll_interval {
122            provider.client().set_poll_interval(Duration::from_secs(interval))
123        }
124
125        let builder = CastTxBuilder::new(&provider, tx, &config)
126            .await?
127            .with_to(to)
128            .await?
129            .with_code_sig_and_args(code, sig, args)
130            .await?
131            .with_blob_data(blob_data)?;
132
133        let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout);
134
135        // Case 1:
136        // Default to sending via eth_sendTransaction if the --unlocked flag is passed.
137        // This should be the only way this RPC method is used as it requires a local node
138        // or remote RPC with unlocked accounts.
139        if unlocked && !send_tx.eth.wallet.browser {
140            // only check current chain id if it was specified in the config
141            if let Some(config_chain) = config.chain {
142                let current_chain_id = provider.get_chain_id().await?;
143                let config_chain_id = config_chain.id();
144                // switch chain if current chain id is not the same as the one specified in the
145                // config
146                if config_chain_id != current_chain_id {
147                    sh_warn!("Switching to chain {}", config_chain)?;
148                    provider
149                        .raw_request::<_, ()>(
150                            "wallet_switchEthereumChain".into(),
151                            [serde_json::json!({
152                                "chainId": format!("0x{:x}", config_chain_id),
153                            })],
154                        )
155                        .await?;
156                }
157            }
158
159            let (tx, _) = builder.build(config.sender).await?;
160
161            cast_send(
162                provider,
163                tx,
164                send_tx.cast_async,
165                send_tx.sync,
166                send_tx.confirmations,
167                timeout,
168            )
169            .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 = send_tx.eth.wallet.signer().await?;
177            let from = signer.address();
178
179            tx::validate_from_address(send_tx.eth.wallet.from, from)?;
180
181            // Browser wallets work differently as they sign and send the transaction in one step.
182            if send_tx.eth.wallet.browser
183                && let WalletSigner::Browser(ref browser_signer) = signer
184            {
185                let (tx_request, _) = builder.build(from).await?;
186                let tx_hash = browser_signer.send_transaction_via_browser(tx_request.inner).await?;
187
188                if send_tx.cast_async {
189                    sh_println!("{tx_hash:#x}")?;
190                } else {
191                    let receipt = CastTxSender::new(&provider)
192                        .receipt(
193                            format!("{tx_hash:#x}"),
194                            None,
195                            send_tx.confirmations,
196                            Some(timeout),
197                            false,
198                        )
199                        .await?;
200                    sh_println!("{receipt}")?;
201                }
202
203                return Ok(());
204            }
205
206            let (tx_request, _) = builder.build(&signer).await?;
207
208            let wallet = EthereumWallet::from(signer);
209            let provider = ProviderBuilder::<_, _, AnyNetwork>::default()
210                .wallet(wallet)
211                .connect_provider(&provider);
212
213            cast_send(
214                provider,
215                tx_request,
216                send_tx.cast_async,
217                send_tx.sync,
218                send_tx.confirmations,
219                timeout,
220            )
221            .await
222        }
223    }
224}
225
226pub(crate) async fn cast_send<P: Provider<AnyNetwork>>(
227    provider: P,
228    tx: WithOtherFields<TransactionRequest>,
229    cast_async: bool,
230    sync: bool,
231    confs: u64,
232    timeout: u64,
233) -> Result<()> {
234    let cast = CastTxSender::new(&provider);
235
236    if sync {
237        // Send transaction and wait for receipt synchronously
238        let receipt = cast.send_sync(tx).await?;
239        sh_println!("{receipt}")?;
240    } else {
241        let pending_tx = cast.send(tx).await?;
242        let tx_hash = pending_tx.inner().tx_hash();
243
244        if cast_async {
245            sh_println!("{tx_hash:#x}")?;
246        } else {
247            let receipt =
248                cast.receipt(format!("{tx_hash:#x}"), None, confs, Some(timeout), false).await?;
249            sh_println!("{receipt}")?;
250        }
251    }
252
253    Ok(())
254}