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
10pub struct ContractAccount {
16 address: Address,
17}
18
19impl ContractAccount {
20 pub fn new(address: Address) -> Self {
22 Self { address }
23 }
24
25 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 if now >= key.scope.expires_at {
86 return Ok(false);
87 }
88
89 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, };
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, 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}