Skip to main content

foundry_common/tempo/
mod.rs

1//! Tempo network utilities.
2
3pub mod auth;
4
5use crate::FoundryTransactionBuilder;
6use alloy_network::Network;
7use alloy_primitives::{Address, B256, Signature};
8use alloy_signer::Signer;
9use eyre::{Context, Result};
10use foundry_wallets::{RawWalletOpts, WalletOpts, WalletSigner};
11use std::sync::Arc;
12
13mod keystore;
14mod registry;
15mod session;
16#[cfg(test)]
17mod test_utils;
18
19pub(crate) use auth::is_known_tempo_endpoint;
20pub use auth::{AccessKeyOutcome, EnsureAccessKeyConfig, ensure_access_key};
21pub use keystore::*;
22pub use session::*;
23
24#[cfg(test)]
25pub(crate) use test_utils::{test_env_mutex, with_tempo_home};
26
27#[cfg(test)]
28mod tests;
29
30/// Conservative gas buffer for browser wallet transactions on Tempo chains.
31///
32/// Browser wallets may sign with P256 or WebAuthn instead of secp256k1, which costs more gas
33/// for signature verification. Since we can't determine the signature type before signing,
34/// we add the worst-case (WebAuthn) overhead:
35///   - P256: +5,000 gas (P256 precompile cost minus ecrecover savings)
36///   - WebAuthn: ~6,500 gas (P256 cost + calldata for webauthn_data)
37///
38/// See <https://github.com/tempoxyz/tempo/blob/6ebf1a8/crates/revm/src/handler.rs#L108-L124>
39pub const TEMPO_BROWSER_GAS_BUFFER: u64 = 7_000;
40
41/// Gas sponsor configuration for Tempo fee-payer signatures.
42#[derive(Clone, Debug)]
43pub struct TempoSponsor {
44    sponsor: Address,
45    signer: Option<Arc<WalletSigner>>,
46    signature: Option<Signature>,
47}
48
49impl TempoSponsor {
50    pub const fn new(
51        sponsor: Address,
52        signer: Option<Arc<WalletSigner>>,
53        signature: Option<Signature>,
54    ) -> Self {
55        Self { sponsor, signer, signature }
56    }
57
58    pub const fn sponsor(&self) -> Address {
59        self.sponsor
60    }
61
62    pub async fn attach_and_print<N: Network>(
63        &self,
64        tx: &mut N::TransactionRequest,
65        sender: Address,
66    ) -> Result<TempoSponsorPreview>
67    where
68        N::TransactionRequest: FoundryTransactionBuilder<N>,
69    {
70        if self.sponsor == sender {
71            eyre::bail!(
72                "invalid Tempo sponsorship: sponsor {} must not equal transaction sender",
73                self.sponsor
74            );
75        }
76
77        let digest = tx.compute_sponsor_hash(sender).ok_or_else(|| {
78            eyre::eyre!(
79                "failed to compute Tempo sponsor digest; make sure this is a complete Tempo AA transaction"
80            )
81        })?;
82
83        let preview = TempoSponsorPreview {
84            sponsor: self.sponsor,
85            fee_token: tx.fee_token(),
86            valid_before: tx.valid_before().map(|v| v.get()),
87            valid_after: tx.valid_after().map(|v| v.get()),
88            digest,
89        };
90        preview.print()?;
91
92        let signature = if let Some(signature) = self.signature {
93            signature
94        } else if let Some(signer) = &self.signer {
95            signer.sign_hash(&digest).await.context("failed to sign Tempo sponsor digest")?
96        } else {
97            eyre::bail!("missing Tempo sponsor signature or signer")
98        };
99
100        let recovered = signature
101            .recover_address_from_prehash(&digest)
102            .context("failed to recover Tempo sponsor signature")?;
103        if recovered != self.sponsor {
104            eyre::bail!("Tempo sponsor signature recovered {recovered}, expected {}", self.sponsor);
105        }
106        if recovered == sender {
107            eyre::bail!(
108                "invalid Tempo sponsorship: recovered fee payer {recovered} must not equal transaction sender"
109            );
110        }
111
112        tx.set_fee_payer_signature(signature);
113        Ok(preview)
114    }
115}
116
117/// User-visible sponsor digest metadata for a single outgoing Tempo transaction.
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub struct TempoSponsorPreview {
120    pub sponsor: Address,
121    pub fee_token: Option<Address>,
122    pub valid_before: Option<u64>,
123    pub valid_after: Option<u64>,
124    pub digest: B256,
125}
126
127impl TempoSponsorPreview {
128    pub fn print(&self) -> Result<()> {
129        crate::sh_eprintln!("Tempo sponsor: {}", self.sponsor)?;
130        crate::sh_eprintln!(
131            "Tempo fee token: {}",
132            self.fee_token.map_or_else(|| "network default".to_string(), |addr| addr.to_string())
133        )?;
134        crate::sh_eprintln!(
135            "Tempo validity: after {}, before {}",
136            self.valid_after.map_or_else(|| "none".to_string(), |v| v.to_string()),
137            self.valid_before.map_or_else(|| "none".to_string(), |v| v.to_string())
138        )?;
139        crate::sh_eprintln!("Tempo sponsor digest: {:?}", self.digest)?;
140        Ok(())
141    }
142}
143
144/// Resolves a `--tempo.sponsor-signer` URI into a Foundry wallet signer.
145pub async fn resolve_tempo_sponsor_signer(spec: &str) -> Result<WalletSigner> {
146    let spec = spec.trim();
147    let (scheme, value) = spec
148        .split_once("://")
149        .map(|(scheme, value)| (scheme.to_ascii_lowercase(), value))
150        .unwrap_or_else(|| (spec.to_ascii_lowercase(), ""));
151
152    match scheme.as_str() {
153        "env" => {
154            if value.is_empty() {
155                eyre::bail!("env:// sponsor signer requires an environment variable name");
156            }
157            let private_key = std::env::var(value)
158                .wrap_err_with(|| format!("{value} environment variable is required"))?;
159            foundry_wallets::utils::create_private_key_signer(&private_key)
160        }
161        "private-key" => {
162            if value.is_empty() {
163                eyre::bail!("private-key:// sponsor signer requires a private key");
164            }
165            foundry_wallets::utils::create_private_key_signer(value)
166        }
167        "keystore" => {
168            if value.is_empty() {
169                eyre::bail!("keystore:// sponsor signer requires a keystore path");
170            }
171            WalletOpts { keystore_path: Some(value.to_string()), ..Default::default() }
172                .signer()
173                .await
174        }
175        "account" => {
176            if value.is_empty() {
177                eyre::bail!("account:// sponsor signer requires an account name");
178            }
179            WalletOpts { keystore_account_name: Some(value.to_string()), ..Default::default() }
180                .signer()
181                .await
182        }
183        "ledger" => {
184            let raw = RawWalletOpts {
185                hd_path: (!value.is_empty()).then(|| value.to_string()),
186                ..Default::default()
187            };
188            WalletOpts { ledger: true, raw, ..Default::default() }.signer().await
189        }
190        "trezor" => {
191            let raw = RawWalletOpts {
192                hd_path: (!value.is_empty()).then(|| value.to_string()),
193                ..Default::default()
194            };
195            WalletOpts { trezor: true, raw, ..Default::default() }.signer().await
196        }
197        "aws" => WalletOpts { aws: true, ..Default::default() }.signer().await,
198        "gcp" => WalletOpts { gcp: true, ..Default::default() }.signer().await,
199        "turnkey" => WalletOpts { turnkey: true, ..Default::default() }.signer().await,
200        "browser" => {
201            eyre::bail!(
202                "browser:// sponsor signing is not supported by the current browser wallet API; use --tempo.sponsor-sig or another sponsor signer"
203            )
204        }
205        _ => eyre::bail!(
206            "unsupported Tempo sponsor signer `{spec}`; expected env://VAR, keystore://PATH, account://NAME, ledger://, trezor://, aws://, gcp://, turnkey://, or private-key://KEY"
207        ),
208    }
209}