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