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