cast/cmd/
send.rs

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