From 1fb27174fc87e644eaab8259610343d2797053e0 Mon Sep 17 00:00:00 2001 From: KevinMB0220 Date: Tue, 24 Feb 2026 01:41:20 -0600 Subject: [PATCH 1/4] feat(identity-registry): add enumerable expert directory storage Add VerifiedExpertIndex(u64) and TotalVerifiedCount data keys to support on-chain iteration over verified experts. Implement: - add_expert_to_index: appends address at current count index, increments counter (instance storage), persists index entry with TTL - get_total_experts: returns total count from instance storage - get_expert_by_index: retrieves address at a given numeric index Closes #25 --- .../identity-registry-contract/src/storage.rs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/contracts/identity-registry-contract/src/storage.rs b/contracts/identity-registry-contract/src/storage.rs index eae2008..53a1222 100644 --- a/contracts/identity-registry-contract/src/storage.rs +++ b/contracts/identity-registry-contract/src/storage.rs @@ -7,6 +7,8 @@ use soroban_sdk::{contracttype, Address, Env, String}; pub enum DataKey { Admin, Expert(Address), + VerifiedExpertIndex(u64), + TotalVerifiedCount, } // Constants for TTL (Time To Live) @@ -87,3 +89,43 @@ pub fn get_expert_record(env: &Env, expert: &Address) -> ExpertRecord { pub fn get_expert_status(env: &Env, expert: &Address) -> ExpertStatus { get_expert_record(env, expert).status } + +// ... [Expert Directory Index Helpers] ... + +/// Add an expert address to the enumerable index and increment the count +pub fn add_expert_to_index(env: &Env, expert: &Address) { + let count: u64 = env + .storage() + .instance() + .get(&DataKey::TotalVerifiedCount) + .unwrap_or(0u64); + + env.storage() + .persistent() + .set(&DataKey::VerifiedExpertIndex(count), expert); + env.storage().persistent().extend_ttl( + &DataKey::VerifiedExpertIndex(count), + LEDGERS_THRESHOLD, + LEDGERS_EXTEND_TO, + ); + + env.storage() + .instance() + .set(&DataKey::TotalVerifiedCount, &(count + 1)); +} + +/// Get the total number of verified experts ever indexed +pub fn get_total_experts(env: &Env) -> u64 { + env.storage() + .instance() + .get(&DataKey::TotalVerifiedCount) + .unwrap_or(0u64) +} + +/// Get the expert address at the given index +pub fn get_expert_by_index(env: &Env, index: u64) -> Address { + env.storage() + .persistent() + .get(&DataKey::VerifiedExpertIndex(index)) + .expect("Index out of bounds") +} From 4dace81cd316e8d098cb2692a7a907e0758eaa69 Mon Sep 17 00:00:00 2001 From: KevinMB0220 Date: Tue, 24 Feb 2026 01:41:27 -0600 Subject: [PATCH 2/4] feat(identity-registry): index experts on verification Call add_expert_to_index in verify_expert and batch_add_experts only when the expert was not previously Verified, preventing duplicates in the enumerable directory. Expose get_total_experts and get_expert_by_index as contract-level functions. Closes #25 --- .../identity-registry-contract/src/contract.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/contracts/identity-registry-contract/src/contract.rs b/contracts/identity-registry-contract/src/contract.rs index 2972129..051493f 100644 --- a/contracts/identity-registry-contract/src/contract.rs +++ b/contracts/identity-registry-contract/src/contract.rs @@ -1,7 +1,7 @@ -use soroban_sdk::{Address, Env, Vec, String}; -use crate::storage; use crate::events; +use crate::storage; use crate::{error::RegistryError, types::ExpertStatus}; +use soroban_sdk::{Address, Env, String, Vec}; /// Initialize the registry with an admin address pub fn initialize_registry(env: &Env, admin: &Address) -> Result<(), RegistryError> { @@ -16,7 +16,7 @@ pub fn initialize_registry(env: &Env, admin: &Address) -> Result<(), RegistryErr /// Verify an expert by setting their status to Verified (Admin only) /// Batch Verification -pub fn batch_add_experts(env:Env, experts: Vec
) -> Result<(), RegistryError> { +pub fn batch_add_experts(env: Env, experts: Vec
) -> Result<(), RegistryError> { if experts.len() > 20 { return Err(RegistryError::ExpertVecMax); } @@ -32,6 +32,7 @@ pub fn batch_add_experts(env:Env, experts: Vec
) -> Result<(), RegistryE // Default empty URI for batch adds let empty_uri = String::from_str(&env, ""); storage::set_expert_record(&env, &expert, ExpertStatus::Verified, empty_uri); + storage::add_expert_to_index(&env, &expert); events::emit_status_change(&env, expert, status, ExpertStatus::Verified, admin.clone()); } @@ -77,6 +78,7 @@ pub fn verify_expert(env: &Env, expert: &Address, data_uri: String) -> Result<() } storage::set_expert_record(env, expert, ExpertStatus::Verified, data_uri); + storage::add_expert_to_index(env, expert); events::emit_status_change( env, @@ -115,6 +117,16 @@ pub fn ban_expert(env: &Env, expert: &Address) -> Result<(), RegistryError> { Ok(()) } +/// Get the total number of verified experts ever indexed +pub fn get_total_experts(env: &Env) -> u64 { + storage::get_total_experts(env) +} + +/// Get the expert address at the given index +pub fn get_expert_by_index(env: &Env, index: u64) -> Address { + storage::get_expert_by_index(env, index) +} + /// Get the current status of an expert pub fn get_expert_status(env: &Env, expert: &Address) -> ExpertStatus { storage::get_expert_status(env, expert) From c7252ba9f65c40f84912a3d9210d763084809174 Mon Sep 17 00:00:00 2001 From: KevinMB0220 Date: Tue, 24 Feb 2026 01:41:35 -0600 Subject: [PATCH 3/4] feat(identity-registry): expose get_total_experts and get_expert_by_index Add two public contract methods to the IdentityRegistryContract interface so the frontend can enumerate the full verified expert directory by iterating from 0 to total_experts - 1. Closes #25 --- contracts/identity-registry-contract/src/lib.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/contracts/identity-registry-contract/src/lib.rs b/contracts/identity-registry-contract/src/lib.rs index 8beda1a..b226d4b 100644 --- a/contracts/identity-registry-contract/src/lib.rs +++ b/contracts/identity-registry-contract/src/lib.rs @@ -10,7 +10,7 @@ mod types; use crate::error::RegistryError; use crate::types::ExpertStatus; -use soroban_sdk::{contract, contractimpl, Address, Env, Vec, String}; +use soroban_sdk::{contract, contractimpl, Address, Env, String, Vec}; #[contract] pub struct IdentityRegistryContract; @@ -22,7 +22,7 @@ impl IdentityRegistryContract { contract::initialize_registry(&env, &admin) } - /// Batch Add an expert to the whitelist (Admin only) + /// Batch Add an expert to the whitelist (Admin only) pub fn batch_add_experts(env: Env, experts: Vec
) -> Result<(), RegistryError> { contract::batch_add_experts(env, experts) } @@ -43,6 +43,16 @@ impl IdentityRegistryContract { contract::ban_expert(&env, &expert) } + /// Get the total number of verified experts ever added to the directory + pub fn get_total_experts(env: Env) -> u64 { + contract::get_total_experts(&env) + } + + /// Get the expert address at the given index in the directory + pub fn get_expert_by_index(env: Env, index: u64) -> Address { + contract::get_expert_by_index(&env, index) + } + /// Get the current status of an expert pub fn get_status(env: Env, expert: Address) -> ExpertStatus { contract::get_expert_status(&env, &expert) From c7590d4ef5a875492a65424cc0f2fdcb97bde988 Mon Sep 17 00:00:00 2001 From: KevinMB0220 Date: Tue, 24 Feb 2026 01:41:44 -0600 Subject: [PATCH 4/4] test(identity-registry): add expert directory enumeration tests Three new test cases covering the enumerable expert directory: - test_expert_directory_enumeration: verifies 3 experts via add_expert, asserts total == 3 and correct address at indices 0, 1, 2 - test_expert_directory_no_duplicates_on_reverify: confirms AlreadyVerified error and that total stays at 1 when re-verifying the same expert - test_expert_directory_via_batch_add: same index assertions through the batch_add_experts code path Closes #25 --- .../identity-registry-contract/src/test.rs | 167 ++++++++++++++++-- 1 file changed, 151 insertions(+), 16 deletions(-) diff --git a/contracts/identity-registry-contract/src/test.rs b/contracts/identity-registry-contract/src/test.rs index bc3f2a7..41c7195 100644 --- a/contracts/identity-registry-contract/src/test.rs +++ b/contracts/identity-registry-contract/src/test.rs @@ -3,10 +3,12 @@ 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, String}; +use crate::{IdentityRegistryContract, IdentityRegistryContractClient}; use soroban_sdk::testutils::{AuthorizedFunction, AuthorizedInvocation, Events}; +use soroban_sdk::{ + testutils::Address as _, vec, Address, Env, IntoVal, String, Symbol, TryIntoVal, +}; #[test] fn test_initialization() { @@ -113,7 +115,12 @@ fn test_batch_verification_no_admin() { let contract_id = env.register(IdentityRegistryContract, ()); let client = IdentityRegistryContractClient::new(&env, &contract_id); - let experts = vec![&env, Address::generate(&env), Address::generate(&env), Address::generate(&env)]; + let experts = vec![ + &env, + Address::generate(&env), + Address::generate(&env), + Address::generate(&env), + ]; client.batch_add_experts(&experts); } @@ -135,16 +142,38 @@ fn test_batch_verification_check_status() { let e4 = Address::generate(&env); let e5 = Address::generate(&env); - let experts = vec![&env, e1.clone(), e2.clone(), e3.clone(), e4.clone(), e5.clone()]; + let experts = vec![ + &env, + e1.clone(), + e2.clone(), + e3.clone(), + e4.clone(), + e5.clone(), + ]; client.batch_add_experts(&experts); - env.as_contract(&contract_id, ||{ - assert_eq!(storage::get_expert_status(&env, &e1), ExpertStatus::Verified); - assert_eq!(storage::get_expert_status(&env, &e2), ExpertStatus::Verified); - assert_eq!(storage::get_expert_status(&env, &e3), ExpertStatus::Verified); - assert_eq!(storage::get_expert_status(&env, &e4), ExpertStatus::Verified); - assert_eq!(storage::get_expert_status(&env, &e5), ExpertStatus::Verified); + env.as_contract(&contract_id, || { + assert_eq!( + storage::get_expert_status(&env, &e1), + ExpertStatus::Verified + ); + assert_eq!( + storage::get_expert_status(&env, &e2), + ExpertStatus::Verified + ); + assert_eq!( + storage::get_expert_status(&env, &e3), + ExpertStatus::Verified + ); + assert_eq!( + storage::get_expert_status(&env, &e4), + ExpertStatus::Verified + ); + assert_eq!( + storage::get_expert_status(&env, &e5), + ExpertStatus::Verified + ); }) } @@ -165,12 +194,32 @@ fn test_batch_verification_max_vec() { let e3 = Address::generate(&env); let e4 = Address::generate(&env); - let experts = vec![&env, e1.clone(), e2.clone(), e3.clone(), e4.clone(), - e1.clone(), e2.clone(), e3.clone(), e4.clone(), - e1.clone(), e2.clone(), e3.clone(), e4.clone(), - e1.clone(), e2.clone(), e3.clone(), e4.clone(), - e1.clone(), e2.clone(), e3.clone(), e4.clone(), - e1.clone(), e2.clone(), e3.clone(), e4.clone() + let experts = vec![ + &env, + e1.clone(), + e2.clone(), + e3.clone(), + e4.clone(), + e1.clone(), + e2.clone(), + e3.clone(), + e4.clone(), + e1.clone(), + e2.clone(), + e3.clone(), + e4.clone(), + e1.clone(), + e2.clone(), + e3.clone(), + e4.clone(), + e1.clone(), + e2.clone(), + e3.clone(), + e4.clone(), + e1.clone(), + e2.clone(), + e3.clone(), + e4.clone(), ]; client.batch_add_experts(&experts); @@ -446,3 +495,89 @@ fn test_getters() { assert_eq!(client.is_verified(&expert), false); assert_eq!(client.get_status(&expert), ExpertStatus::Banned); } + +#[test] +fn test_expert_directory_enumeration() { + let env = Env::default(); + env.mock_all_auths(); + + 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); + + client.init(&admin); + + // Verify 3 separate experts + let uri1 = String::from_str(&env, "ipfs://e1"); + let uri2 = String::from_str(&env, "ipfs://e2"); + let uri3 = String::from_str(&env, "ipfs://e3"); + client.add_expert(&expert1, &uri1); + client.add_expert(&expert2, &uri2); + client.add_expert(&expert3, &uri3); + + // Total should be 3 + assert_eq!(client.get_total_experts(), 3u64); + + // Indices 0, 1, 2 should return experts in chronological order + assert_eq!(client.get_expert_by_index(&0u64), expert1); + assert_eq!(client.get_expert_by_index(&1u64), expert2); + assert_eq!(client.get_expert_by_index(&2u64), expert3); +} + +#[test] +fn test_expert_directory_no_duplicates_on_reverify() { + let env = Env::default(); + env.mock_all_auths(); + + 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); + + let uri = String::from_str(&env, "ipfs://expert"); + client.add_expert(&expert, &uri); + + // Total is 1 + assert_eq!(client.get_total_experts(), 1u64); + + // Re-verifying an already verified expert returns AlreadyVerified + let result = client.try_add_expert(&expert, &uri); + assert_eq!(result, Err(Ok(RegistryError::AlreadyVerified))); + + // Total remains 1 — no duplicate in the index + assert_eq!(client.get_total_experts(), 1u64); +} + +#[test] +fn test_expert_directory_via_batch_add() { + let env = Env::default(); + env.mock_all_auths(); + + 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); + + client.init(&admin); + + let experts = vec![&env, expert1.clone(), expert2.clone(), expert3.clone()]; + client.batch_add_experts(&experts); + + // Total should be 3 + assert_eq!(client.get_total_experts(), 3u64); + + // Indices should map correctly + assert_eq!(client.get_expert_by_index(&0u64), expert1); + assert_eq!(client.get_expert_by_index(&1u64), expert2); + assert_eq!(client.get_expert_by_index(&2u64), expert3); +}