diff --git a/crates/cast/src/cmd/keychain.rs b/crates/cast/src/cmd/keychain.rs index eb45bd62bdae7..45bd1b78730f7 100644 --- a/crates/cast/src/cmd/keychain.rs +++ b/crates/cast/src/cmd/keychain.rs @@ -3758,6 +3758,28 @@ where } } +/// Fails early with `requirement` when a Tempo precompile is not active yet: a pre-fork call +/// would succeed as a silent no-op instead of reverting. Prefers the hardfork query and falls +/// back to checking the precompile's code when the RPC lacks the method. +pub(crate) async fn ensure_tempo_precompile_active

( + provider: &P, + hardfork: TempoHardfork, + precompile: Address, + requirement: &str, +) -> Result<()> +where + P: Provider, +{ + let active = match is_tempo_hardfork_active(provider, hardfork).await { + Ok(active) => active, + Err(_) => !provider.get_code_at(precompile).await?.is_empty(), + }; + if !active { + eyre::bail!("{requirement}"); + } + Ok(()) +} + async fn anvil_tempo_hardfork_active

( provider: &P, hardfork: TempoHardfork, diff --git a/crates/cast/src/cmd/receive_policy.rs b/crates/cast/src/cmd/receive_policy.rs index 0b58a292e0770..d1d76bfe4e5aa 100644 --- a/crates/cast/src/cmd/receive_policy.rs +++ b/crates/cast/src/cmd/receive_policy.rs @@ -1,8 +1,9 @@ use crate::{ cmd::{ - keychain::is_tempo_hardfork_active, + keychain::ensure_tempo_precompile_active, tip20::{resolve_tip20_signer, send_tip20_transaction}, }, + tempo::print_payload, tx::{SendTxOpts, TxParams}, }; use alloy_ens::NameOrAddress; @@ -399,16 +400,13 @@ async fn ensure_receive_policy_t6

(provider: &P, command: &str) -> Result<()> where P: Provider, { - // Prefer the hardfork query, but if it is unavailable (e.g. an older RPC without the method) - // fall back to checking whether the guard precompile has code. - let active = match is_tempo_hardfork_active(provider, TempoHardfork::T6).await { - Ok(active) => active, - Err(_) => !provider.get_code_at(RECEIVE_POLICY_GUARD_ADDRESS).await?.is_empty(), - }; - if !active { - eyre::bail!("{command} requires a Tempo T6-capable ReceivePolicy RPC"); - } - Ok(()) + ensure_tempo_precompile_active( + provider, + TempoHardfork::T6, + RECEIVE_POLICY_GUARD_ADDRESS, + &format!("{command} requires a Tempo T6-capable ReceivePolicy RPC"), + ) + .await } async fn burn_receipt(receipt: Bytes, send_tx: SendTxOpts, tx: TxParams) -> Result<()> { @@ -624,18 +622,6 @@ fn print_claim_hint(payload: &Value) -> Result<()> { } } -fn print_payload(payload: Value, human: F) -> Result<()> -where - F: FnOnce(&Value) -> Result<()>, -{ - if shell::is_json() { - print_json_success(payload)?; - } else { - human(&payload)?; - } - Ok(()) -} - fn recovery_mode(recovery_authority: Address) -> &'static str { if recovery_authority == Address::ZERO { "originator" } else { "authority" } } diff --git a/crates/cast/src/cmd/storage_credits.rs b/crates/cast/src/cmd/storage_credits.rs index 58c3e7880c1f0..effc808b833d4 100644 --- a/crates/cast/src/cmd/storage_credits.rs +++ b/crates/cast/src/cmd/storage_credits.rs @@ -1,17 +1,18 @@ use crate::{ cmd::{ - keychain::is_tempo_hardfork_active, + keychain::ensure_tempo_precompile_active, tip20::{resolve_tip20_signer, send_tip20_transaction}, }, + tempo::print_payload, tx::{SendTxOpts, TxParams}, }; use alloy_ens::NameOrAddress; use clap::{Parser, ValueEnum}; use eyre::Result; -use foundry_cli::{json::print_json_success, opts::RpcOpts, utils::LoadConfig}; -use foundry_common::{provider::ProviderBuilder, shell}; +use foundry_cli::{opts::RpcOpts, utils::LoadConfig}; +use foundry_common::provider::ProviderBuilder; use foundry_evm::hardfork::TempoHardfork; -use serde_json::{Value, json}; +use serde_json::json; use std::str::FromStr; use tempo_alloy::TempoNetwork; use tempo_contracts::precompiles::{IStorageCredits, STORAGE_CREDITS_ADDRESS}; @@ -206,14 +207,13 @@ async fn ensure_storage_credits_t7

(provider: &P, command: &str) -> Result<()> where P: alloy_provider::Provider, { - let active = match is_tempo_hardfork_active(provider, TempoHardfork::T7).await { - Ok(active) => active, - Err(_) => !provider.get_code_at(STORAGE_CREDITS_ADDRESS).await?.is_empty(), - }; - if !active { - eyre::bail!("{command} requires a Tempo T7-capable StorageCredits RPC"); - } - Ok(()) + ensure_tempo_precompile_active( + provider, + TempoHardfork::T7, + STORAGE_CREDITS_ADDRESS, + &format!("{command} requires a Tempo T7-capable StorageCredits RPC"), + ) + .await } /// Gate a write command on T7 before signing: on pre-T7 the precompile address is an empty account, @@ -224,18 +224,6 @@ async fn ensure_send_storage_credits_t7(send_tx: &SendTxOpts, command: &str) -> ensure_storage_credits_t7(&provider, command).await } -fn print_payload(payload: Value, human: F) -> Result<()> -where - F: FnOnce(&Value) -> Result<()>, -{ - if shell::is_json() { - print_json_success(payload)?; - } else { - human(&payload)?; - } - Ok(()) -} - impl CreditMode { const fn to_sol(self) -> IStorageCredits::Mode { match self { diff --git a/crates/cast/src/cmd/tip20/mod.rs b/crates/cast/src/cmd/tip20/mod.rs index d8860b3d5cdd0..c94f780dc726b 100644 --- a/crates/cast/src/cmd/tip20/mod.rs +++ b/crates/cast/src/cmd/tip20/mod.rs @@ -290,14 +290,14 @@ pub(crate) async fn send_tip20_transaction( tx.set_gas_limit(gas + TEMPO_BROWSER_GAS_BUFFER); } if let Some(sponsor) = &tempo_sponsor { - sponsor - .resolve_and_set_fee_token( - (!config.eth_rpc_curl).then_some(&provider), - Some(chain), - &mut tx, - ) - .await?; - sponsor.attach_and_print::(&mut tx, browser.address()).await?; + attach_sponsor( + sponsor, + (!config.eth_rpc_curl).then_some(&provider), + chain, + &mut tx, + browser.address(), + ) + .await?; } else { let fee_token = resolve_and_set_fee_token( (!config.eth_rpc_curl).then_some(&provider), @@ -319,14 +319,14 @@ pub(crate) async fn send_tip20_transaction( let (mut tx, _) = builder.build_with_access_key(ak.wallet_address, &ak).await?; maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; if let Some(sponsor) = &tempo_sponsor { - sponsor - .resolve_and_set_fee_token( - (!config.eth_rpc_curl).then_some(&provider), - Some(chain), - &mut tx, - ) - .await?; - sponsor.attach_and_print::(&mut tx, ak.wallet_address).await?; + attach_sponsor( + sponsor, + (!config.eth_rpc_curl).then_some(&provider), + chain, + &mut tx, + ak.wallet_address, + ) + .await?; } cast_send_with_access_key( &provider, @@ -342,12 +342,7 @@ pub(crate) async fn send_tip20_transaction( ) .await?; } else if let Some(sponsor_url) = sponsor_url { - let signer = match pre_resolved_signer { - Some(signer) => signer, - None => send_tx.eth.wallet.signer().await?, - }; - let from = signer.address(); - crate::tx::validate_from_address(send_tx.eth.wallet.from, from)?; + let (signer, _) = resolve_send_signer(pre_resolved_signer, &send_tx.eth).await?; let (mut tx, _) = builder.build(&signer).await?; maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; @@ -376,24 +371,19 @@ pub(crate) async fn send_tip20_transaction( ) .await?; } else { - let signer = match pre_resolved_signer { - Some(signer) => signer, - None => send_tx.eth.wallet.signer().await?, - }; - let from = signer.address(); - crate::tx::validate_from_address(send_tx.eth.wallet.from, from)?; + let (signer, from) = resolve_send_signer(pre_resolved_signer, &send_tx.eth).await?; let (mut tx, _) = builder.build(&signer).await?; maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?; if let Some(sponsor) = &tempo_sponsor { - sponsor - .resolve_and_set_fee_token( - (!config.eth_rpc_curl).then_some(&provider), - Some(chain), - &mut tx, - ) - .await?; - sponsor.attach_and_print::(&mut tx, from).await?; + attach_sponsor( + sponsor, + (!config.eth_rpc_curl).then_some(&provider), + chain, + &mut tx, + from, + ) + .await?; } let wallet = EthereumWallet::from(signer); @@ -417,6 +407,43 @@ pub(crate) async fn send_tip20_transaction( Ok(()) } +/// Resolves the sending signer, falling back to the wallet options, and validates it against +/// an explicit `--from`. +async fn resolve_send_signer( + pre_resolved: Option, + eth: &foundry_cli::opts::EthereumOpts, +) -> eyre::Result<(WalletSigner, Address)> { + let signer = match pre_resolved { + Some(signer) => signer, + None => eth.wallet.signer().await?, + }; + let from = signer.address(); + crate::tx::validate_from_address(eth.wallet.from, from)?; + Ok((signer, from)) +} + +/// Resolves the sponsored fee token and attaches the sponsor signature preview for `payer`. +async fn attach_sponsor

( + sponsor: &crate::tempo::TempoSponsor, + provider: Option<&P>, + chain: foundry_config::Chain, + tx: &mut ::TransactionRequest, + payer: Address, +) -> eyre::Result<()> +where + P: Provider, +{ + sponsor + .resolve_and_set_fee_token( + provider.map(|p| p as &dyn Provider), + Some(chain), + tx, + ) + .await?; + sponsor.attach_and_print::(tx, payer).await?; + Ok(()) +} + impl TxParams { fn into_transaction_opts(self) -> TransactionOpts { TransactionOpts { diff --git a/crates/cast/src/cmd/tip403.rs b/crates/cast/src/cmd/tip403.rs index 1d61d3780d561..3af30ce81b0ce 100644 --- a/crates/cast/src/cmd/tip403.rs +++ b/crates/cast/src/cmd/tip403.rs @@ -1,5 +1,6 @@ use crate::{ cmd::tip20::{resolve_tip20_signer, send_tip20_transaction}, + tempo::print_payload, tx::{SendTxOpts, TxParams}, }; use alloy_ens::NameOrAddress; @@ -7,12 +8,10 @@ use alloy_primitives::Address; use clap::{Parser, ValueEnum}; use eyre::Result; use foundry_cli::{ - json::print_json_success, opts::RpcOpts, utils::{LoadConfig, get_provider}, }; -use foundry_common::shell; -use serde_json::{Value, json}; +use serde_json::json; use std::str::FromStr; use tempo_contracts::precompiles::{ITIP403Registry, TIP403_REGISTRY_ADDRESS}; use tempo_primitives::TempoAddressExt; @@ -307,18 +306,6 @@ fn address_array(accounts: &[Address]) -> String { format!("[{}]", accounts.iter().map(Address::to_string).collect::>().join(",")) } -fn print_payload(payload: Value, human: F) -> Result<()> -where - F: FnOnce(&Value) -> Result<()>, -{ - if shell::is_json() { - print_json_success(payload)?; - } else { - human(&payload)?; - } - Ok(()) -} - impl PolicyKind { const fn to_sol(self) -> ITIP403Registry::PolicyType { match self { diff --git a/crates/cast/src/tempo.rs b/crates/cast/src/tempo.rs index 011532d1c8fb8..223b8af2c6cea 100644 --- a/crates/cast/src/tempo.rs +++ b/crates/cast/src/tempo.rs @@ -4,14 +4,28 @@ use crate::tx::fill_transaction_gas_fees; use alloy_network::{Network, TransactionBuilder}; use alloy_provider::Provider; use eyre::Result; -use foundry_cli::opts::TempoOpts; -use foundry_common::FoundryTransactionBuilder; +use foundry_cli::{json::print_json_success, opts::TempoOpts}; +use foundry_common::{FoundryTransactionBuilder, shell}; use foundry_config::{Chain, Eip1559FeeEstimatePreset}; use foundry_wallets::{TempoAccessKeyConfig, WalletOpts, WalletSigner}; +use serde_json::Value; use tempo_alloy::TempoNetwork; pub use foundry_common::tempo::{TempoSponsor, TempoSponsorPreview, resolve_tempo_sponsor_signer}; +/// Prints a command result: the raw payload in JSON mode, the human rendering otherwise. +pub(crate) fn print_payload(payload: Value, human: F) -> Result<()> +where + F: FnOnce(&Value) -> Result<()>, +{ + if shell::is_json() { + print_json_success(payload)?; + } else { + human(&payload)?; + } + Ok(()) +} + pub(crate) fn print_expires(expires_at: Option) -> Result<()> { if let Some(ts) = expires_at { sh_status!("Transaction expires at unix timestamp {ts}")?;