Skip to main content

foundry_common/tempo/
keystore.rs

1//! Tempo Accounts store discovery and local key metadata.
2
3use alloy_primitives::{Address, hex};
4use alloy_rlp::Decodable;
5use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7use tempo_alloy::accounts::{TempoAccountsStore, default_accounts_store_path};
8use tempo_primitives::{
9    SignatureType,
10    transaction::{SignedKeyAuthorization, TokenLimit},
11};
12
13/// Environment variable to override the Tempo home directory.
14pub const TEMPO_HOME_ENV: &str = "TEMPO_HOME";
15
16/// Cryptographic key type used by Tempo Accounts access keys.
17#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
18#[serde(rename_all = "lowercase")]
19pub enum KeyType {
20    #[default]
21    Secp256k1,
22    P256,
23    WebAuthn,
24}
25
26impl From<SignatureType> for KeyType {
27    fn from(value: SignatureType) -> Self {
28        match value {
29            SignatureType::Secp256k1 => Self::Secp256k1,
30            SignatureType::P256 => Self::P256,
31            SignatureType::WebAuthn => Self::WebAuthn,
32        }
33    }
34}
35
36/// Per-token spending limit exposed by the local keychain commands.
37#[derive(Debug, Clone)]
38pub struct StoredTokenLimit {
39    pub currency: Address,
40    pub limit: String,
41    pub period: u64,
42}
43
44impl From<&TokenLimit> for StoredTokenLimit {
45    fn from(value: &TokenLimit) -> Self {
46        Self { currency: value.token, limit: value.limit.to_string(), period: value.period }
47    }
48}
49
50/// Non-secret view of one access key in the Tempo Accounts store.
51#[derive(Clone, Default)]
52pub struct KeyEntry {
53    pub wallet_address: Address,
54    pub chain_id: u64,
55    pub key_type: KeyType,
56    pub key_address: Address,
57    pub key_authorization: Option<SignedKeyAuthorization>,
58    pub expiry: Option<u64>,
59    pub limits: Vec<StoredTokenLimit>,
60    locally_signable: bool,
61}
62
63impl std::fmt::Debug for KeyEntry {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        f.debug_struct("KeyEntry")
66            .field("wallet_address", &self.wallet_address)
67            .field("chain_id", &self.chain_id)
68            .field("key_type", &self.key_type)
69            .field("key_address", &self.key_address)
70            .field("has_key", &self.locally_signable)
71            .field("has_key_authorization", &self.key_authorization.is_some())
72            .field("expiry", &self.expiry)
73            .field("limits", &self.limits)
74            .finish()
75    }
76}
77
78impl KeyEntry {
79    /// Construct non-secret metadata for one Tempo Accounts access key.
80    pub const fn new(
81        wallet_address: Address,
82        chain_id: u64,
83        key_type: KeyType,
84        key_address: Address,
85    ) -> Self {
86        Self {
87            wallet_address,
88            chain_id,
89            key_type,
90            key_address,
91            key_authorization: None,
92            expiry: None,
93            limits: Vec::new(),
94            locally_signable: false,
95        }
96    }
97
98    /// Attach a pending authorization to this metadata view.
99    #[doc(hidden)]
100    pub fn with_key_authorization(mut self, authorization: SignedKeyAuthorization) -> Self {
101        self.key_authorization = Some(authorization);
102        self
103    }
104
105    /// Whether the Accounts store contains usable local signing material.
106    pub const fn has_inline_key(&self) -> bool {
107        self.locally_signable
108    }
109
110    /// Override local signing availability when constructing diagnostic fixtures.
111    #[doc(hidden)]
112    pub const fn with_locally_signable(mut self, locally_signable: bool) -> Self {
113        self.locally_signable = locally_signable;
114        self
115    }
116}
117
118/// Snapshot used by the existing keychain list/show/doctor commands.
119#[derive(Debug, Default)]
120pub struct AccountsStoreView {
121    pub keys: Vec<KeyEntry>,
122}
123
124/// Return the canonical Tempo Accounts `store.json` path.
125pub fn tempo_accounts_store_path() -> Option<PathBuf> {
126    default_accounts_store_path().ok()
127}
128
129/// Read non-secret metadata from the canonical Tempo Accounts store.
130pub fn read_tempo_accounts_store() -> Option<AccountsStoreView> {
131    let store = match TempoAccountsStore::try_open_default() {
132        Ok(Some(store)) => store,
133        Ok(None) => return None,
134        Err(error) => {
135            tracing::warn!(%error, "failed to open Tempo Accounts store");
136            return None;
137        }
138    };
139    let keys = match store.access_keys() {
140        Ok(keys) => keys,
141        Err(error) => {
142            tracing::warn!(%error, path = %store.path().display(), "failed to read Tempo Accounts access keys");
143            return None;
144        }
145    };
146    Some(AccountsStoreView {
147        keys: keys
148            .into_iter()
149            .map(|key| KeyEntry {
150                wallet_address: key.account(),
151                chain_id: key.chain_id(),
152                key_type: key.key_type().into(),
153                key_address: key.address(),
154                key_authorization: key.key_authorization().cloned(),
155                expiry: key.expiry(),
156                limits: key.limits().iter().map(Into::into).collect(),
157                locally_signable: key.is_locally_signable(),
158            })
159            .collect(),
160    })
161}
162
163/// Decode an RLP key authorization stored by Tempo Accounts.
164pub fn decode_key_authorization<T: Decodable>(encoded: &str) -> eyre::Result<T> {
165    let bytes = hex::decode(encoded)?;
166    let mut bytes = bytes.as_slice();
167    let authorization = T::decode(&mut bytes)?;
168    if !bytes.is_empty() {
169        eyre::bail!("key authorization has trailing bytes");
170    }
171    Ok(authorization)
172}