Skip to main content

cougr_core/accounts/
device_storage.rs

1//! Persistent storage for multi-device key management.
2//!
3//! Follows the same pattern as [`SessionStorage`](super::storage::SessionStorage),
4//! using Soroban's persistent contract storage keyed by account address.
5
6use soroban_sdk::{Address, BytesN, Env, Symbol, Vec};
7
8use super::error::AccountError;
9use super::multi_device::{DeviceKey, DevicePolicy};
10
11const DEVICES_PREFIX: &str = "dev_keys";
12const POLICY_PREFIX: &str = "dev_policy";
13
14/// Persistent storage for device keys and device policy.
15pub struct DeviceStorage;
16
17impl DeviceStorage {
18    // --- Device Keys ---
19
20    /// Store the full device key list for an account (overwrites).
21    pub fn store_devices(env: &Env, account: &Address, devices: &Vec<DeviceKey>) {
22        let key = Self::devices_key(env, account);
23        env.storage().persistent().set(&key, devices);
24    }
25
26    /// Load all device keys. Returns empty vec if none stored.
27    pub fn load_devices(env: &Env, account: &Address) -> Vec<DeviceKey> {
28        let key = Self::devices_key(env, account);
29        env.storage()
30            .persistent()
31            .get(&key)
32            .unwrap_or_else(|| Vec::new(env))
33    }
34
35    // --- Device Policy ---
36
37    /// Store device management policy for an account.
38    pub fn store_policy(env: &Env, account: &Address, policy: &DevicePolicy) {
39        let key = Self::policy_key(env, account);
40        env.storage().persistent().set(&key, policy);
41    }
42
43    /// Load device policy. Returns None if not set.
44    pub fn load_policy(env: &Env, account: &Address) -> Option<DevicePolicy> {
45        let key = Self::policy_key(env, account);
46        env.storage().persistent().get(&key)
47    }
48
49    // --- Helpers ---
50
51    /// Update a specific device key by ID. Loads all devices, applies the
52    /// updater to the matching device, and writes back.
53    pub fn update_device(
54        env: &Env,
55        account: &Address,
56        key_id: &BytesN<32>,
57        updater: impl FnOnce(&mut DeviceKey),
58    ) -> Result<(), AccountError> {
59        let devices = Self::load_devices(env, account);
60        let mut new_devices: Vec<DeviceKey> = Vec::new(env);
61        let mut found = false;
62        let mut updater = Some(updater);
63
64        for i in 0..devices.len() {
65            if let Some(mut d) = devices.get(i) {
66                if &d.key_id == key_id {
67                    found = true;
68                    if let Some(f) = updater.take() {
69                        f(&mut d);
70                    }
71                }
72                new_devices.push_back(d);
73            }
74        }
75
76        if !found {
77            return Err(AccountError::DeviceNotFound);
78        }
79
80        Self::store_devices(env, account, &new_devices);
81        Ok(())
82    }
83
84    // --- Storage keys ---
85
86    fn devices_key(env: &Env, account: &Address) -> (Symbol, Address) {
87        (Symbol::new(env, DEVICES_PREFIX), account.clone())
88    }
89
90    fn policy_key(env: &Env, account: &Address) -> (Symbol, Address) {
91        (Symbol::new(env, POLICY_PREFIX), account.clone())
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use soroban_sdk::{contract, contractimpl, testutils::Address as _, Env};
99
100    #[contract]
101    pub struct TestContract;
102
103    #[contractimpl]
104    impl TestContract {}
105
106    fn make_device(env: &Env, id_byte: u8, name: &str) -> DeviceKey {
107        DeviceKey {
108            key_id: BytesN::from_array(env, &[id_byte; 32]),
109            device_name: Symbol::new(env, name),
110            registered_at: 0,
111            last_used: 0,
112            is_active: true,
113        }
114    }
115
116    #[test]
117    fn test_store_and_load_devices() {
118        let env = Env::default();
119        let contract_id = env.register(TestContract, ());
120        let addr = Address::generate(&env);
121
122        env.as_contract(&contract_id, || {
123            let mut devices = Vec::new(&env);
124            devices.push_back(make_device(&env, 1, "phone"));
125            devices.push_back(make_device(&env, 2, "laptop"));
126
127            DeviceStorage::store_devices(&env, &addr, &devices);
128            let loaded = DeviceStorage::load_devices(&env, &addr);
129            assert_eq!(loaded.len(), 2);
130        });
131    }
132
133    #[test]
134    fn test_load_devices_empty() {
135        let env = Env::default();
136        let contract_id = env.register(TestContract, ());
137        let addr = Address::generate(&env);
138
139        env.as_contract(&contract_id, || {
140            let loaded = DeviceStorage::load_devices(&env, &addr);
141            assert_eq!(loaded.len(), 0);
142        });
143    }
144
145    #[test]
146    fn test_store_and_load_policy() {
147        let env = Env::default();
148        let contract_id = env.register(TestContract, ());
149        let addr = Address::generate(&env);
150
151        env.as_contract(&contract_id, || {
152            let policy = DevicePolicy {
153                max_devices: 5,
154                auto_revoke_after: 1000,
155            };
156            DeviceStorage::store_policy(&env, &addr, &policy);
157
158            let loaded = DeviceStorage::load_policy(&env, &addr).unwrap();
159            assert_eq!(loaded.max_devices, 5);
160            assert_eq!(loaded.auto_revoke_after, 1000);
161        });
162    }
163
164    #[test]
165    fn test_load_policy_none() {
166        let env = Env::default();
167        let contract_id = env.register(TestContract, ());
168        let addr = Address::generate(&env);
169
170        env.as_contract(&contract_id, || {
171            assert!(DeviceStorage::load_policy(&env, &addr).is_none());
172        });
173    }
174
175    #[test]
176    fn test_update_device() {
177        let env = Env::default();
178        let contract_id = env.register(TestContract, ());
179        let addr = Address::generate(&env);
180
181        env.as_contract(&contract_id, || {
182            let mut devices = Vec::new(&env);
183            devices.push_back(make_device(&env, 1, "phone"));
184
185            DeviceStorage::store_devices(&env, &addr, &devices);
186
187            let key_id = BytesN::from_array(&env, &[1u8; 32]);
188            DeviceStorage::update_device(&env, &addr, &key_id, |d| {
189                d.last_used = 999;
190            })
191            .unwrap();
192
193            let loaded = DeviceStorage::load_devices(&env, &addr);
194            assert_eq!(loaded.get(0).unwrap().last_used, 999);
195        });
196    }
197}