use soroban_sdk::{Address, BytesN, Env};
use super::error::AccountError;
use super::intent::{AuthResult, SignedIntent};
use super::kernel::AccountKernel;
use super::storage::SessionStorage;
use super::traits::{CougrAccount, IntentAccount, SessionKeyProvider};
use super::types::{AccountCapabilities, GameAction, SessionKey, SessionScope};
pub struct ContractAccount {
address: Address,
}
impl ContractAccount {
pub fn new(address: Address) -> Self {
Self { address }
}
pub fn session_count(&self, env: &Env) -> usize {
SessionStorage::load_all(env, &self.address).len() as usize
}
}
impl CougrAccount for ContractAccount {
fn address(&self) -> &Address {
&self.address
}
fn capabilities(&self) -> AccountCapabilities {
AccountCapabilities {
can_batch: true,
has_session_keys: true,
has_social_recovery: true,
has_passkey_auth: true,
}
}
fn authorize(&self, _env: &Env, action: &GameAction) -> Result<(), AccountError> {
let _ = action;
self.address.require_auth();
Ok(())
}
}
impl IntentAccount for ContractAccount {
fn authorize_intent(
&mut self,
env: &Env,
intent: &SignedIntent,
) -> Result<AuthResult, AccountError> {
AccountKernel::new(self.address.clone()).authorize(env, intent)
}
}
impl SessionKeyProvider for ContractAccount {
fn create_session(
&mut self,
env: &Env,
scope: SessionScope,
) -> Result<SessionKey, AccountError> {
let existing = SessionStorage::load_all(env, &self.address).len();
let key_id = session_key_id(env, existing, &scope);
let key = SessionKey {
key_id,
scope,
created_at: env.ledger().timestamp(),
operations_used: 0,
next_nonce: 0,
};
SessionStorage::store(env, &self.address, &key);
Ok(key)
}
fn validate_session(&self, env: &Env, key: &SessionKey) -> Result<bool, AccountError> {
let now = env.ledger().timestamp();
if now >= key.scope.expires_at {
return Ok(false);
}
if key.operations_used >= key.scope.max_operations {
return Ok(false);
}
if SessionStorage::load(env, &self.address, &key.key_id).is_none() {
return Ok(false);
}
Ok(true)
}
fn revoke_session(&mut self, env: &Env, key_id: &BytesN<32>) -> Result<(), AccountError> {
if !SessionStorage::remove(env, &self.address, key_id) {
return Err(AccountError::InvalidScope);
}
Ok(())
}
}
fn session_key_id(env: &Env, existing_sessions: u32, scope: &SessionScope) -> BytesN<32> {
let mut bytes = [0u8; 32];
bytes[0..8].copy_from_slice(&env.ledger().timestamp().to_be_bytes());
bytes[8..12].copy_from_slice(&env.ledger().sequence().to_be_bytes());
bytes[12..16].copy_from_slice(&existing_sessions.to_be_bytes());
bytes[16..20].copy_from_slice(&(scope.allowed_actions.len()).to_be_bytes());
bytes[20..24].copy_from_slice(&scope.max_operations.to_be_bytes());
bytes[24..32].copy_from_slice(&scope.expires_at.to_be_bytes());
BytesN::from_array(env, &bytes)
}
#[cfg(test)]
mod tests {
use super::*;
use soroban_sdk::{contract, contractimpl, symbol_short, testutils::Address as _, vec, Env};
#[contract]
pub struct TestContract;
#[contractimpl]
impl TestContract {}
#[test]
fn test_contract_account_creation() {
let env = Env::default();
let contract_id = env.register(TestContract, ());
let addr = Address::generate(&env);
env.as_contract(&contract_id, || {
let account = ContractAccount::new(addr.clone());
assert_eq!(*account.address(), addr);
assert_eq!(account.session_count(&env), 0);
});
}
#[test]
fn test_contract_account_capabilities() {
let env = Env::default();
let addr = Address::generate(&env);
let account = ContractAccount::new(addr);
let caps = account.capabilities();
assert!(caps.can_batch);
assert!(caps.has_session_keys);
assert!(caps.has_social_recovery);
}
#[test]
fn test_create_session() {
let env = Env::default();
let contract_id = env.register(TestContract, ());
let addr = Address::generate(&env);
env.as_contract(&contract_id, || {
let mut account = ContractAccount::new(addr);
let scope = SessionScope {
allowed_actions: vec![&env, symbol_short!("move")],
max_operations: 100,
expires_at: 99999,
};
let key = account.create_session(&env, scope).unwrap();
assert_eq!(key.operations_used, 0);
assert_eq!(account.session_count(&env), 1);
});
}
#[test]
fn test_validate_session_active() {
let env = Env::default();
let contract_id = env.register(TestContract, ());
let addr = Address::generate(&env);
env.as_contract(&contract_id, || {
let mut account = ContractAccount::new(addr);
let scope = SessionScope {
allowed_actions: vec![&env, symbol_short!("move")],
max_operations: 100,
expires_at: 99999,
};
let key = account.create_session(&env, scope).unwrap();
assert_eq!(account.validate_session(&env, &key), Ok(true));
});
}
#[test]
fn test_validate_session_expired() {
let env = Env::default();
let contract_id = env.register(TestContract, ());
let addr = Address::generate(&env);
env.as_contract(&contract_id, || {
let mut account = ContractAccount::new(addr);
let scope = SessionScope {
allowed_actions: vec![&env, symbol_short!("move")],
max_operations: 100,
expires_at: 0, };
let key = account.create_session(&env, scope).unwrap();
assert_eq!(account.validate_session(&env, &key), Ok(false));
});
}
#[test]
fn test_validate_session_ops_exhausted() {
let env = Env::default();
let contract_id = env.register(TestContract, ());
let addr = Address::generate(&env);
env.as_contract(&contract_id, || {
let mut account = ContractAccount::new(addr);
let scope = SessionScope {
allowed_actions: vec![&env, symbol_short!("move")],
max_operations: 0, expires_at: 99999,
};
let key = account.create_session(&env, scope).unwrap();
assert_eq!(account.validate_session(&env, &key), Ok(false));
});
}
#[test]
fn test_revoke_session() {
let env = Env::default();
let contract_id = env.register(TestContract, ());
let addr = Address::generate(&env);
env.as_contract(&contract_id, || {
let mut account = ContractAccount::new(addr);
let scope = SessionScope {
allowed_actions: vec![&env, symbol_short!("move")],
max_operations: 100,
expires_at: 99999,
};
let key = account.create_session(&env, scope).unwrap();
assert_eq!(account.session_count(&env), 1);
account.revoke_session(&env, &key.key_id).unwrap();
assert_eq!(account.session_count(&env), 0);
});
}
#[test]
fn test_revoke_nonexistent_session() {
let env = Env::default();
let contract_id = env.register(TestContract, ());
let addr = Address::generate(&env);
env.as_contract(&contract_id, || {
let mut account = ContractAccount::new(addr);
let fake_id = BytesN::from_array(&env, &[99u8; 32]);
assert_eq!(
account.revoke_session(&env, &fake_id),
Err(AccountError::InvalidScope)
);
});
}
}