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::{Ethereum, EthereumWallet, Network};
6use alloy_primitives::Address;
7use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder};
8use alloy_signer::{Signature, Signer};
9use clap::Parser;
10use eyre::{Result, eyre};
11use foundry_cli::{opts::TransactionOpts, utils::LoadConfig};
12use foundry_common::{
13    FoundryTransactionBuilder,
14    fmt::{UIfmt, UIfmtReceiptExt},
15    provider::ProviderBuilder,
16};
17use foundry_wallets::{TempoAccessKeyConfig, WalletSigner};
18use tempo_alloy::TempoNetwork;
19
20use crate::{
21    cmd::tip20::iso4217_warning_message,
22    tx::{self, CastTxBuilder, CastTxSender, SendTxOpts},
23};
24use tempo_contracts::precompiles::{TIP20_FACTORY_ADDRESS, is_iso4217_currency};
25
26/// CLI arguments for `cast send`.
27#[derive(Debug, Parser)]
28pub struct SendTxArgs {
29    /// The destination of the transaction.
30    ///
31    /// If not provided, you must use cast send --create.
32    #[arg(value_parser = NameOrAddress::from_str)]
33    to: Option<NameOrAddress>,
34
35    /// The signature of the function to call.
36    sig: Option<String>,
37
38    /// The arguments of the function to call.
39    #[arg(allow_negative_numbers = true)]
40    args: Vec<String>,
41
42    /// Raw hex-encoded data for the transaction. Used instead of \[SIG\] and \[ARGS\].
43    #[arg(
44        long,
45        conflicts_with_all = &["sig", "args"]
46    )]
47    data: Option<String>,
48
49    #[command(flatten)]
50    send_tx: SendTxOpts,
51
52    #[command(subcommand)]
53    command: Option<SendTxSubcommands>,
54
55    /// Send via `eth_sendTransaction` using the `--from` argument or $ETH_FROM as sender
56    #[arg(long, requires = "from")]
57    unlocked: bool,
58
59    /// Skip confirmation prompts (e.g. non-ISO 4217 currency warnings).
60    #[arg(long)]
61    force: bool,
62
63    #[command(flatten)]
64    tx: TransactionOpts,
65
66    /// The path of blob data to be sent.
67    #[arg(
68        long,
69        value_name = "BLOB_DATA_PATH",
70        conflicts_with = "legacy",
71        requires = "blob",
72        help_heading = "Transaction options"
73    )]
74    path: Option<PathBuf>,
75}
76
77#[derive(Debug, Parser)]
78pub enum SendTxSubcommands {
79    /// Use to deploy raw contract bytecode.
80    #[command(name = "--create")]
81    Create {
82        /// The bytecode of the contract to deploy.
83        code: String,
84
85        /// The signature of the function to call.
86        sig: Option<String>,
87
88        /// The arguments of the function to call.
89        #[arg(allow_negative_numbers = true)]
90        args: Vec<String>,
91    },
92}
93
94impl SendTxArgs {
95    pub async fn run(self) -> Result<()> {
96        // Resolve the signer early so we know if it's a Tempo access key.
97        let (signer, tempo_access_key) = self.send_tx.eth.wallet.maybe_signer().await?;
98
99        if tempo_access_key.is_some() || self.tx.tempo.is_tempo() {
100            self.run_generic::<TempoNetwork>(signer, tempo_access_key).await
101        } else {
102            self.run_generic::<Ethereum>(signer, None).await
103        }
104    }
105
106    pub async fn run_generic<N: Network>(
107        self,
108        pre_resolved_signer: Option<WalletSigner>,
109        access_key: Option<TempoAccessKeyConfig>,
110    ) -> Result<()>
111    where
112        N::TxEnvelope: From<Signed<N::UnsignedTx>>,
113        N::UnsignedTx: SignableTransaction<Signature>,
114        N::TransactionRequest: FoundryTransactionBuilder<N>,
115        N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
116    {
117        let Self { to, mut sig, mut args, data, send_tx, mut tx, command, unlocked, force, path } =
118            self;
119
120        let print_sponsor_hash = tx.tempo.print_sponsor_hash;
121        let sponsor_signature = tx.tempo.sponsor_signature;
122
123        let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None };
124
125        if let Some(data) = data {
126            sig = Some(data);
127        }
128
129        let code = if let Some(SendTxSubcommands::Create {
130            code,
131            sig: constructor_sig,
132            args: constructor_args,
133        }) = command
134        {
135            // ensure we don't violate settings for transactions that can't be CREATE: 7702 and 4844
136            // which require mandatory target
137            if to.is_none() && !tx.auth.is_empty() {
138                return Err(eyre!(
139                    "EIP-7702 transactions can't be CREATE transactions and require a destination address"
140                ));
141            }
142            // ensure we don't violate settings for transactions that can't be CREATE: 7702 and 4844
143            // which require mandatory target
144            if to.is_none() && blob_data.is_some() {
145                return Err(eyre!(
146                    "EIP-4844 transactions can't be CREATE transactions and require a destination address"
147                ));
148            }
149
150            sig = constructor_sig;
151            args = constructor_args;
152            Some(code)
153        } else {
154            None
155        };
156
157        // Validate ISO 4217 currency code for TIP20Factory createToken calls.
158        if let Some(ref to_addr) = to {
159            let is_factory = match to_addr {
160                NameOrAddress::Address(addr) => *addr == TIP20_FACTORY_ADDRESS,
161                NameOrAddress::Name(name) => {
162                    Address::from_str(name).ok() == Some(TIP20_FACTORY_ADDRESS)
163                }
164            };
165
166            if !force
167                && is_factory
168                && let Some(ref sig_str) = sig
169                && sig_str.starts_with("createToken")
170                && let Some(currency) = args.get(2)
171                && !is_iso4217_currency(currency)
172            {
173                sh_warn!("{}", iso4217_warning_message(currency))?;
174                let response: String = foundry_common::prompt!("\nContinue anyway? [y/N] ")?;
175                if !matches!(response.trim(), "y" | "Y") {
176                    sh_println!("Aborted.")?;
177                    return Ok(());
178                }
179            }
180        }
181
182        let config = send_tx.eth.load_config()?;
183        let provider = ProviderBuilder::<N>::from_config(&config)?.build()?;
184
185        if let Some(interval) = send_tx.poll_interval {
186            provider.client().set_poll_interval(Duration::from_secs(interval))
187        }
188
189        // Inject access key ID into TempoOpts so it's set before gas estimation.
190        if let Some(ref ak) = access_key {
191            tx.tempo.key_id = Some(ak.key_address);
192        }
193
194        let builder = CastTxBuilder::new(&provider, tx, &config)
195            .await?
196            .with_to(to)
197            .await?
198            .with_code_sig_and_args(code, sig, args)
199            .await?
200            .with_blob_data(blob_data)?;
201
202        // If --tempo.print-sponsor-hash was passed, build the tx, print the hash, and exit.
203        if print_sponsor_hash {
204            // Use the pre-resolved signer to derive the actual sender address, since the
205            // sponsor hash commits to the sender.
206            let signer = pre_resolved_signer.as_ref().ok_or_else(|| {
207                eyre!("--tempo.print-sponsor-hash requires a signer (e.g. --private-key)")
208            })?;
209            let from = signer.address();
210            let (tx, _) = builder.build(from).await?;
211            let hash = tx
212                .compute_sponsor_hash(from)
213                .ok_or_else(|| eyre!("This network does not support sponsored transactions"))?;
214            sh_println!("{hash:?}")?;
215            return Ok(());
216        }
217
218        let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout);
219
220        // Launch browser signer if `--browser` flag is set
221        let browser = send_tx.browser.run::<N>().await?;
222
223        // Case 1:
224        // Default to sending via eth_sendTransaction if the --unlocked flag is passed.
225        // This should be the only way this RPC method is used as it requires a local node
226        // or remote RPC with unlocked accounts.
227        if unlocked && browser.is_none() {
228            // only check current chain id if it was specified in the config
229            if let Some(config_chain) = config.chain {
230                let current_chain_id = provider.get_chain_id().await?;
231                let config_chain_id = config_chain.id();
232                // switch chain if current chain id is not the same as the one specified in the
233                // config
234                if config_chain_id != current_chain_id {
235                    sh_warn!("Switching to chain {}", config_chain)?;
236                    provider
237                        .raw_request::<_, ()>(
238                            "wallet_switchEthereumChain".into(),
239                            [serde_json::json!({
240                                "chainId": format!("0x{:x}", config_chain_id),
241                            })],
242                        )
243                        .await?;
244                }
245            }
246
247            let (tx, _) = builder.build(config.sender).await?;
248
249            cast_send(
250                provider,
251                tx,
252                send_tx.cast_async,
253                send_tx.sync,
254                send_tx.confirmations,
255                timeout,
256            )
257            .await
258        // Case 2:
259        // Browser wallet signs and sends the transaction in one step.
260        } else if let Some(browser) = browser {
261            let (tx_request, _) = builder.build(browser.address()).await?;
262            let tx_hash = browser.send_transaction_via_browser(tx_request).await?;
263
264            let cast = CastTxSender::new(&provider);
265            cast.print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout).await
266        // Case 3:
267        // Tempo access key (keychain) signing. Uses `sign_with_access_key` which
268        // handles the provisioning check and embeds `key_authorization` when needed.
269        } else if let Some(ak) = access_key {
270            let signer = match pre_resolved_signer {
271                Some(s) => s,
272                None => send_tx.eth.wallet.signer().await?,
273            };
274            let from = ak.wallet_address;
275
276            let (tx_request, _) = builder.build(from).await?;
277
278            let raw_tx = tx_request
279                .sign_with_access_key(
280                    &provider,
281                    &signer,
282                    ak.wallet_address,
283                    ak.key_address,
284                    ak.key_authorization.as_ref(),
285                )
286                .await?;
287
288            let tx_hash = *provider.send_raw_transaction(&raw_tx).await?.tx_hash();
289
290            let cast = CastTxSender::new(&provider);
291            cast.print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout).await
292        // Case 4:
293        // An option to use a local signer was provided.
294        // If we cannot successfully instantiate a local signer, then we will assume we don't have
295        // enough information to sign and we must bail.
296        } else {
297            let signer = match pre_resolved_signer {
298                Some(s) => s,
299                None => send_tx.eth.wallet.signer().await?,
300            };
301            let from = signer.address();
302
303            tx::validate_from_address(send_tx.eth.wallet.from, from)?;
304
305            let (mut tx_request, _) = builder.build(&signer).await?;
306
307            // Apply sponsor signature after gas estimation so the estimate is
308            // consistent with what `--tempo.print-sponsor-hash` computes.
309            if let Some(sig) = sponsor_signature {
310                tx_request.set_fee_payer_signature(sig);
311            }
312
313            let wallet = EthereumWallet::from(signer);
314            let provider = AlloyProviderBuilder::<_, _, N>::default()
315                .wallet(wallet)
316                .connect_provider(&provider);
317
318            cast_send(
319                provider,
320                tx_request,
321                send_tx.cast_async,
322                send_tx.sync,
323                send_tx.confirmations,
324                timeout,
325            )
326            .await
327        }
328    }
329}
330
331pub(crate) async fn cast_send<N: Network, P: Provider<N>>(
332    provider: P,
333    tx: N::TransactionRequest,
334    cast_async: bool,
335    sync: bool,
336    confs: u64,
337    timeout: u64,
338) -> Result<()>
339where
340    N::TransactionRequest: FoundryTransactionBuilder<N>,
341    N::ReceiptResponse: UIfmt + UIfmtReceiptExt,
342{
343    let cast = CastTxSender::new(provider);
344
345    if sync {
346        // Send transaction and wait for receipt synchronously
347        let receipt = cast.send_sync(tx).await?;
348        sh_println!("{receipt}")?;
349    } else {
350        let pending_tx = cast.send(tx).await?;
351        let tx_hash = *pending_tx.inner().tx_hash();
352        cast.print_tx_result(tx_hash, cast_async, confs, timeout).await?;
353    }
354
355    Ok(())
356}