Skip to main content

foundry_wallets/
tempo.rs

1use alloy_primitives::{Address, hex};
2use alloy_rlp::Decodable;
3use eyre::Result;
4use std::path::PathBuf;
5use tempo_primitives::transaction::SignedKeyAuthorization;
6
7use crate::{WalletSigner, utils};
8
9/// Wallet type: how this wallet was created.
10#[derive(Clone, Copy, Default, serde::Deserialize)]
11#[serde(rename_all = "lowercase")]
12enum WalletType {
13    #[default]
14    Local,
15    Passkey,
16}
17
18/// Cryptographic key type.
19#[derive(Clone, Copy, Default, serde::Deserialize)]
20#[serde(rename_all = "lowercase")]
21enum KeyType {
22    #[default]
23    Secp256k1,
24    P256,
25    WebAuthn,
26}
27
28/// A single entry from Tempo's `keys.toml`.
29#[derive(serde::Deserialize)]
30#[allow(dead_code)]
31struct KeyEntry {
32    #[serde(default)]
33    wallet_type: WalletType,
34    #[serde(default)]
35    wallet_address: Address,
36    #[serde(default)]
37    chain_id: u64,
38    #[serde(default)]
39    key_type: KeyType,
40    #[serde(default)]
41    key_address: Option<Address>,
42    #[serde(default)]
43    key: Option<String>,
44    #[serde(default)]
45    key_authorization: Option<String>,
46    #[serde(default)]
47    expiry: Option<u64>,
48    #[serde(default)]
49    limits: Vec<StoredTokenLimit>,
50}
51
52/// Per-token spending limit stored in `keys.toml`.
53#[derive(serde::Deserialize)]
54struct StoredTokenLimit {
55    #[allow(dead_code)]
56    currency: Address,
57    #[allow(dead_code)]
58    limit: String,
59}
60
61/// The top-level structure of `~/.tempo/wallet/keys.toml`.
62#[derive(serde::Deserialize)]
63struct KeysFile {
64    #[serde(default)]
65    keys: Vec<KeyEntry>,
66}
67
68/// Configuration for a Tempo access key (keychain mode).
69///
70/// When a Tempo wallet entry uses keychain mode (`wallet_address != key_address`), the signer
71/// is an access key that signs on behalf of the root wallet. This struct carries the metadata
72/// needed to construct the correct transaction.
73#[derive(Debug, Clone)]
74pub struct TempoAccessKeyConfig {
75    /// The root wallet address (the `from` address for transactions).
76    pub wallet_address: Address,
77    /// The access key's address (derived from the private key that actually signs).
78    pub key_address: Address,
79    /// Decoded key authorization for on-chain provisioning.
80    ///
81    /// When present, callers should check whether the key is already provisioned on-chain
82    /// (via the AccountKeychain precompile) before including this in a transaction.
83    pub key_authorization: Option<SignedKeyAuthorization>,
84}
85
86/// Result of looking up an address in Tempo's key store.
87pub enum TempoLookup {
88    /// A direct (EOA) signer was found — `wallet_address == key_address`.
89    Direct(WalletSigner),
90    /// A keychain (access key) signer was found — `wallet_address != key_address`.
91    Keychain(WalletSigner, Box<TempoAccessKeyConfig>),
92    /// No matching entry was found.
93    NotFound,
94}
95
96/// Returns the path to Tempo's keys file.
97///
98/// Respects `TEMPO_HOME` env var, defaulting to `~/.tempo`.
99fn keys_path() -> Option<PathBuf> {
100    let base = std::env::var_os("TEMPO_HOME")
101        .map(PathBuf::from)
102        .or_else(|| dirs::home_dir().map(|h| h.join(".tempo")))?;
103    Some(base.join("wallet").join("keys.toml"))
104}
105
106/// Decodes a hex-encoded, RLP-encoded [`SignedKeyAuthorization`].
107fn decode_key_authorization(hex_str: &str) -> Result<SignedKeyAuthorization> {
108    let bytes = hex::decode(hex_str)?;
109    let auth = SignedKeyAuthorization::decode(&mut bytes.as_slice())?;
110    Ok(auth)
111}
112
113/// Looks up a signer for the given address in Tempo's `keys.toml`.
114///
115/// Returns [`TempoLookup::Direct`] if a direct-mode (EOA) key is found,
116/// [`TempoLookup::Keychain`] if a keychain-mode access key is found,
117/// or [`TempoLookup::NotFound`] if no entry matches.
118pub fn lookup_signer(from: Address) -> Result<TempoLookup> {
119    let path = match keys_path() {
120        Some(p) if p.is_file() => p,
121        _ => return Ok(TempoLookup::NotFound),
122    };
123
124    let contents = std::fs::read_to_string(&path)?;
125    let file: KeysFile = toml::from_str(&contents)?;
126
127    for entry in &file.keys {
128        if entry.wallet_address != from {
129            continue;
130        }
131
132        let Some(key) = &entry.key else {
133            continue;
134        };
135
136        // Direct mode: wallet_address == key_address (or key_address is absent).
137        let is_direct =
138            entry.key_address.is_none() || entry.key_address == Some(entry.wallet_address);
139
140        let signer = utils::create_private_key_signer(key)?;
141
142        if is_direct {
143            return Ok(TempoLookup::Direct(signer));
144        }
145
146        // Keychain mode: the key is an access key signing on behalf of wallet_address.
147        let key_authorization =
148            entry.key_authorization.as_deref().map(decode_key_authorization).transpose()?;
149
150        let config = TempoAccessKeyConfig {
151            wallet_address: entry.wallet_address,
152            // SAFETY: `is_direct` was false, so `key_address` is `Some` and != wallet_address
153            key_address: entry.key_address.unwrap(),
154            key_authorization,
155        };
156        return Ok(TempoLookup::Keychain(signer, Box::new(config)));
157    }
158
159    Ok(TempoLookup::NotFound)
160}