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