diff --git a/crates/cast/src/cmd/erc20.rs b/crates/cast/src/cmd/erc20.rs index bddc0939ed215..d0d2437ca49a3 100644 --- a/crates/cast/src/cmd/erc20.rs +++ b/crates/cast/src/cmd/erc20.rs @@ -1,7 +1,7 @@ use std::{str::FromStr, time::Duration}; use crate::{ - cmd::send::{cast_send, cast_send_with_access_key}, + cmd::send::{cast_send, cast_send_with_access_key, cast_send_with_browser_eip712}, format_uint_exp, tx::{CastTxSender, SendTxOpts, TxParams}, }; @@ -446,15 +446,27 @@ impl Erc20Subcommand { if let Some(sponsor) = &tempo_sponsor { sponsor.attach_and_print::(&mut tx, browser.address()).await?; } - let tx_hash = browser.send_transaction_via_browser(tx).await?; - CastTxSender::new(&$provider) - .print_tx_result( - tx_hash, + if chain.is_tempo() { + cast_send_with_browser_eip712( + &$provider, + tx, + &browser, $send_tx.cast_async, $send_tx.confirmations, timeout, ) .await? + } else { + let tx_hash = browser.send_transaction_via_browser(tx).await?; + CastTxSender::new(&$provider) + .print_tx_result( + tx_hash, + $send_tx.cast_async, + $send_tx.confirmations, + timeout, + ) + .await? + } } else { let signer = pre_resolved_signer.unwrap_or($send_tx.eth.wallet.signer().await?); let from = signer.address(); diff --git a/crates/cast/src/cmd/send.rs b/crates/cast/src/cmd/send.rs index 421aae1f2153e..13afe5a498bc7 100644 --- a/crates/cast/src/cmd/send.rs +++ b/crates/cast/src/cmd/send.rs @@ -18,7 +18,7 @@ use foundry_common::{ provider::ProviderBuilder, tempo::TEMPO_BROWSER_GAS_BUFFER, }; -use foundry_wallets::{TempoAccessKeyConfig, WalletSigner}; +use foundry_wallets::{TempoAccessKeyConfig, WalletSigner, wallet_browser::signer::BrowserSigner}; use tempo_alloy::TempoNetwork; use crate::{ @@ -302,10 +302,23 @@ impl SendTxArgs { sponsor.attach_and_print::(&mut tx_request, browser.address()).await?; } - let tx_hash = browser.send_transaction_via_browser(tx_request).await?; + if chain.is_tempo() { + cast_send_with_browser_eip712( + &provider, + tx_request, + &browser, + send_tx.cast_async, + send_tx.confirmations, + timeout, + ) + .await + } else { + let tx_hash = browser.send_transaction_via_browser(tx_request).await?; - let cast = CastTxSender::new(&provider); - cast.print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout).await + let cast = CastTxSender::new(&provider); + cast.print_tx_result(tx_hash, send_tx.cast_async, send_tx.confirmations, timeout) + .await + } // Case 3: // Tempo access key (keychain) signing. Uses `sign_with_access_key` which // handles the provisioning check and embeds `key_authorization` when needed. @@ -433,3 +446,24 @@ where let tx_hash = *provider.send_raw_transaction(&raw_tx).await?.tx_hash(); CastTxSender::new(provider).print_tx_result(tx_hash, cast_async, confirmations, timeout).await } + +/// Signs a Tempo transaction in the browser using EIP-712 typed data and submits it raw. +/// +/// This is used for Tempo because browser wallets cannot sign/send the custom `0x76` typed +/// transaction envelope directly. +pub(crate) async fn cast_send_with_browser_eip712>( + provider: &P, + tx: N::TransactionRequest, + browser: &BrowserSigner, + cast_async: bool, + confirmations: u64, + timeout: u64, +) -> Result<()> +where + N::TransactionRequest: FoundryTransactionBuilder, + N::ReceiptResponse: UIfmt + UIfmtReceiptExt, +{ + let raw_tx = tx.sign_with_browser_eip712(browser).await?; + let tx_hash = *provider.send_raw_transaction(&raw_tx).await?.tx_hash(); + CastTxSender::new(provider).print_tx_result(tx_hash, cast_async, confirmations, timeout).await +} diff --git a/crates/common/src/transactions/builder.rs b/crates/common/src/transactions/builder.rs index aa4c971680d00..1a81e4a99e3fc 100644 --- a/crates/common/src/transactions/builder.rs +++ b/crates/common/src/transactions/builder.rs @@ -2,6 +2,7 @@ use std::num::NonZeroU64; use alloy_consensus::{ BlobTransactionSidecar, BlobTransactionSidecarEip7594, BlobTransactionSidecarVariant, + transaction::SignerRecoverable, }; use alloy_eips::{Encodable2718, eip7702::SignedAuthorization}; use alloy_network::{AnyNetwork, Ethereum, Network, NetworkTransactionBuilder}; @@ -9,6 +10,7 @@ use alloy_primitives::{Address, B256, Signature, TxKind, U256}; use alloy_provider::Provider; use alloy_signer::Signer; use eyre::Result; +use foundry_wallets::wallet_browser::signer::BrowserSigner; #[cfg(feature = "optimism")] use op_alloy_network::Optimism; #[cfg(feature = "optimism")] @@ -295,6 +297,16 @@ pub trait FoundryTransactionBuilder: NetworkTransactionBuilder { ) -> impl Future>> + Send { async { eyre::bail!("access key signing is not supported for this network") } } + + /// Signs the transaction as a browser-wallet Tempo transaction using EIP-712 typed data. + /// + /// The default implementation returns an error. Only `TempoNetwork` supports this. + fn sign_with_browser_eip712( + self, + _browser: &BrowserSigner, + ) -> impl Future>> + Send { + async { eyre::bail!("browser EIP-712 signing is not supported for this network") } + } } impl FoundryTransactionBuilder for ::TransactionRequest { @@ -550,4 +562,34 @@ impl FoundryTransactionBuilder for ::Tran Ok(buf) } } + + fn sign_with_browser_eip712( + self, + browser: &BrowserSigner, + ) -> impl Future>> + Send { + async move { + let tempo_tx = self + .build_aa() + .map_err(|e| eyre::eyre!("failed to build Tempo AA transaction: {e}"))?; + let raw_sig = browser.sign_typed_data_v4(tempo_tx.eip712_typed_data()).await?; + let signature = Signature::try_from(raw_sig.as_ref()) + .map_err(|e| eyre::eyre!("failed to parse browser EIP-712 signature: {e}"))?; + let aa_signed = tempo_tx.into_signed(TempoSignature::Primitive( + PrimitiveSignature::Eip712Secp256k1(signature), + )); + let recovered = aa_signed + .recover_signer() + .map_err(|e| eyre::eyre!("failed to recover browser EIP-712 signer: {e}"))?; + if recovered != browser.address() { + eyre::bail!( + "browser EIP-712 signature recovered {recovered}, expected {}", + browser.address() + ); + } + + let mut buf = Vec::new(); + aa_signed.encode_2718(&mut buf); + Ok(buf) + } + } }