Skip to main content

cougr_core/accounts/
contract.rs

1use soroban_sdk::{Address, BytesN, Env};
2
3use super::error::AccountError;
4use super::intent::{AuthResult, SignedIntent};
5use super::kernel::AccountKernel;
6use super::storage::SessionStorage;
7use super::traits::{CougrAccount, IntentAccount, SessionKeyProvider};
8use super::types::{AccountCapabilities, GameAction, SessionKey, SessionScope};
9
10/// A Contract Stellar account (C-address).
11///
12/// Wraps a contract address and provides full account abstraction
13/// including session key management. Session keys are persisted via
14/// [`SessionStorage`] so they survive across contract invocations.
15pub struct ContractAccount {
16    address: Address,
17}
18
19impl ContractAccount {
20    /// Create a new Contract account wrapper.
21    pub fn new(address: Address) -> Self {
22        Self { address }
23    }
24
25    /// Returns the number of active session keys.
26    pub fn session_count(&self, env: &Env) -> usize {
27        SessionStorage::load_all(env, &self.address).len() as usize
28    }
29}
30
31impl CougrAccount for ContractAccount {
32    fn address(&self) -> &Address {
33        &self.address
34    }
35
36    fn capabilities(&self) -> AccountCapabilities {
37        AccountCapabilities {
38            can_batch: true,
39            has_session_keys: true,
40            has_social_recovery: true,
41            has_passkey_auth: true,
42        }
43    }
44
45    fn authorize(&self, _env: &Env, action: &GameAction) -> Result<(), AccountError> {
46        let _ = action;
47        self.address.require_auth();
48        Ok(())
49    }
50}
51
52impl IntentAccount for ContractAccount {
53    fn authorize_intent(
54        &mut self,
55        env: &Env,
56        intent: &SignedIntent,
57    ) -> Result<AuthResult, AccountError> {
58        AccountKernel::new(self.address.clone()).authorize(env, intent)
59    }
60}
61
62impl SessionKeyProvider for ContractAccount {
63    fn create_session(
64        &mut self,
65        env: &Env,
66        scope: SessionScope,
67    ) -> Result<SessionKey, AccountError> {
68        let existing = SessionStorage::load_all(env, &self.address).len();
69        let key_id = session_key_id(env, existing, &scope);
70        let key = SessionKey {
71            key_id,
72            scope,
73            created_at: env.ledger().timestamp(),
74            operations_used: 0,
75            next_nonce: 0,
76        };
77        SessionStorage::store(env, &self.address, &key);
78        Ok(key)
79    }
80
81    fn validate_session(&self, env: &Env, key: &SessionKey) -> Result<bool, AccountError> {
82        let now = env.ledger().timestamp();
83
84        // Check expiration
85        if now >= key.scope.expires_at {
86            return Ok(false);
87        }
88
89        // Check operation limit
90        if key.operations_used >= key.scope.max_operations {
91            return Ok(false);
92        }
93
94        if SessionStorage::load(env, &self.address, &key.key_id).is_none() {
95            return Ok(false);
96        }
97
98        Ok(true)
99    }
100
101    fn revoke_session(&mut self, env: &Env, key_id: &BytesN<32>) -> Result<(), AccountError> {
102        if !SessionStorage::remove(env, &self.address, key_id) {
103            return Err(AccountError::InvalidScope);
104        }
105        Ok(())
106    }
107}
108
109fn session_key_id(env: &Env, existing_sessions: u32, scope: &SessionScope) -> BytesN<32> {
110    let mut bytes = [0u8; 32];
111    bytes[0..8].copy_from_slice(&env.ledger().timestamp().to_be_bytes());
112    bytes[8..12].copy_from_slice(&env.ledger().sequence().to_be_bytes());
113    bytes[12..16].copy_from_slice(&existing_sessions.to_be_bytes());
114    bytes[16..20].copy_from_slice(&(scope.allowed_actions.len()).to_be_bytes());
115    bytes[20..24].copy_from_slice(&scope.max_operations.to_be_bytes());
116    bytes[24..32].copy_from_slice(&scope.expires_at.to_be_bytes());
117    BytesN::from_array(env, &bytes)
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use soroban_sdk::{contract, contractimpl, symbol_short, testutils::Address as _, vec, Env};
124
125    #[contract]
126    pub struct TestContract;
127
128    #[contractimpl]
129    impl TestContract {}
130
131    #[test]
132    fn test_contract_account_creation() {
133        let env = Env::default();
134        let contract_id = env.register(TestContract, ());
135        let addr = Address::generate(&env);
136        env.as_contract(&contract_id, || {
137            let account = ContractAccount::new(addr.clone());
138            assert_eq!(*account.address(), addr);
139            assert_eq!(account.session_count(&env), 0);
140        });
141    }
142
143    #[test]
144    fn test_contract_account_capabilities() {
145        let env = Env::default();
146        let addr = Address::generate(&env);
147        let account = ContractAccount::new(addr);
148        let caps = account.capabilities();
149        assert!(caps.can_batch);
150        assert!(caps.has_session_keys);
151        assert!(caps.has_social_recovery);
152    }
153
154    #[test]
155    fn test_create_session() {
156        let env = Env::default();
157        let contract_id = env.register(TestContract, ());
158        let addr = Address::generate(&env);
159
160        env.as_contract(&contract_id, || {
161            let mut account = ContractAccount::new(addr);
162
163            let scope = SessionScope {
164                allowed_actions: vec![&env, symbol_short!("move")],
165                max_operations: 100,
166                expires_at: 99999,
167            };
168
169            let key = account.create_session(&env, scope).unwrap();
170            assert_eq!(key.operations_used, 0);
171            assert_eq!(account.session_count(&env), 1);
172        });
173    }
174
175    #[test]
176    fn test_validate_session_active() {
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 account = ContractAccount::new(addr);
183
184            let scope = SessionScope {
185                allowed_actions: vec![&env, symbol_short!("move")],
186                max_operations: 100,
187                expires_at: 99999,
188            };
189
190            let key = account.create_session(&env, scope).unwrap();
191            assert_eq!(account.validate_session(&env, &key), Ok(true));
192        });
193    }
194
195    #[test]
196    fn test_validate_session_expired() {
197        let env = Env::default();
198        let contract_id = env.register(TestContract, ());
199        let addr = Address::generate(&env);
200
201        env.as_contract(&contract_id, || {
202            let mut account = ContractAccount::new(addr);
203
204            let scope = SessionScope {
205                allowed_actions: vec![&env, symbol_short!("move")],
206                max_operations: 100,
207                expires_at: 0, // Already expired
208            };
209
210            let key = account.create_session(&env, scope).unwrap();
211            assert_eq!(account.validate_session(&env, &key), Ok(false));
212        });
213    }
214
215    #[test]
216    fn test_validate_session_ops_exhausted() {
217        let env = Env::default();
218        let contract_id = env.register(TestContract, ());
219        let addr = Address::generate(&env);
220
221        env.as_contract(&contract_id, || {
222            let mut account = ContractAccount::new(addr);
223
224            let scope = SessionScope {
225                allowed_actions: vec![&env, symbol_short!("move")],
226                max_operations: 0, // No operations allowed
227                expires_at: 99999,
228            };
229
230            let key = account.create_session(&env, scope).unwrap();
231            assert_eq!(account.validate_session(&env, &key), Ok(false));
232        });
233    }
234
235    #[test]
236    fn test_revoke_session() {
237        let env = Env::default();
238        let contract_id = env.register(TestContract, ());
239        let addr = Address::generate(&env);
240
241        env.as_contract(&contract_id, || {
242            let mut account = ContractAccount::new(addr);
243
244            let scope = SessionScope {
245                allowed_actions: vec![&env, symbol_short!("move")],
246                max_operations: 100,
247                expires_at: 99999,
248            };
249
250            let key = account.create_session(&env, scope).unwrap();
251            assert_eq!(account.session_count(&env), 1);
252
253            account.revoke_session(&env, &key.key_id).unwrap();
254            assert_eq!(account.session_count(&env), 0);
255        });
256    }
257
258    #[test]
259    fn test_revoke_nonexistent_session() {
260        let env = Env::default();
261        let contract_id = env.register(TestContract, ());
262        let addr = Address::generate(&env);
263
264        env.as_contract(&contract_id, || {
265            let mut account = ContractAccount::new(addr);
266
267            let fake_id = BytesN::from_array(&env, &[99u8; 32]);
268            assert_eq!(
269                account.revoke_session(&env, &fake_id),
270                Err(AccountError::InvalidScope)
271            );
272        });
273    }
274}