1pub 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
43fn redacted_debug(value: &str) -> &'static str {
45 if value.trim().is_empty() { "<empty>" } else { "<redacted>" }
46}
47
48pub const TEMPO_BROWSER_GAS_BUFFER: u64 = 7_000;
58
59pub const ALPHA_USD_ADDRESS: Address = address!("0x20C0000000000000000000000000000000000001");
65pub const BETA_USD_ADDRESS: Address = address!("0x20C0000000000000000000000000000000000002");
66pub const THETA_USD_ADDRESS: Address = address!("0x20C0000000000000000000000000000000000003");
67
68pub 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
76pub 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
87pub 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
95pub 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
103pub 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#[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#[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
215pub 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}