1use super::TempoOpts;
2use alloy_primitives::{Address, B256};
3use eyre::{Result, WrapErr};
4use foundry_common::tempo::{ResolvedSessionSigner, resolve_live_session_signer};
5use foundry_wallets::{MultiWalletOpts, WalletOpts};
6use std::{
7 str::FromStr,
8 time::{SystemTime, UNIX_EPOCH},
9};
10
11pub const TEMPO_SESSION_ID_ENV: &str = "TEMPO_SESSION_ID";
13
14impl TempoOpts {
15 pub fn session_id(&self) -> Result<Option<B256>> {
17 if let Some(session) = self.session {
18 return Ok(Some(session));
19 }
20
21 let raw = match std::env::var(TEMPO_SESSION_ID_ENV) {
22 Ok(raw) => raw,
23 Err(std::env::VarError::NotPresent) => return Ok(None),
24 Err(std::env::VarError::NotUnicode(_)) => {
25 eyre::bail!("invalid {TEMPO_SESSION_ID_ENV}: value is not valid UTF-8");
26 }
27 };
28
29 let raw = raw.trim();
30 if raw.is_empty() {
31 return Ok(None);
32 }
33
34 B256::from_str(raw).map(Some).wrap_err_with(|| {
35 format!("invalid {TEMPO_SESSION_ID_ENV}: expected 32-byte hex session id")
36 })
37 }
38
39 pub fn session_signer_for_wallet(
44 &self,
45 wallet: &WalletOpts,
46 expected_chain_id: u64,
47 ) -> Result<Option<ResolvedSessionSigner>> {
48 let Some(session_id) = self.session_id()? else {
49 return Ok(None);
50 };
51 ensure_no_explicit_wallet_signer(wallet)?;
52 Ok(Some(resolve_session_signer(session_id, wallet.from, expected_chain_id)?))
53 }
54
55 pub fn session_signer_for_multi_wallet(
57 &self,
58 wallets: &MultiWalletOpts,
59 expected_sender: Option<Address>,
60 expected_chain_id: u64,
61 ) -> Result<Option<ResolvedSessionSigner>> {
62 let Some(session_id) = self.session_id()? else {
63 return Ok(None);
64 };
65 ensure_no_explicit_multi_wallet_signer(wallets)?;
66 Ok(Some(resolve_session_signer(session_id, expected_sender, expected_chain_id)?))
67 }
68
69 pub fn session_signer_for_multi_wallet_any_chain(
75 &self,
76 wallets: &MultiWalletOpts,
77 expected_sender: Option<Address>,
78 ) -> Result<Option<ResolvedSessionSigner>> {
79 let Some(session_id) = self.session_id()? else {
80 return Ok(None);
81 };
82 ensure_no_explicit_multi_wallet_signer(wallets)?;
83 let resolved = resolve_session(session_id)?;
84 ensure_expected_sender(expected_sender, resolved.access_key.wallet_address)?;
85 Ok(Some(resolved))
86 }
87
88 pub fn session_sender_for_multi_wallet(
94 &self,
95 wallets: &MultiWalletOpts,
96 expected_sender: Option<Address>,
97 ) -> Result<Option<Address>> {
98 Ok(self
99 .session_signer_for_multi_wallet_any_chain(wallets, expected_sender)?
100 .map(|resolved| resolved.access_key.wallet_address))
101 }
102}
103
104fn resolve_session_signer(
109 session_id: B256,
110 expected_sender: Option<Address>,
111 expected_chain_id: u64,
112) -> Result<ResolvedSessionSigner> {
113 let resolved = resolve_session(session_id)?;
114
115 if resolved.session.chain_id != expected_chain_id {
116 eyre::bail!(
117 "Tempo session {session_id:?} is for chain {}, but command is using chain {}",
118 resolved.session.chain_id,
119 expected_chain_id
120 );
121 }
122
123 ensure_expected_sender(expected_sender, resolved.access_key.wallet_address)?;
124 Ok(resolved)
125}
126
127fn resolve_session(session_id: B256) -> Result<ResolvedSessionSigner> {
128 let now = SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards");
129 resolve_live_session_signer(session_id, now.as_secs())?
130 .ok_or_else(|| eyre::eyre!("Tempo session {session_id:?} is not active or has no live key"))
131}
132
133fn ensure_expected_sender(expected_sender: Option<Address>, session_sender: Address) -> Result<()> {
134 if let Some(from) = expected_sender
135 && from != session_sender
136 {
137 eyre::bail!("sender {from} does not match Tempo session root account {session_sender}");
138 }
139
140 Ok(())
141}
142
143fn ensure_no_explicit_wallet_signer(wallet: &WalletOpts) -> Result<()> {
147 if wallet.has_explicit_signer() {
148 eyre::bail!(
149 "--tempo.session/TEMPO_SESSION_ID cannot be combined with explicit wallet signer options"
150 );
151 }
152 Ok(())
153}
154
155trait ExplicitSignerOpts {
156 fn has_explicit_signer(&self) -> bool;
157}
158
159impl ExplicitSignerOpts for WalletOpts {
160 fn has_explicit_signer(&self) -> bool {
161 self.raw.interactive
162 || self.raw.private_key.is_some()
163 || self.raw.mnemonic.is_some()
164 || self.keystore_path.is_some()
165 || self.keystore_account_name.is_some()
166 || self.ledger
167 || self.trezor
168 || self.aws
169 || self.gcp
170 || self.turnkey
171 || self.tempo_access_key.is_some()
172 }
173}
174
175fn ensure_no_explicit_multi_wallet_signer(wallets: &MultiWalletOpts) -> Result<()> {
180 if wallets.has_explicit_signer() {
181 eyre::bail!(
182 "--tempo.session/TEMPO_SESSION_ID cannot be combined with explicit wallet signer options"
183 );
184 }
185 Ok(())
186}
187
188impl ExplicitSignerOpts for MultiWalletOpts {
189 fn has_explicit_signer(&self) -> bool {
190 self.interactive
191 || self.interactives > 0
192 || self.private_key.is_some()
193 || self.private_keys.is_some()
194 || self.mnemonics.is_some()
195 || self.keystore_paths.is_some()
196 || self.keystore_account_names.is_some()
197 || self.ledger
198 || self.trezor
199 || self.aws
200 || self.gcp
201 || self.turnkey
202 || self.browser.browser
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use alloy_signer::Signer;
210 use clap::Parser;
211 use foundry_common::tempo::{
212 KeyType, SessionEntry, SessionKeyMaterial, SessionStatus, TEMPO_HOME_ENV,
213 upsert_session_entry,
214 };
215 use std::sync::Mutex;
216
217 const SESSION_PRIVATE_KEY: &str =
218 "0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0";
219
220 static ENV_MUTEX: Mutex<()> = Mutex::new(());
221
222 fn with_clean_session_env(test: impl FnOnce()) {
223 with_session_env(None, test);
224 }
225
226 fn with_clean_session_home(test: impl FnOnce()) {
227 let tmp = tempfile::tempdir().unwrap();
228 with_session_env(Some(tmp.path()), test);
229 }
230
231 fn with_session_env(tempo_home: Option<&std::path::Path>, test: impl FnOnce()) {
232 let _guard = ENV_MUTEX.lock().unwrap();
233 unsafe {
235 std::env::remove_var(TEMPO_SESSION_ID_ENV);
236 if let Some(tempo_home) = tempo_home {
237 std::env::set_var(TEMPO_HOME_ENV, tempo_home);
238 }
239 }
240 test();
241 unsafe {
243 std::env::remove_var(TEMPO_SESSION_ID_ENV);
244 std::env::remove_var(TEMPO_HOME_ENV);
245 }
246 }
247
248 fn session_id(byte: u8) -> B256 {
249 B256::from([byte; 32])
250 }
251
252 fn active_session_entry(session_id: B256) -> SessionEntry {
253 let key = foundry_wallets::utils::create_private_key_signer(SESSION_PRIVATE_KEY).unwrap();
254 SessionEntry {
255 session_id,
256 root_account: Address::from([0x11; 20]),
257 chain_id: 4217,
258 key_address: key.address(),
259 expiry: u64::MAX,
260 scope: None,
261 limits: None,
262 status: SessionStatus::Active,
263 key: Some(SessionKeyMaterial {
264 key_type: KeyType::Secp256k1,
265 key: SESSION_PRIVATE_KEY.to_string(),
266 key_authorization: None,
267 }),
268 }
269 }
270
271 #[test]
272 fn parses_tempo_session_cli_arg() {
273 with_clean_session_env(|| {
274 let id = session_id(0x11);
275 let opts =
276 TempoOpts::try_parse_from(["", "--tempo.session", &format!("{id:?}")]).unwrap();
277
278 assert_eq!(opts.session, Some(id));
279 assert_eq!(opts.session_id().unwrap(), Some(id));
280 });
281 }
282
283 #[test]
284 fn tempo_session_env_is_used_when_cli_arg_is_absent() {
285 with_clean_session_env(|| {
286 let id = session_id(0x22);
287 unsafe { std::env::set_var(TEMPO_SESSION_ID_ENV, format!("{id:?}")) };
289 let opts = TempoOpts::default();
290
291 assert_eq!(opts.session_id().unwrap(), Some(id));
292 });
293 }
294
295 #[test]
296 fn tempo_session_cli_arg_overrides_env() {
297 with_clean_session_env(|| {
298 let env_id = session_id(0x33);
299 let cli_id = session_id(0x44);
300 unsafe { std::env::set_var(TEMPO_SESSION_ID_ENV, format!("{env_id:?}")) };
302
303 let opts =
304 TempoOpts::try_parse_from(["", "--tempo.session", &format!("{cli_id:?}")]).unwrap();
305
306 assert_eq!(opts.session_id().unwrap(), Some(cli_id));
307 });
308 }
309
310 #[test]
311 fn invalid_tempo_session_env_fails_closed() {
312 with_clean_session_env(|| {
313 unsafe { std::env::set_var(TEMPO_SESSION_ID_ENV, "not-a-session-id") };
315 let err = TempoOpts::default().session_id().unwrap_err();
316
317 assert!(err.to_string().contains(TEMPO_SESSION_ID_ENV), "{err}");
318 });
319 }
320
321 #[cfg(unix)]
322 #[test]
323 fn non_unicode_tempo_session_env_fails_closed() {
324 use std::{ffi::OsString, os::unix::ffi::OsStringExt};
325
326 with_clean_session_env(|| {
327 unsafe {
329 std::env::set_var(TEMPO_SESSION_ID_ENV, OsString::from_vec(vec![0xff]));
330 }
331 let err = TempoOpts::default().session_id().unwrap_err();
332
333 assert!(err.to_string().contains("value is not valid UTF-8"), "{err}");
334 });
335 }
336
337 #[test]
338 fn tempo_session_rejects_explicit_wallet_signers() {
339 let opts = TempoOpts { session: Some(session_id(0x55)), ..Default::default() };
340 let wallet = WalletOpts {
341 raw: foundry_wallets::RawWalletOpts {
342 private_key: Some("0xdead".to_string()),
343 ..Default::default()
344 },
345 ..Default::default()
346 };
347
348 let err = opts.session_signer_for_wallet(&wallet, 4217).unwrap_err();
349 assert!(err.to_string().contains("explicit wallet signer"), "{err}");
350 }
351
352 #[test]
353 fn absent_tempo_session_does_not_reject_explicit_wallet_signers() {
354 with_clean_session_env(|| {
355 let opts = TempoOpts::default();
356 let wallet = WalletOpts {
357 raw: foundry_wallets::RawWalletOpts {
358 private_key: Some("0xdead".to_string()),
359 ..Default::default()
360 },
361 ..Default::default()
362 };
363
364 assert!(opts.session_signer_for_wallet(&wallet, 4217).unwrap().is_none());
365 });
366 }
367
368 #[test]
369 fn tempo_session_rejects_explicit_multi_wallet_signers() {
370 let opts = TempoOpts { session: Some(session_id(0x66)), ..Default::default() };
371 let wallets =
372 MultiWalletOpts { private_key: Some("0xdead".to_string()), ..Default::default() };
373
374 let err = opts.session_signer_for_multi_wallet(&wallets, None, 4217).unwrap_err();
375 assert!(err.to_string().contains("explicit wallet signer"), "{err}");
376 }
377
378 #[test]
379 fn absent_tempo_session_does_not_reject_explicit_multi_wallet_signers() {
380 with_clean_session_env(|| {
381 let opts = TempoOpts::default();
382 let wallets =
383 MultiWalletOpts { private_key: Some("0xdead".to_string()), ..Default::default() };
384
385 assert!(opts.session_signer_for_multi_wallet(&wallets, None, 4217).unwrap().is_none());
386 });
387 }
388
389 #[test]
390 fn tempo_session_rejects_wrong_chain() {
391 with_clean_session_home(|| {
392 let id = session_id(0x77);
393 upsert_session_entry(active_session_entry(id)).unwrap();
394 let opts = TempoOpts { session: Some(id), ..Default::default() };
395
396 let err = opts.session_signer_for_wallet(&WalletOpts::default(), 1).unwrap_err();
397
398 assert!(err.to_string().contains("is for chain 4217"), "{err}");
399 });
400 }
401
402 #[test]
403 fn tempo_session_sender_does_not_validate_chain() {
404 with_clean_session_home(|| {
405 let id = session_id(0x99);
406 upsert_session_entry(active_session_entry(id)).unwrap();
407 let opts = TempoOpts { session: Some(id), ..Default::default() };
408 let wallets = MultiWalletOpts::default();
409
410 let sender = opts.session_sender_for_multi_wallet(&wallets, None).unwrap();
411
412 assert_eq!(sender, Some(Address::from([0x11; 20])));
413 });
414 }
415
416 #[test]
417 fn tempo_session_signer_any_chain_returns_session_chain() {
418 with_clean_session_home(|| {
419 let id = session_id(0xaa);
420 upsert_session_entry(active_session_entry(id)).unwrap();
421 let opts = TempoOpts { session: Some(id), ..Default::default() };
422 let wallets = MultiWalletOpts::default();
423
424 let session = opts
425 .session_signer_for_multi_wallet_any_chain(
426 &wallets,
427 Some(Address::from([0x11; 20])),
428 )
429 .unwrap()
430 .unwrap();
431
432 assert_eq!(session.session.chain_id, 4217);
433 assert_eq!(session.access_key.wallet_address, Address::from([0x11; 20]));
434 });
435 }
436
437 #[test]
438 fn tempo_session_rejects_sender_mismatch() {
439 with_clean_session_home(|| {
440 let id = session_id(0x88);
441 upsert_session_entry(active_session_entry(id)).unwrap();
442 let opts = TempoOpts { session: Some(id), ..Default::default() };
443 let wallets = MultiWalletOpts::default();
444
445 let err = opts
446 .session_signer_for_multi_wallet(&wallets, Some(Address::from([0x22; 20])), 4217)
447 .unwrap_err();
448
449 assert!(err.to_string().contains("does not match Tempo session root account"), "{err}");
450 });
451 }
452}