Skip to main content

cast/cmd/
send.rs

1use std::{path::PathBuf, str::FromStr, time::Duration};
2
3use alloy_consensus::{SignableTransaction, Signed};
4use alloy_ens::NameOrAddress;
5use alloy_network::{AnyNetwork, EthereumWallet, Network};
6use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder};
7use alloy_signer::{Signature, Signer};
8use clap::Parser;
9use eyre::{Result, eyre};
10use foundry_cli::{opts::TransactionOpts, utils::LoadConfig};
11use foundry_common::{
12    fmt::{UIfmt, UIfmtReceiptExt},
13    provider::ProviderBuilder,
14};
15use foundry_primitives::FoundryTransactionBuilder;
16use tempo_alloy::TempoNetwork;
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) -> Result<()> {
86        if self.tx.tempo.is_tempo() {
87            self.run_generic::<TempoNetwork>().await
88        } else {
89            self.run_generic::<AnyNetwork>().await
90        }
91    }
92
93    pub async fn run_generic<N: Network>(self) -> Result<()>
94    where
95        N::TxEnvelope: From<Signed<N::UnsignedTx>>,
96        N::UnsignedTx: SignableTransaction<Signature>,
97        N::TransactionRequest: FoundryTransactionBuilder<N>,
98        N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
99    {
100        let Self { to, mut sig, mut args, data, send_tx, tx, command, unlocked, path } = self;
101
102        let print_sponsor_hash = tx.tempo.print_sponsor_hash;
103
104        let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None };
105
106        if let Some(data) = data {
107            sig = Some(data);
108        }
109
110        let code = if let Some(SendTxSubcommands::Create {
111            code,
112            sig: constructor_sig,
113            args: constructor_args,
114        }) = command
115        {
116            // ensure we don't violate settings for transactions that can't be CREATE: 7702 and 4844
117            // which require mandatory target
118            if to.is_none() && !tx.auth.is_empty() {
119                return Err(eyre!(
120                    "EIP-7702 transactions can't be CREATE transactions and require a destination address"
121                ));
122            }
123            // ensure we don't violate settings for transactions that can't be CREATE: 7702 and 4844
124            // which require mandatory target
125            if to.is_none() && blob_data.is_some() {
126                return Err(eyre!(
127                    "EIP-4844 transactions can't be CREATE transactions and require a destination address"
128                ));
129            }
130
131            sig = constructor_sig;
132            args = constructor_args;
133            Some(code)
134        } else {
135            None
136        };
137
138        let config = send_tx.eth.load_config()?;
139        let provider = ProviderBuilder::<N>::from_config(&config)?.build()?;
140
141        if let Some(interval) = send_tx.poll_interval {
142            provider.client().set_poll_interval(Duration::from_secs(interval))
143        }
144
145        let builder = CastTxBuilder::new(&provider, tx, &config)
146            .await?
147            .with_to(to)
148            .await?
149            .with_code_sig_and_args(code, sig, args)
150            .await?
151            .with_blob_data(blob_data)?;
152
153        // If --tempo.print-sponsor-hash was passed, build the tx, print the hash, and exit.
154        if print_sponsor_hash {
155            let from = send_tx.eth.wallet.from.unwrap_or(config.sender);
156            let (tx, _) = builder.build(from).await?;
157            let hash = tx
158                .compute_sponsor_hash(from)
159                .ok_or_else(|| eyre!("This network does not support sponsored transactions"))?;
160            sh_println!("{hash:?}")?;
161            return Ok(());
162        }
163
164        let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout);
165
166        // Launch browser signer if `--browser` flag is set
167        let browser = send_tx.browser.run::<N>().await?;
168
169        // Case 1:
170        // Default to sending via eth_sendTransaction if the --unlocked flag is passed.
171        // This should be the only way this RPC method is used as it requires a local node
172        // or remote RPC with unlocked accounts.
173        if unlocked && browser.is_none() {
174            // only check current chain id if it was specified in the config
175            if let Some(config_chain) = config.chain {
176                let current_chain_id = provider.get_chain_id().await?;
177                let config_chain_id = config_chain.id();
178                // switch chain if current chain id is not the same as the one specified in the
179                // config
180                if config_chain_id != current_chain_id {
181                    sh_warn!("Switching to chain {}", config_chain)?;
182                    provider
183                        .raw_request::<_, ()>(
184                            "wallet_switchEthereumChain".into(),
185                            [serde_json::json!({
186                                "chainId": format!("0x{:x}", config_chain_id),
187                            })],
188                        )
189                        .await?;
190                }
191            }
192
193            let (tx, _) = builder.build(config.sender).await?;
194
195            cast_send(
196                provider,
197                tx,
198                send_tx.cast_async,
199                send_tx.sync,
200                send_tx.confirmations,
201                timeout,
202            )
203            .await
204        // Case 2:
205        // Browser wallet signs and sends the transaction in one step.
206        } else if let Some(browser) = browser {
207            let (tx_request, _) = builder.build(browser.address()).await?;
208            let tx_hash = browser.send_transaction_via_browser(tx_request).await?;
209
210            let cast = CastTxSender::new(&provider);
211            cast.print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout).await
212        // Case 3:
213        // An option to use a local signer was provided.
214        // If we cannot successfully instantiate a local signer, then we will assume we don't have
215        // enough information to sign and we must bail.
216        } else {
217            let signer = send_tx.eth.wallet.signer().await?;
218            let from = signer.address();
219
220            tx::validate_from_address(send_tx.eth.wallet.from, from)?;
221
222            let (tx_request, _) = builder.build(&signer).await?;
223
224            let wallet = EthereumWallet::from(signer);
225            let provider = AlloyProviderBuilder::<_, _, N>::default()
226                .wallet(wallet)
227                .connect_provider(&provider);
228
229            cast_send(
230                provider,
231                tx_request,
232                send_tx.cast_async,
233                send_tx.sync,
234                send_tx.confirmations,
235                timeout,
236            )
237            .await
238        }
239    }
240}
241
242pub(crate) async fn cast_send<N: Network, P: Provider<N>>(
243    provider: P,
244    tx: N::TransactionRequest,
245    cast_async: bool,
246    sync: bool,
247    confs: u64,
248    timeout: u64,
249) -> Result<()>
250where
251    N::TransactionRequest: FoundryTransactionBuilder<N>,
252    N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
253{
254    let cast = CastTxSender::new(provider);
255
256    if sync {
257        // Send transaction and wait for receipt synchronously
258        let receipt = cast.send_sync(tx).await?;
259        sh_println!("{receipt}")?;
260    } else {
261        let pending_tx = cast.send(tx).await?;
262        let tx_hash = *pending_tx.inner().tx_hash();
263        cast.print_tx_result(tx_hash, cast_async, confs, timeout).await?;
264    }
265
266    Ok(())
267}