Skip to main content

foundry_common/tempo/
mod.rs

1//! Tempo network utilities.
2
3pub mod auth;
4
5use crate::FoundryTransactionBuilder;
6use alloy_chains::Chain;
7use alloy_network::Network;
8use alloy_primitives::{Address, B256, Signature, address};
9use alloy_signer::Signer;
10use eyre::{Context, Result};
11use foundry_wallets::{RawWalletOpts, WalletOpts, WalletSigner};
12use std::sync::Arc;
13use tempo_alloy::contracts::precompiles::DEFAULT_FEE_TOKEN;
14pub use tempo_alloy::contracts::precompiles::PATH_USD_ADDRESS;
15
16mod keystore;
17mod registry;
18mod session;
19mod session_policy;
20#[cfg(test)]
21mod test_utils;
22mod tip20;
23
24pub(crate) use auth::is_known_tempo_endpoint;
25pub use auth::{AccessKeyOutcome, EnsureAccessKeyConfig, ensure_access_key};
26pub use keystore::*;
27pub use session::*;
28pub use session_policy::{
29    GeneratedSessionKey, PreparedSessionAuthorization, SessionAuthorizationRequest,
30    SessionSpendLimit,
31};
32pub use tip20::{
33    TIP20_ALLOWED_LOGO_URI_SCHEMES, TIP20_MAX_LOGO_URI_BYTES, Tip20LogoUriValidationError,
34    validate_tip20_logo_uri,
35};
36
37#[cfg(test)]
38pub(crate) use test_utils::{test_env_mutex, with_tempo_home};
39
40#[cfg(test)]
41mod tests;
42
43/// Placeholder rendered by `Debug` impls in place of secret key material.
44fn redacted_debug(value: &str) -> &'static str {
45    if value.trim().is_empty() { "<empty>" } else { "<redacted>" }
46}
47
48/// Conservative gas buffer for browser wallet transactions on Tempo chains.
49///
50/// Browser wallets may sign with P256 or WebAuthn instead of secp256k1, which costs more gas
51/// for signature verification. Since we can't determine the signature type before signing,
52/// we add the worst-case (WebAuthn) overhead:
53///   - P256: +5,000 gas (P256 precompile cost minus ecrecover savings)
54///   - WebAuthn: ~6,500 gas (P256 cost + calldata for webauthn_data)
55///
56/// See <https://github.com/tempoxyz/tempo/blob/6ebf1a8/crates/revm/src/handler.rs#L108-L124>
57pub const TEMPO_BROWSER_GAS_BUFFER: u64 = 7_000;
58
59/// Reserved Tempo TIP20 fee-token addresses created during Foundry genesis.
60///
61/// Unlike [`PATH_USD_ADDRESS`], these tokens are not defined by the canonical
62/// `tempo-contracts` crate; they only exist in Foundry's local genesis setup, so
63/// they are defined here as the single source of truth and re-exported elsewhere.
64pub const ALPHA_USD_ADDRESS: Address = address!("0x20C0000000000000000000000000000000000001");
65pub const BETA_USD_ADDRESS: Address = address!("0x20C0000000000000000000000000000000000002");
66pub const THETA_USD_ADDRESS: Address = address!("0x20C0000000000000000000000000000000000003");
67
68/// Resolves an explicit Tempo fee token or the canonical default for a known Tempo network.
69pub fn resolve_fee_token(
70    chain: Option<Chain>,
71    explicit_fee_token: Option<Address>,
72) -> Option<Address> {
73    explicit_fee_token.or_else(|| chain.is_some_and(Chain::is_tempo).then_some(DEFAULT_FEE_TOKEN))
74}
75
76/// Returns the known symbol for a Tempo fee token without making an RPC call.
77pub const fn known_fee_token_symbol(fee_token: Address) -> Option<&'static str> {
78    match fee_token {
79        PATH_USD_ADDRESS => Some("PathUSD"),
80        ALPHA_USD_ADDRESS => Some("AlphaUSD"),
81        BETA_USD_ADDRESS => Some("BetaUSD"),
82        THETA_USD_ADDRESS => Some("ThetaUSD"),
83        _ => None,
84    }
85}
86
87/// Formats a Tempo fee-token selection for command output.
88pub fn format_fee_token_selection(fee_token: Address) -> String {
89    match known_fee_token_symbol(fee_token) {
90        Some(symbol) => format!("Paying gas in {symbol} ({fee_token})"),
91        None => format!("Paying gas in {fee_token}"),
92    }
93}
94
95/// Prints the selected Tempo fee token when one is set.
96pub fn print_fee_token_selection(fee_token: Option<Address>) -> Result<()> {
97    if let Some(fee_token) = fee_token {
98        sh_status!("{}", format_fee_token_selection(fee_token))?;
99    }
100    Ok(())
101}
102
103/// Prints the fee token selected for display, resolving the chain default without mutating a
104/// transaction request.
105pub fn print_resolved_fee_token_selection(
106    chain: Option<Chain>,
107    fee_token: Option<Address>,
108) -> Result<()> {
109    print_fee_token_selection(resolve_fee_token(chain, fee_token))
110}
111
112/// Gas sponsor configuration for Tempo fee-payer signatures.
113#[derive(Clone, Debug)]
114pub struct TempoSponsor {
115    sponsor: Address,
116    signer: Option<Arc<WalletSigner>>,
117    signature: Option<Signature>,
118}
119
120impl TempoSponsor {
121    pub const fn new(
122        sponsor: Address,
123        signer: Option<Arc<WalletSigner>>,
124        signature: Option<Signature>,
125    ) -> Self {
126        Self { sponsor, signer, signature }
127    }
128
129    pub const fn sponsor(&self) -> Address {
130        self.sponsor
131    }
132
133    pub async fn attach_and_print<N: Network>(
134        &self,
135        tx: &mut N::TransactionRequest,
136        sender: Address,
137    ) -> Result<TempoSponsorPreview>
138    where
139        N::TransactionRequest: FoundryTransactionBuilder<N>,
140    {
141        if self.sponsor == sender {
142            eyre::bail!(
143                "invalid Tempo sponsorship: sponsor {} must not equal transaction sender",
144                self.sponsor
145            );
146        }
147
148        let digest = tx.compute_sponsor_hash(sender).ok_or_else(|| {
149            eyre::eyre!(
150                "failed to compute Tempo sponsor digest; make sure this is a complete Tempo AA transaction"
151            )
152        })?;
153
154        let preview = TempoSponsorPreview {
155            sponsor: self.sponsor,
156            fee_token: tx.fee_token(),
157            valid_before: tx.valid_before().map(|v| v.get()),
158            valid_after: tx.valid_after().map(|v| v.get()),
159            digest,
160        };
161        preview.print()?;
162
163        let signature = if let Some(signature) = self.signature {
164            signature
165        } else if let Some(signer) = &self.signer {
166            signer.sign_hash(&digest).await.context("failed to sign Tempo sponsor digest")?
167        } else {
168            eyre::bail!("missing Tempo sponsor signature or signer")
169        };
170
171        let recovered = signature
172            .recover_address_from_prehash(&digest)
173            .context("failed to recover Tempo sponsor signature")?;
174        if recovered != self.sponsor {
175            eyre::bail!("Tempo sponsor signature recovered {recovered}, expected {}", self.sponsor);
176        }
177        if recovered == sender {
178            eyre::bail!(
179                "invalid Tempo sponsorship: recovered fee payer {recovered} must not equal transaction sender"
180            );
181        }
182
183        tx.set_fee_payer_signature(signature);
184        Ok(preview)
185    }
186}
187
188/// User-visible sponsor digest metadata for a single outgoing Tempo transaction.
189#[derive(Clone, Copy, Debug, PartialEq, Eq)]
190pub struct TempoSponsorPreview {
191    pub sponsor: Address,
192    pub fee_token: Option<Address>,
193    pub valid_before: Option<u64>,
194    pub valid_after: Option<u64>,
195    pub digest: B256,
196}
197
198impl TempoSponsorPreview {
199    pub fn print(&self) -> Result<()> {
200        crate::sh_eprintln!("Tempo sponsor: {}", self.sponsor)?;
201        crate::sh_eprintln!(
202            "Tempo fee token: {}",
203            self.fee_token.map_or_else(|| "network default".to_string(), |addr| addr.to_string())
204        )?;
205        crate::sh_eprintln!(
206            "Tempo validity: after {}, before {}",
207            self.valid_after.map_or_else(|| "none".to_string(), |v| v.to_string()),
208            self.valid_before.map_or_else(|| "none".to_string(), |v| v.to_string())
209        )?;
210        crate::sh_eprintln!("Tempo sponsor digest: {:?}", self.digest)?;
211        Ok(())
212    }
213}
214
215/// Resolves a `--tempo.sponsor-signer` URI into a Foundry wallet signer.
216pub async fn resolve_tempo_sponsor_signer(spec: &str) -> Result<WalletSigner> {
217    let spec = spec.trim();
218    let (scheme, value) = spec
219        .split_once("://")
220        .map(|(scheme, value)| (scheme.to_ascii_lowercase(), value))
221        .unwrap_or_else(|| (spec.to_ascii_lowercase(), ""));
222
223    match scheme.as_str() {
224        "env" => {
225            if value.is_empty() {
226                eyre::bail!("env:// sponsor signer requires an environment variable name");
227            }
228            let private_key = std::env::var(value)
229                .wrap_err_with(|| format!("{value} environment variable is required"))?;
230            foundry_wallets::utils::create_private_key_signer(&private_key)
231        }
232        "private-key" => {
233            if value.is_empty() {
234                eyre::bail!("private-key:// sponsor signer requires a private key");
235            }
236            foundry_wallets::utils::create_private_key_signer(value)
237        }
238        "keystore" => {
239            if value.is_empty() {
240                eyre::bail!("keystore:// sponsor signer requires a keystore path");
241            }
242            WalletOpts { keystore_path: Some(value.to_string()), ..Default::default() }
243                .signer()
244                .await
245        }
246        "account" => {
247            if value.is_empty() {
248                eyre::bail!("account:// sponsor signer requires an account name");
249            }
250            WalletOpts { keystore_account_name: Some(value.to_string()), ..Default::default() }
251                .signer()
252                .await
253        }
254        "ledger" => {
255            let raw = RawWalletOpts {
256                hd_path: (!value.is_empty()).then(|| value.to_string()),
257                ..Default::default()
258            };
259            WalletOpts { ledger: true, raw, ..Default::default() }.signer().await
260        }
261        "trezor" => {
262            let raw = RawWalletOpts {
263                hd_path: (!value.is_empty()).then(|| value.to_string()),
264                ..Default::default()
265            };
266            WalletOpts { trezor: true, raw, ..Default::default() }.signer().await
267        }
268        "aws" => WalletOpts { aws: true, ..Default::default() }.signer().await,
269        "gcp" => WalletOpts { gcp: true, ..Default::default() }.signer().await,
270        "turnkey" => WalletOpts { turnkey: true, ..Default::default() }.signer().await,
271        "browser" => {
272            eyre::bail!(
273                "browser:// sponsor signing is not supported by the current browser wallet API; use --tempo.sponsor-sig or another sponsor signer"
274            )
275        }
276        _ => eyre::bail!(
277            "unsupported Tempo sponsor signer `{spec}`; expected env://VAR, keystore://PATH, account://NAME, ledger://, trezor://, aws://, gcp://, turnkey://, or private-key://KEY"
278        ),
279    }
280}