diff --git a/contracts/identity-registry-contract/src/contract.rs b/contracts/identity-registry-contract/src/contract.rs index 6e9f581..9e82c45 100644 --- a/contracts/identity-registry-contract/src/contract.rs +++ b/contracts/identity-registry-contract/src/contract.rs @@ -3,6 +3,7 @@ use crate::storage; use crate::events; use crate::{error::RegistryError, types::ExpertStatus}; +/// Initialize the registry with an admin address pub fn initialize_registry(env: &Env, admin: &Address) -> Result<(), RegistryError> { if storage::has_admin(env) { return Err(RegistryError::AlreadyInitialized); @@ -13,6 +14,7 @@ pub fn initialize_registry(env: &Env, admin: &Address) -> Result<(), RegistryErr Ok(()) } +/// Verify an expert by setting their status to Verified (Admin only) /// Batch Verification pub fn batch_add_experts(env:Env, experts: Vec
) -> Result<(), RegistryError> { if experts.len() > 20 { @@ -36,18 +38,53 @@ pub fn batch_add_experts(env:Env, experts: Vec) -> Result<(), RegistryE pub fn verify_expert(env: &Env, expert: &Address) -> Result<(), RegistryError> { let admin = storage::get_admin(env).ok_or(RegistryError::NotInitialized)?; - + admin.require_auth(); - + let current_status = storage::get_expert_status(env, expert); - + if current_status == ExpertStatus::Verified { return Err(RegistryError::AlreadyVerified); } - + storage::set_expert_record(env, expert, ExpertStatus::Verified); - - events::emit_status_change(env, expert.clone(), current_status, ExpertStatus::Verified, admin); - + + events::emit_status_change( + env, + expert.clone(), + current_status, + ExpertStatus::Verified, + admin, + ); + Ok(()) -} \ No newline at end of file +} + +/// Ban an expert by setting their status to Banned (Admin only) +pub fn ban_expert(env: &Env, expert: &Address) -> Result<(), RegistryError> { + let admin = storage::get_admin(env).ok_or(RegistryError::NotInitialized)?; + admin.require_auth(); + + let current_status = storage::get_expert_status(env, expert); + + if current_status == ExpertStatus::Banned { + return Err(RegistryError::AlreadyBanned); + } + + storage::set_expert_record(env, expert, ExpertStatus::Banned); + + events::emit_status_change( + env, + expert.clone(), + current_status, + ExpertStatus::Banned, + admin, + ); + + Ok(()) +} + +/// Get the current status of an expert +pub fn get_expert_status(env: &Env, expert: &Address) -> ExpertStatus { + storage::get_expert_status(env, expert) +} diff --git a/contracts/identity-registry-contract/src/error.rs b/contracts/identity-registry-contract/src/error.rs index 8bf9322..a29c727 100644 --- a/contracts/identity-registry-contract/src/error.rs +++ b/contracts/identity-registry-contract/src/error.rs @@ -16,4 +16,4 @@ pub enum RegistryError { AlreadyVerified = 5, AlreadyBanned = 6, ExpertVecMax = 7, -} \ No newline at end of file +} diff --git a/contracts/identity-registry-contract/src/events.rs b/contracts/identity-registry-contract/src/events.rs index 7d4065d..faf0fa9 100644 --- a/contracts/identity-registry-contract/src/events.rs +++ b/contracts/identity-registry-contract/src/events.rs @@ -1,5 +1,5 @@ -use soroban_sdk::{contracttype, Address, Env, Symbol}; use crate::types::ExpertStatus; +use soroban_sdk::{contracttype, Address, Env, Symbol}; // The Event Data Structure #[contracttype] @@ -11,13 +11,13 @@ pub struct ExpertStatusChangedEvent { pub admin: Address, } -// Helper function to emit the event +// Helper function to emit the status change event pub fn emit_status_change( - env: &Env, - expert: Address, - old_status: ExpertStatus, - new_status: ExpertStatus, - admin: Address + env: &Env, + expert: Address, + old_status: ExpertStatus, + new_status: ExpertStatus, + admin: Address, ) { let event = ExpertStatusChangedEvent { expert, @@ -25,7 +25,8 @@ pub fn emit_status_change( new_status, admin, }; - + // We publish with the topic "status_change" so indexers can find it easily - env.events().publish((Symbol::new(env, "status_change"),), event); -} \ No newline at end of file + env.events() + .publish((Symbol::new(env, "status_change"),), event); +} diff --git a/contracts/identity-registry-contract/src/lib.rs b/contracts/identity-registry-contract/src/lib.rs index d03207f..0e82929 100644 --- a/contracts/identity-registry-contract/src/lib.rs +++ b/contracts/identity-registry-contract/src/lib.rs @@ -4,12 +4,13 @@ mod contract; mod error; mod events; mod storage; -mod types; #[cfg(test)] mod test; +mod types; -use soroban_sdk::{contract, contractimpl, Address, Env, Vec}; -use crate::{error::RegistryError}; +use crate::error::RegistryError; +use crate::types::ExpertStatus; +use soroban_sdk::{contract, contractimpl, Address, Env,Vec}; #[contract] pub struct IdentityRegistryContract; @@ -30,4 +31,14 @@ impl IdentityRegistryContract { pub fn add_expert(env: Env, expert: Address) -> Result<(), RegistryError> { contract::verify_expert(&env, &expert) } + + /// Ban an expert and revoke their verification status (Admin only) + pub fn ban_expert(env: Env, expert: Address) -> Result<(), RegistryError> { + contract::ban_expert(&env, &expert) + } + + /// Get the current status of an expert + pub fn get_status(env: Env, expert: Address) -> ExpertStatus { + contract::get_expert_status(&env, &expert) + } } diff --git a/contracts/identity-registry-contract/src/storage.rs b/contracts/identity-registry-contract/src/storage.rs index b681d80..68f9c37 100644 --- a/contracts/identity-registry-contract/src/storage.rs +++ b/contracts/identity-registry-contract/src/storage.rs @@ -1,12 +1,12 @@ +use crate::types::{ExpertRecord, ExpertStatus}; use soroban_sdk::{contracttype, Address, Env}; -use crate::types::{ExpertStatus, ExpertRecord}; // 1. Data Keys #[contracttype] #[derive(Clone)] pub enum DataKey { - Admin, - Expert(Address), + Admin, + Expert(Address), } // Constants for TTL (Time To Live) @@ -23,23 +23,27 @@ const LEDGERS_EXTEND_TO: u32 = 6_300_000; // ~1 year // ... [Admin Helpers] ... +/// Check if the admin has been set pub fn has_admin(env: &Env) -> bool { env.storage().instance().has(&DataKey::Admin) } +/// Set the admin address pub fn set_admin(env: &Env, admin: &Address) { env.storage().instance().set(&DataKey::Admin, admin); } +/// Get the admin address pub fn get_admin(env: &Env) -> Option { env.storage().instance().get(&DataKey::Admin) } // ... [Expert Helpers] ... +/// Set the expert record with status and timestamp pub fn set_expert_record(env: &Env, expert: &Address, status: ExpertStatus) { let key = DataKey::Expert(expert.clone()); - + let record = ExpertRecord { status, updated_at: env.ledger().timestamp(), @@ -49,26 +53,23 @@ pub fn set_expert_record(env: &Env, expert: &Address, status: ExpertStatus) { env.storage().persistent().set(&key, &record); // 2. Extend the TTL - // This tells the network: "If this data is going to die in less than 2 months, + // This tells the network: "If this data is going to die in less than 2 months, // extend its life to 1 full year from now." - env.storage().persistent().extend_ttl( - &key, - LEDGERS_THRESHOLD, - LEDGERS_EXTEND_TO - ); + env.storage() + .persistent() + .extend_ttl(&key, LEDGERS_THRESHOLD, LEDGERS_EXTEND_TO); } +/// Get the expert record, extending TTL if exists pub fn get_expert_record(env: &Env, expert: &Address) -> ExpertRecord { let key = DataKey::Expert(expert.clone()); - + // We also bump TTL on reads // If an expert is being checked frequently, they should stay alive. if env.storage().persistent().has(&key) { - env.storage().persistent().extend_ttl( - &key, - LEDGERS_THRESHOLD, - LEDGERS_EXTEND_TO - ); + env.storage() + .persistent() + .extend_ttl(&key, LEDGERS_THRESHOLD, LEDGERS_EXTEND_TO); } env.storage() @@ -80,6 +81,7 @@ pub fn get_expert_record(env: &Env, expert: &Address) -> ExpertRecord { }) } +/// Get the expert status pub fn get_expert_status(env: &Env, expert: &Address) -> ExpertStatus { get_expert_record(env, expert).status -} \ No newline at end of file +} diff --git a/contracts/identity-registry-contract/src/test.rs b/contracts/identity-registry-contract/src/test.rs index b729f57..451e808 100644 --- a/contracts/identity-registry-contract/src/test.rs +++ b/contracts/identity-registry-contract/src/test.rs @@ -2,6 +2,7 @@ extern crate std; +use crate::error::RegistryError; use crate::{IdentityRegistryContract, IdentityRegistryContractClient}; use crate::{storage, types::ExpertStatus}; use soroban_sdk::{Env, testutils::Address as _, Symbol, Address, IntoVal, TryIntoVal, Vec, vec}; @@ -10,7 +11,7 @@ use soroban_sdk::testutils::{AuthorizedFunction, AuthorizedInvocation, Events}; #[test] fn test_initialization() { let env = Env::default(); - let contract_id = env.register_contract(None, IdentityRegistryContract); + let contract_id = env.register(IdentityRegistryContract, ()); let client = IdentityRegistryContractClient::new(&env, &contract_id); // 1. Generate a fake admin address @@ -100,8 +101,8 @@ fn test_batch_verification_max_vec() { fn test_add_expert() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register_contract(None, IdentityRegistryContract); + + let contract_id = env.register(IdentityRegistryContract, ()); let client = IdentityRegistryContractClient::new(&env, &contract_id); let admin = Address::generate(&env); @@ -132,8 +133,8 @@ fn test_add_expert() { #[should_panic] fn test_add_expert_unauthorized() { let env = Env::default(); - - let contract_id = env.register_contract(None, IdentityRegistryContract); + + let contract_id = env.register(IdentityRegistryContract, ()); let client = IdentityRegistryContractClient::new(&env, &contract_id); let admin = Address::generate(&env); @@ -148,8 +149,8 @@ fn test_add_expert_unauthorized() { fn test_expert_status_changed_event() { let env = Env::default(); env.mock_all_auths(); - - let contract_id = env.register_contract(None, IdentityRegistryContract); + + let contract_id = env.register(IdentityRegistryContract, ()); let client = IdentityRegistryContractClient::new(&env, &contract_id); let admin = Address::generate(&env); @@ -162,7 +163,170 @@ fn test_expert_status_changed_event() { let event = events.last().unwrap(); assert_eq!(event.0, contract_id); - + let topic: Symbol = event.1.get(0).unwrap().try_into_val(&env).unwrap(); assert_eq!(topic, Symbol::new(&env, "status_change")); -} \ No newline at end of file +} +#[test] +fn test_ban_expert() { + let env = Env::default(); + let contract_id = env.register(IdentityRegistryContract, ()); + let client = IdentityRegistryContractClient::new(&env, &contract_id); + + // Setup: Create admin and expert addresses + let admin = Address::generate(&env); + let expert = Address::generate(&env); + + // Initialize the contract + client.init(&admin); + + // Verify the expert first + env.mock_all_auths(); + client.add_expert(&expert); + + // Verify status is Verified + let status = client.get_status(&expert); + assert_eq!(status, ExpertStatus::Verified); + + // Ban the expert (should succeed) + client.ban_expert(&expert); + + // Check that status is now Banned + let status = client.get_status(&expert); + assert_eq!(status, ExpertStatus::Banned); + + // Test: Try to ban again (should fail with AlreadyBanned) + let result = client.try_ban_expert(&expert); + assert_eq!(result, Err(Ok(RegistryError::AlreadyBanned))); +} + +#[test] +#[should_panic] +fn test_ban_expert_unauthorized() { + let env = Env::default(); + let contract_id = env.register(IdentityRegistryContract, ()); + let client = IdentityRegistryContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let expert = Address::generate(&env); + + client.init(&admin); + + env.mock_all_auths(); + client.add_expert(&expert); + + env.mock_all_auths_allowing_non_root_auth(); + + env.mock_auths(&[]); + + client.ban_expert(&expert); +} + +#[test] +fn test_ban_unverified_expert() { + let env = Env::default(); + let contract_id = env.register(IdentityRegistryContract, ()); + let client = IdentityRegistryContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let expert = Address::generate(&env); + + // Initialize + client.init(&admin); + + // Verify initial status is Unverified + let status = client.get_status(&expert); + assert_eq!(status, ExpertStatus::Unverified); + + // Ban an expert who was never verified (should still succeed) + env.mock_all_auths(); + client.ban_expert(&expert); + + // Status should be Banned now + let status = client.get_status(&expert); + assert_eq!(status, ExpertStatus::Banned); +} + +#[test] +fn test_ban_expert_workflow() { + let env = Env::default(); + let contract_id = env.register(IdentityRegistryContract, ()); + let client = IdentityRegistryContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let expert1 = Address::generate(&env); + let expert2 = Address::generate(&env); + let expert3 = Address::generate(&env); + + // Initialize + client.init(&admin); + + env.mock_all_auths(); + + // Verify multiple experts + client.add_expert(&expert1); + client.add_expert(&expert2); + client.add_expert(&expert3); + + // Check all are verified + assert_eq!(client.get_status(&expert1), ExpertStatus::Verified); + assert_eq!(client.get_status(&expert2), ExpertStatus::Verified); + assert_eq!(client.get_status(&expert3), ExpertStatus::Verified); + + // Ban expert2 + client.ban_expert(&expert2); + + // Verify expert2 is banned, others remain verified + assert_eq!(client.get_status(&expert1), ExpertStatus::Verified); + assert_eq!(client.get_status(&expert2), ExpertStatus::Banned); + assert_eq!(client.get_status(&expert3), ExpertStatus::Verified); + + // Ban expert1 + client.ban_expert(&expert1); + + // Verify expert1 is now banned + assert_eq!(client.get_status(&expert1), ExpertStatus::Banned); + assert_eq!(client.get_status(&expert2), ExpertStatus::Banned); + assert_eq!(client.get_status(&expert3), ExpertStatus::Verified); +} + +#[test] +fn test_ban_before_contract_initialized() { + let env = Env::default(); + let contract_id = env.register(IdentityRegistryContract, ()); + let client = IdentityRegistryContractClient::new(&env, &contract_id); + + let expert = Address::generate(&env); + + env.mock_all_auths(); + + // Try to ban without initializing (should fail) + let result = client.try_ban_expert(&expert); + assert_eq!(result, Err(Ok(RegistryError::NotInitialized))); +} + +#[test] +fn test_complete_expert_lifecycle() { + let env = Env::default(); + let contract_id = env.register(IdentityRegistryContract, ()); + let client = IdentityRegistryContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let expert = Address::generate(&env); + + // Initialize + client.init(&admin); + + env.mock_all_auths(); + + // 1. Initial state: Unverified + assert_eq!(client.get_status(&expert), ExpertStatus::Unverified); + + // 2. Verify the expert + client.add_expert(&expert); + assert_eq!(client.get_status(&expert), ExpertStatus::Verified); + + // 3. Ban the expert + client.ban_expert(&expert); + assert_eq!(client.get_status(&expert), ExpertStatus::Banned); +} diff --git a/contracts/identity-registry-contract/src/types.rs b/contracts/identity-registry-contract/src/types.rs index 65e3335..a6fbef5 100644 --- a/contracts/identity-registry-contract/src/types.rs +++ b/contracts/identity-registry-contract/src/types.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contracttype, Address}; +use soroban_sdk::contracttype; // 1. Expert Status Enum #[contracttype] @@ -16,4 +16,4 @@ pub enum ExpertStatus { pub struct ExpertRecord { pub status: ExpertStatus, pub updated_at: u64, // Ledger timestamp of the last change -} \ No newline at end of file +}