Skip to main content

cougr_core/accounts/
recovery_storage.rs

1//! Persistent storage for social recovery data.
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, Env, Symbol, Vec};
7
8use super::recovery::{Guardian, RecoveryConfig, RecoveryRequest};
9
10const GUARDIANS_PREFIX: &str = "rcv_guard";
11const CONFIG_PREFIX: &str = "rcv_conf";
12const REQUEST_PREFIX: &str = "rcv_req";
13
14/// Persistent storage for recovery guardians, config, and active requests.
15pub struct RecoveryStorage;
16
17impl RecoveryStorage {
18    // --- Guardians ---
19
20    /// Store the full guardians list for an account (overwrites).
21    pub fn store_guardians(env: &Env, account: &Address, guardians: &Vec<Guardian>) {
22        let key = Self::guardians_key(env, account);
23        env.storage().persistent().set(&key, guardians);
24    }
25
26    /// Load the guardians list. Returns empty vec if none stored.
27    pub fn load_guardians(env: &Env, account: &Address) -> Vec<Guardian> {
28        let key = Self::guardians_key(env, account);
29        env.storage()
30            .persistent()
31            .get(&key)
32            .unwrap_or_else(|| Vec::new(env))
33    }
34
35    // --- Recovery Config ---
36
37    /// Store recovery configuration for an account.
38    pub fn store_config(env: &Env, account: &Address, config: &RecoveryConfig) {
39        let key = Self::config_key(env, account);
40        env.storage().persistent().set(&key, config);
41    }
42
43    /// Load recovery configuration. Returns None if not set.
44    pub fn load_config(env: &Env, account: &Address) -> Option<RecoveryConfig> {
45        let key = Self::config_key(env, account);
46        env.storage().persistent().get(&key)
47    }
48
49    // --- Active Recovery Request ---
50
51    /// Store an active recovery request.
52    pub fn store_request(env: &Env, account: &Address, request: &RecoveryRequest) {
53        let key = Self::request_key(env, account);
54        env.storage().persistent().set(&key, request);
55    }
56
57    /// Load the active recovery request. Returns None if none active.
58    pub fn load_request(env: &Env, account: &Address) -> Option<RecoveryRequest> {
59        let key = Self::request_key(env, account);
60        env.storage().persistent().get(&key)
61    }
62
63    /// Remove the active recovery request.
64    pub fn remove_request(env: &Env, account: &Address) {
65        let key = Self::request_key(env, account);
66        env.storage().persistent().remove(&key);
67    }
68
69    // --- Storage keys ---
70
71    fn guardians_key(env: &Env, account: &Address) -> (Symbol, Address) {
72        (Symbol::new(env, GUARDIANS_PREFIX), account.clone())
73    }
74
75    fn config_key(env: &Env, account: &Address) -> (Symbol, Address) {
76        (Symbol::new(env, CONFIG_PREFIX), account.clone())
77    }
78
79    fn request_key(env: &Env, account: &Address) -> (Symbol, Address) {
80        (Symbol::new(env, REQUEST_PREFIX), account.clone())
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use soroban_sdk::{contract, contractimpl, testutils::Address as _, Env};
88
89    #[contract]
90    pub struct TestContract;
91
92    #[contractimpl]
93    impl TestContract {}
94
95    fn make_config() -> RecoveryConfig {
96        RecoveryConfig {
97            threshold: 2,
98            timelock_period: 100,
99            max_guardians: 5,
100        }
101    }
102
103    #[test]
104    fn test_store_and_load_guardians() {
105        let env = Env::default();
106        let contract_id = env.register(TestContract, ());
107        let addr = Address::generate(&env);
108
109        env.as_contract(&contract_id, || {
110            let mut guardians = Vec::new(&env);
111            guardians.push_back(Guardian {
112                address: Address::generate(&env),
113                added_at: 0,
114            });
115            guardians.push_back(Guardian {
116                address: Address::generate(&env),
117                added_at: 10,
118            });
119
120            RecoveryStorage::store_guardians(&env, &addr, &guardians);
121            let loaded = RecoveryStorage::load_guardians(&env, &addr);
122            assert_eq!(loaded.len(), 2);
123        });
124    }
125
126    #[test]
127    fn test_load_guardians_empty() {
128        let env = Env::default();
129        let contract_id = env.register(TestContract, ());
130        let addr = Address::generate(&env);
131
132        env.as_contract(&contract_id, || {
133            let loaded = RecoveryStorage::load_guardians(&env, &addr);
134            assert_eq!(loaded.len(), 0);
135        });
136    }
137
138    #[test]
139    fn test_store_and_load_config() {
140        let env = Env::default();
141        let contract_id = env.register(TestContract, ());
142        let addr = Address::generate(&env);
143
144        env.as_contract(&contract_id, || {
145            let config = make_config();
146            RecoveryStorage::store_config(&env, &addr, &config);
147
148            let loaded = RecoveryStorage::load_config(&env, &addr).unwrap();
149            assert_eq!(loaded.threshold, 2);
150            assert_eq!(loaded.timelock_period, 100);
151            assert_eq!(loaded.max_guardians, 5);
152        });
153    }
154
155    #[test]
156    fn test_load_config_none() {
157        let env = Env::default();
158        let contract_id = env.register(TestContract, ());
159        let addr = Address::generate(&env);
160
161        env.as_contract(&contract_id, || {
162            assert!(RecoveryStorage::load_config(&env, &addr).is_none());
163        });
164    }
165
166    #[test]
167    fn test_store_and_load_request() {
168        let env = Env::default();
169        let contract_id = env.register(TestContract, ());
170        let addr = Address::generate(&env);
171
172        env.as_contract(&contract_id, || {
173            let request = RecoveryRequest {
174                new_owner: Address::generate(&env),
175                approvals: Vec::new(&env),
176                initiated_at: 50,
177                timelock_until: 150,
178                cancelled: false,
179            };
180
181            RecoveryStorage::store_request(&env, &addr, &request);
182            let loaded = RecoveryStorage::load_request(&env, &addr).unwrap();
183            assert_eq!(loaded.initiated_at, 50);
184            assert_eq!(loaded.timelock_until, 150);
185            assert!(!loaded.cancelled);
186        });
187    }
188
189    #[test]
190    fn test_remove_request() {
191        let env = Env::default();
192        let contract_id = env.register(TestContract, ());
193        let addr = Address::generate(&env);
194
195        env.as_contract(&contract_id, || {
196            let request = RecoveryRequest {
197                new_owner: Address::generate(&env),
198                approvals: Vec::new(&env),
199                initiated_at: 50,
200                timelock_until: 150,
201                cancelled: false,
202            };
203
204            RecoveryStorage::store_request(&env, &addr, &request);
205            assert!(RecoveryStorage::load_request(&env, &addr).is_some());
206
207            RecoveryStorage::remove_request(&env, &addr);
208            assert!(RecoveryStorage::load_request(&env, &addr).is_none());
209        });
210    }
211}