foundry_common/provider/mpp/
keys.rs1use crate::tempo::{TEMPO_PRIVATE_KEY_ENV, WalletType, read_tempo_keys_file};
9use alloy_primitives::Address;
10use std::env;
11use tracing::debug;
12
13#[derive(Debug, Default, Clone)]
15pub struct DiscoverOptions {
16 pub chain_id: Option<u64>,
18 pub currency: Option<Address>,
20}
21
22#[derive(Debug, Clone)]
27pub struct MppKeyConfig {
28 pub key: String,
30 pub wallet_address: Option<Address>,
32 pub key_address: Option<Address>,
34 pub key_authorization: Option<String>,
36 pub chain_id: Option<u64>,
39 pub currencies: Vec<Address>,
41}
42
43pub fn discover_mpp_key() -> Option<String> {
48 discover_mpp_config(Default::default()).map(|c| c.key)
49}
50
51pub fn discover_mpp_config(opts: DiscoverOptions) -> Option<MppKeyConfig> {
58 if let Ok(key) = env::var(TEMPO_PRIVATE_KEY_ENV) {
60 let key = key.trim().to_string();
61 if !key.is_empty() {
62 debug!("using MPP key from {TEMPO_PRIVATE_KEY_ENV} env var");
63 return Some(MppKeyConfig {
64 key,
65 wallet_address: None,
66 key_address: None,
67 key_authorization: None,
68 chain_id: None,
69 currencies: vec![],
70 });
71 }
72 }
73
74 let keys_file = read_tempo_keys_file()?;
76
77 let now = std::time::SystemTime::now()
79 .duration_since(std::time::UNIX_EPOCH)
80 .map(|d| d.as_secs())
81 .unwrap_or(0);
82
83 let candidates: Vec<_> = keys_file
89 .keys
90 .iter()
91 .filter(|k| opts.chain_id.is_none_or(|cid| k.chain_id == cid))
92 .filter(|k| {
93 opts.currency
94 .is_none_or(|cur| k.limits.is_empty() || k.limits.iter().any(|l| l.currency == cur))
95 })
96 .filter(|k| k.expiry.is_none_or(|e| e == 0 || e > now))
97 .collect();
98
99 let primary = candidates
100 .iter()
101 .find(|k| k.wallet_type == WalletType::Passkey && k.has_inline_key())
102 .or_else(|| candidates.iter().find(|k| k.has_inline_key()))
103 .or(candidates.first())
104 .copied();
105
106 if let Some(entry) = primary
107 && let Some(key) = &entry.key
108 {
109 let key = key.trim().to_string();
110 if !key.is_empty() {
111 debug!("using MPP key from tempo wallet keys file");
112 return Some(MppKeyConfig {
113 key,
114 wallet_address: Some(entry.wallet_address),
115 key_address: entry.key_address,
116 key_authorization: entry.key_authorization.clone(),
117 chain_id: Some(entry.chain_id),
118 currencies: entry.limits.iter().map(|l| l.currency).collect(),
119 });
120 }
121 }
122
123 debug!("no usable key found in tempo keys file");
124 None
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::tempo::KeysFile;
131 use std::{io::Write, path::PathBuf};
132
133 fn setup_keys_toml(toml_content: &str) -> (tempfile::TempDir, PathBuf) {
135 let dir = tempfile::tempdir().expect("tempdir");
136 let wallet_dir = dir.path().join("wallet");
137 std::fs::create_dir_all(&wallet_dir).expect("create wallet dir");
138 let keys_path = wallet_dir.join("keys.toml");
139 let mut f = std::fs::File::create(&keys_path).expect("create keys.toml");
140 f.write_all(toml_content.as_bytes()).expect("write keys.toml");
141 (dir, keys_path)
142 }
143
144 #[test]
145 fn discover_from_tempo_home_keys_toml() {
146 let _g = crate::tempo::test_env_mutex().blocking_lock();
147 let key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
148 let toml_content = format!(
149 r#"
150[[keys]]
151wallet_address = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
152key_address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
153key = "{key}"
154chain_id = 4217
155"#
156 );
157 let (dir, _) = setup_keys_toml(&toml_content);
158
159 unsafe {
160 std::env::set_var("TEMPO_HOME", dir.path());
161 std::env::remove_var("TEMPO_PRIVATE_KEY");
162 }
163
164 let discovered = discover_mpp_key();
165 assert_eq!(discovered.as_deref(), Some(key));
166
167 unsafe { std::env::remove_var("TEMPO_HOME") };
168 }
169
170 #[test]
171 fn discover_env_var_takes_priority_over_keys_toml() {
172 let _g = crate::tempo::test_env_mutex().blocking_lock();
173 let file_key = "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
174 let env_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
175 let toml_content = format!(
176 r#"
177[[keys]]
178wallet_address = "0x0000000000000000000000000000000000000001"
179key = "{file_key}"
180"#
181 );
182 let (dir, _) = setup_keys_toml(&toml_content);
183
184 unsafe {
185 std::env::set_var("TEMPO_HOME", dir.path());
186 std::env::set_var("TEMPO_PRIVATE_KEY", env_key);
187 }
188
189 let discovered = discover_mpp_key();
190 assert_eq!(discovered.as_deref(), Some(env_key));
191
192 unsafe {
193 std::env::remove_var("TEMPO_HOME");
194 std::env::remove_var("TEMPO_PRIVATE_KEY");
195 }
196 }
197
198 #[test]
199 fn discover_returns_none_when_no_keys() {
200 let _g = crate::tempo::test_env_mutex().blocking_lock();
201 let (dir, _) = setup_keys_toml("");
202
203 unsafe {
204 std::env::set_var("TEMPO_HOME", dir.path());
205 std::env::remove_var("TEMPO_PRIVATE_KEY");
206 }
207
208 let discovered = discover_mpp_key();
209 assert!(discovered.is_none());
210
211 unsafe { std::env::remove_var("TEMPO_HOME") };
212 }
213
214 #[test]
215 fn discover_skips_entries_without_inline_key() {
216 let _g = crate::tempo::test_env_mutex().blocking_lock();
217 let key = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
218 let toml_content = format!(
219 r#"
220[[keys]]
221wallet_address = "0x0000000000000000000000000000000000000001"
222chain_id = 4217
223
224[[keys]]
225wallet_address = "0x0000000000000000000000000000000000000002"
226key = "{key}"
227chain_id = 4217
228"#
229 );
230 let (dir, _) = setup_keys_toml(&toml_content);
231
232 unsafe {
233 std::env::set_var("TEMPO_HOME", dir.path());
234 std::env::remove_var("TEMPO_PRIVATE_KEY");
235 }
236
237 let discovered = discover_mpp_key();
238 assert_eq!(discovered.as_deref(), Some(key));
239
240 unsafe { std::env::remove_var("TEMPO_HOME") };
241 }
242
243 #[test]
244 fn parse_keys_toml() {
245 let toml_str = r#"
246[[keys]]
247wallet_address = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
248key_address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
249key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
250chain_id = 4217
251"#;
252 let keys_file: KeysFile = toml::from_str(toml_str).unwrap();
253 assert_eq!(keys_file.keys.len(), 1);
254 assert_eq!(
255 keys_file.keys[0].key.as_deref(),
256 Some("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")
257 );
258 }
259
260 #[test]
261 fn parse_keys_toml_no_inline_key() {
262 let toml_str = r#"
263[[keys]]
264wallet_address = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
265key_address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
266chain_id = 4217
267"#;
268 let keys_file: KeysFile = toml::from_str(toml_str).unwrap();
269 assert_eq!(keys_file.keys.len(), 1);
270 assert!(keys_file.keys[0].key.is_none());
271 }
272
273 #[test]
274 fn parse_keys_toml_multiple_entries() {
275 let toml_str = r#"
276[[keys]]
277wallet_address = "0x0000000000000000000000000000000000000001"
278key_address = "0x0000000000000000000000000000000000000002"
279chain_id = 4217
280
281[[keys]]
282wallet_address = "0x0000000000000000000000000000000000000003"
283key_address = "0x0000000000000000000000000000000000000004"
284key = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
285chain_id = 4217
286"#;
287 let keys_file: KeysFile = toml::from_str(toml_str).unwrap();
288 assert_eq!(keys_file.keys.len(), 2);
289 assert!(keys_file.keys[0].key.is_none());
290 assert!(keys_file.keys[1].key.is_some());
291 }
292
293 #[test]
294 fn parse_keys_toml_with_wallet_type() {
295 let toml_str = r#"
296[[keys]]
297wallet_type = "passkey"
298wallet_address = "0x0000000000000000000000000000000000000001"
299key = "0xpasskey_secret"
300chain_id = 4217
301
302[[keys]]
303wallet_type = "local"
304wallet_address = "0x0000000000000000000000000000000000000002"
305key = "0xlocal_secret"
306chain_id = 4217
307"#;
308 let keys_file: KeysFile = toml::from_str(toml_str).unwrap();
309 assert_eq!(keys_file.keys.len(), 2);
310 assert_eq!(keys_file.keys[0].wallet_type, WalletType::Passkey);
311 assert_eq!(keys_file.keys[1].wallet_type, WalletType::Local);
312 }
313
314 #[test]
315 fn primary_key_passkey_wins() {
316 let toml_str = r#"
317[[keys]]
318wallet_type = "local"
319wallet_address = "0x0000000000000000000000000000000000000001"
320key = "0xlocal_key"
321
322[[keys]]
323wallet_type = "passkey"
324wallet_address = "0x0000000000000000000000000000000000000002"
325key = "0xpasskey_key"
326"#;
327 let keys_file: KeysFile = toml::from_str(toml_str).unwrap();
328 let primary = keys_file
329 .keys
330 .iter()
331 .find(|k| k.wallet_type == WalletType::Passkey)
332 .or_else(|| keys_file.keys.iter().find(|k| k.has_inline_key()))
333 .or(keys_file.keys.first());
334 assert_eq!(primary.unwrap().key.as_deref(), Some("0xpasskey_key"));
335 }
336
337 #[test]
338 fn primary_key_inline_key_over_no_key() {
339 let toml_str = r#"
340[[keys]]
341wallet_address = "0x0000000000000000000000000000000000000001"
342
343[[keys]]
344wallet_address = "0x0000000000000000000000000000000000000002"
345key = "0xthe_key"
346"#;
347 let keys_file: KeysFile = toml::from_str(toml_str).unwrap();
348 let primary = keys_file
349 .keys
350 .iter()
351 .find(|k| k.wallet_type == WalletType::Passkey)
352 .or_else(|| keys_file.keys.iter().find(|k| k.has_inline_key()))
353 .or(keys_file.keys.first());
354 assert_eq!(primary.unwrap().key.as_deref(), Some("0xthe_key"));
355 }
356
357 #[test]
358 fn discover_filters_by_chain_id() {
359 let _g = crate::tempo::test_env_mutex().blocking_lock();
360 let mainnet_key = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
361 let testnet_key = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
362 let toml_content = format!(
363 r#"
364[[keys]]
365wallet_type = "passkey"
366wallet_address = "0x0000000000000000000000000000000000000001"
367key = "{mainnet_key}"
368chain_id = 4217
369
370[[keys]]
371wallet_type = "passkey"
372wallet_address = "0x0000000000000000000000000000000000000002"
373key = "{testnet_key}"
374chain_id = 42431
375"#
376 );
377 let (dir, _) = setup_keys_toml(&toml_content);
378 unsafe {
379 std::env::set_var("TEMPO_HOME", dir.path());
380 std::env::remove_var("TEMPO_PRIVATE_KEY");
381 }
382
383 let config =
385 discover_mpp_config(DiscoverOptions { chain_id: Some(42431), ..Default::default() });
386 assert_eq!(config.as_ref().unwrap().key, testnet_key);
387
388 let config =
390 discover_mpp_config(DiscoverOptions { chain_id: Some(4217), ..Default::default() });
391 assert_eq!(config.as_ref().unwrap().key, mainnet_key);
392
393 let config = discover_mpp_config(Default::default());
395 assert_eq!(config.as_ref().unwrap().key, mainnet_key);
396
397 let config =
399 discover_mpp_config(DiscoverOptions { chain_id: Some(9999), ..Default::default() });
400 assert!(config.is_none());
401
402 let toml_mixed = format!(
404 r#"
405[[keys]]
406wallet_type = "local"
407wallet_address = "0x0000000000000000000000000000000000000001"
408key = "{mainnet_key}"
409chain_id = 4217
410
411[[keys]]
412wallet_type = "passkey"
413wallet_address = "0x0000000000000000000000000000000000000002"
414key = "{testnet_key}"
415chain_id = 4217
416"#
417 );
418 let (dir2, _) = setup_keys_toml(&toml_mixed);
419 unsafe { std::env::set_var("TEMPO_HOME", dir2.path()) };
420
421 let config =
422 discover_mpp_config(DiscoverOptions { chain_id: Some(4217), ..Default::default() });
423 assert_eq!(
424 config.as_ref().unwrap().key,
425 testnet_key,
426 "passkey should win over local within the same chain_id"
427 );
428
429 unsafe { std::env::remove_var("TEMPO_HOME") };
430 }
431
432 #[test]
433 fn discover_filters_expired_entries() {
434 let _g = crate::tempo::test_env_mutex().blocking_lock();
437 let expired_key = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
438 let fresh_key = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
439 let toml_content = format!(
440 r#"
441[[keys]]
442wallet_type = "passkey"
443wallet_address = "0x0000000000000000000000000000000000000001"
444key = "{expired_key}"
445chain_id = 4217
446expiry = 1
447
448[[keys]]
449wallet_type = "passkey"
450wallet_address = "0x0000000000000000000000000000000000000002"
451key = "{fresh_key}"
452chain_id = 4217
453expiry = 0
454"#
455 );
456 let (dir, _) = setup_keys_toml(&toml_content);
457 unsafe {
458 std::env::set_var("TEMPO_HOME", dir.path());
459 std::env::remove_var("TEMPO_PRIVATE_KEY");
460 }
461
462 let config =
464 discover_mpp_config(DiscoverOptions { chain_id: Some(4217), ..Default::default() });
465 assert_eq!(config.as_ref().unwrap().key, fresh_key);
466
467 let only_expired = format!(
470 r#"
471[[keys]]
472wallet_type = "passkey"
473wallet_address = "0x0000000000000000000000000000000000000001"
474key = "{expired_key}"
475chain_id = 4217
476expiry = 1
477"#
478 );
479 let (dir2, _) = setup_keys_toml(&only_expired);
480 unsafe { std::env::set_var("TEMPO_HOME", dir2.path()) };
481 let config =
482 discover_mpp_config(DiscoverOptions { chain_id: Some(4217), ..Default::default() });
483 assert!(config.is_none(), "expired-only keys.toml must not yield a usable key");
484
485 unsafe { std::env::remove_var("TEMPO_HOME") };
486 }
487
488 #[test]
489 fn parse_keys_toml_unknown_fields_ignored() {
490 let toml_str = r#"
491[[keys]]
492wallet_address = "0x0000000000000000000000000000000000000001"
493key = "0xsecret"
494chain_id = 4217
495key_authorization = "0xauth_data"
496expiry = 1750000000
497unknown_future_field = "should be ignored"
498
499[[keys.limits]]
500currency = "0x20c000000000000000000000b9537d11c60e8b50"
501limit = "1000"
502"#;
503 let keys_file: KeysFile = toml::from_str(toml_str).unwrap();
504 assert_eq!(keys_file.keys.len(), 1);
505 assert_eq!(keys_file.keys[0].key.as_deref(), Some("0xsecret"));
506 }
507}