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}")?;