cast/cmd/
send.rs

1use std::{path::PathBuf, str::FromStr, time::Duration};
2
3use alloy_eips::Encodable2718;
4use alloy_ens::NameOrAddress;
5use alloy_network::{AnyNetwork, EthereumWallet, NetworkWallet};
6use alloy_provider::{Provider, ProviderBuilder};
7use alloy_rpc_types::TransactionRequest;
8use alloy_serde::WithOtherFields;
9use alloy_signer::Signer;
10use clap::Parser;
11use eyre::{Result, eyre};
12use foundry_cli::{
13    opts::TransactionOpts,
14    utils::{LoadConfig, get_provider_with_curl},
15};
16use foundry_wallets::WalletSigner;
17
18use crate::tx::{self, CastTxBuilder, CastTxSender, SendTxOpts};
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    #[arg(allow_negative_numbers = true)]
34    args: Vec<String>,
35
36    /// Raw hex-encoded data for the transaction. Used instead of \[SIG\] and \[ARGS\].
37    #[arg(
38        long,
39        conflicts_with_all = &["sig", "args"]
40    )]
41    data: Option<String>,
42
43    #[command(flatten)]
44    send_tx: SendTxOpts,
45
46    #[command(subcommand)]
47    command: Option<SendTxSubcommands>,
48
49    /// Send via `eth_sendTransaction` using the `--from` argument or $ETH_FROM as sender
50    #[arg(long, requires = "from")]
51    unlocked: bool,
52
53    #[command(flatten)]
54    tx: TransactionOpts,
55
56    /// The path of blob data to be sent.
57    #[arg(
58        long,
59        value_name = "BLOB_DATA_PATH",
60        conflicts_with = "legacy",
61        requires = "blob",
62        help_heading = "Transaction options"
63    )]
64    path: Option<PathBuf>,
65}
66
67#[derive(Debug, Parser)]
68pub enum SendTxSubcommands {
69    /// Use to deploy raw contract bytecode.
70    #[command(name = "--create")]
71    Create {
72        /// The bytecode of the contract to deploy.
73        code: String,
74
75        /// The signature of the function to call.
76        sig: Option<String>,
77
78        /// The arguments of the function to call.
79        #[arg(allow_negative_numbers = true)]
80        args: Vec<String>,
81    },
82}
83
84impl SendTxArgs {
85    pub async fn run(self) -> eyre::Result<()> {
86        let Self { to, mut sig, mut args, data, send_tx, tx, command, unlocked, path } = self;
87
88        let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None };
89
90        if let Some(data) = data {
91            sig = Some(data);
92        }
93
94        let code = if let Some(SendTxSubcommands::Create {
95            code,
96            sig: constructor_sig,
97            args: constructor_args,
98        }) = command
99        {
100            // ensure we don't violate settings for transactions that can't be CREATE: 7702 and 4844
101            // which require mandatory target
102            if to.is_none() && !tx.auth.is_empty() {
103                return Err(eyre!(
104                    "EIP-7702 transactions can't be CREATE transactions and require a destination address"
105                ));
106            }
107            // ensure we don't violate settings for transactions that can't be CREATE: 7702 and 4844
108            // which require mandatory target
109            if to.is_none() && blob_data.is_some() {
110                return Err(eyre!(
111                    "EIP-4844 transactions can't be CREATE transactions and require a destination address"
112                ));
113            }
114
115            sig = constructor_sig;
116            args = constructor_args;
117            Some(code)
118        } else {
119            None
120        };
121
122        let config = send_tx.eth.load_config()?;
123        let provider = get_provider_with_curl(&config, send_tx.eth.rpc.curl)?;
124
125        if let Some(interval) = send_tx.poll_interval {
126            provider.client().set_poll_interval(Duration::from_secs(interval))
127        }
128
129        let builder = CastTxBuilder::new(&provider, tx, &config)
130            .await?
131            .with_to(to)
132            .await?
133            .with_code_sig_and_args(code, sig, args)
134            .await?
135            .with_blob_data(blob_data)?;
136
137        let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout);
138
139        // Check if this is a Tempo transaction - requires special handling for local signing
140        let is_tempo = builder.is_tempo();
141
142        // Tempo transactions with browser wallets are not supported
143        if is_tempo && send_tx.eth.wallet.browser {
144            return Err(eyre!("Tempo transactions are not supported with browser wallets."));
145        }
146
147        // Case 1:
148        // Default to sending via eth_sendTransaction if the --unlocked flag is passed.
149        // This should be the only way this RPC method is used as it requires a local node
150        // or remote RPC with unlocked accounts.
151        if unlocked && !send_tx.eth.wallet.browser {
152            // only check current chain id if it was specified in the config
153            if let Some(config_chain) = config.chain {
154                let current_chain_id = provider.get_chain_id().await?;
155                let config_chain_id = config_chain.id();
156                // switch chain if current chain id is not the same as the one specified in the
157                // config
158                if config_chain_id != current_chain_id {
159                    sh_warn!("Switching to chain {}", config_chain)?;
160                    provider
161                        .raw_request::<_, ()>(
162                            "wallet_switchEthereumChain".into(),
163                            [serde_json::json!({
164                                "chainId": format!("0x{:x}", config_chain_id),
165                            })],
166                        )
167                        .await?;
168                }
169            }
170
171            let (tx, _) = builder.build(config.sender).await?;
172
173            cast_send(
174                provider,
175                tx.into_inner(),
176                send_tx.cast_async,
177                send_tx.sync,
178                send_tx.confirmations,
179                timeout,
180            )
181            .await
182        // Case 2:
183        // An option to use a local signer was provided.
184        // If we cannot successfully instantiate a local signer, then we will assume we don't have
185        // enough information to sign and we must bail.
186        } else {
187            // Retrieve the signer, and bail if it can't be constructed.
188            let signer = send_tx.eth.wallet.signer().await?;
189            let from = signer.address();
190
191            tx::validate_from_address(send_tx.eth.wallet.from, from)?;
192
193            // Browser wallets work differently as they sign and send the transaction in one step.
194            if send_tx.eth.wallet.browser
195                && let WalletSigner::Browser(ref browser_signer) = signer
196            {
197                let (tx_request, _) = builder.build(from).await?;
198                let tx_hash = browser_signer
199                    .send_transaction_via_browser(tx_request.into_inner().inner)
200                    .await?;
201
202                if send_tx.cast_async {
203                    sh_println!("{tx_hash:#x}")?;
204                } else {
205                    let receipt = CastTxSender::new(&provider)
206                        .receipt(
207                            format!("{tx_hash:#x}"),
208                            None,
209                            send_tx.confirmations,
210                            Some(timeout),
211                            false,
212                        )
213                        .await?;
214                    sh_println!("{receipt}")?;
215                }
216
217                return Ok(());
218            }
219
220            // Tempo transactions need to be signed locally and sent as raw transactions
221            // because EthereumWallet doesn't understand type 0x76
222            // TODO(onbjerg): All of this is a side effect of a few things, most notably that we do
223            // not use `FoundryNetwork` and `FoundryTransactionRequest` everywhere, which is
224            // downstream of the fact that we use `EthereumWallet` everywhere.
225            if is_tempo {
226                let (ftx, _) = builder.build(&signer).await?;
227
228                // Sign using NetworkWallet<FoundryNetwork>
229                let signed_tx = signer.sign_request(ftx).await?;
230
231                // Encode and send raw
232                let mut raw_tx = Vec::with_capacity(signed_tx.encode_2718_len());
233                signed_tx.encode_2718(&mut raw_tx);
234
235                let cast = CastTxSender::new(&provider);
236                let pending_tx = cast.send_raw(&raw_tx).await?;
237                let tx_hash = pending_tx.inner().tx_hash();
238
239                if send_tx.cast_async {
240                    sh_println!("{tx_hash:#x}")?;
241                } else if send_tx.sync {
242                    // For sync mode, we already have the hash, just wait for receipt
243                    let receipt = cast
244                        .receipt(
245                            format!("{tx_hash:#x}"),
246                            None,
247                            send_tx.confirmations,
248                            Some(timeout),
249                            false,
250                        )
251                        .await?;
252                    sh_println!("{receipt}")?;
253                } else {
254                    let receipt = cast
255                        .receipt(
256                            format!("{tx_hash:#x}"),
257                            None,
258                            send_tx.confirmations,
259                            Some(timeout),
260                            false,
261                        )
262                        .await?;
263                    sh_println!("{receipt}")?;
264                }
265
266                return Ok(());
267            }
268
269            let (tx_request, _) = builder.build(&signer).await?;
270
271            let wallet = EthereumWallet::from(signer);
272            let provider = ProviderBuilder::<_, _, AnyNetwork>::default()
273                .wallet(wallet)
274                .connect_provider(&provider);
275
276            cast_send(
277                provider,
278                tx_request.into_inner(),
279                send_tx.cast_async,
280                send_tx.sync,
281                send_tx.confirmations,
282                timeout,
283            )
284            .await
285        }
286    }
287}
288
289pub(crate) async fn cast_send<P: Provider<AnyNetwork>>(
290    provider: P,
291    tx: WithOtherFields<TransactionRequest>,
292    cast_async: bool,
293    sync: bool,
294    confs: u64,
295    timeout: u64,
296) -> Result<()> {
297    let cast = CastTxSender::new(&provider);
298
299    if sync {
300        // Send transaction and wait for receipt synchronously
301        let receipt = cast.send_sync(tx).await?;
302        sh_println!("{receipt}")?;
303    } else {
304        let pending_tx = cast.send(tx).await?;
305        let tx_hash = pending_tx.inner().tx_hash();
306
307        if cast_async {
308            sh_println!("{tx_hash:#x}")?;
309        } else {
310            let receipt =
311                cast.receipt(format!("{tx_hash:#x}"), None, confs, Some(timeout), false).await?;
312            sh_println!("{receipt}")?;
313        }
314    }
315
316    Ok(())
317}