-
Notifications
You must be signed in to change notification settings - Fork 8
feat(identity-registry): implement Expert Directory enumeration (#25) #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1fb2717
4dace81
c7252ba
c7590d4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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") | ||
| } | ||
|
Comment on lines
+126
to
+131
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing TTL extension on index entry reads — inconsistent with
♻️ Proposed fix pub fn get_expert_by_index(env: &Env, index: u64) -> Address {
+ let key = DataKey::VerifiedExpertIndex(index);
+ if env.storage().persistent().has(&key) {
+ env.storage()
+ .persistent()
+ .extend_ttl(&key, LEDGERS_THRESHOLD, LEDGERS_EXTEND_TO);
+ }
env.storage()
.persistent()
- .get(&DataKey::VerifiedExpertIndex(index))
+ .get(&key)
.expect("Index out of bounds")
}🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
| } | ||
|
Comment on lines
+531
to
+556
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing test for This test only covers the Suggested test#[test]
fn test_expert_directory_no_duplicates_on_reverify_after_ban() {
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);
assert_eq!(client.get_total_experts(), 1u64);
// Ban the expert
client.ban_expert(&expert);
// Re-verify — should NOT create a duplicate index entry
client.add_expert(&expert, &uri);
assert_eq!(client.get_total_experts(), 1u64);
assert_eq!(client.get_expert_by_index(&0u64), expert);
}🤖 Prompt for AI Agents |
||
|
|
||
| #[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); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: LightForgeHub/SkillSphere-Contracts
Length of output: 965
🏁 Script executed:
Repository: LightForgeHub/SkillSphere-Contracts
Length of output: 1061
🏁 Script executed:
Repository: LightForgeHub/SkillSphere-Contracts
Length of output: 961
🏁 Script executed:
Repository: LightForgeHub/SkillSphere-Contracts
Length of output: 862
🏁 Script executed:
Repository: LightForgeHub/SkillSphere-Contracts
Length of output: 61
🏁 Script executed:
Repository: LightForgeHub/SkillSphere-Contracts
Length of output: 459
Append-only index creates duplicate entries when banned experts are re-verified.
The index has no removal path and is purely append-only. More critically, the verification logic allows the state transition
Banned → Verifiedwithout preventing re-indexing. Bothverify_expertandbatch_add_expertsonly guard againstVerified → Verified(checkingif current_status == ExpertStatus::Verified), but they do not blockBanned → Verified. This means a banned expert can be re-verified, causingadd_expert_to_indexto append a duplicate entry.Additionally,
get_total_expertsreturns the total index count (including duplicates), not the count of currently-verified experts, which is misleading.Frontends iterating the index will need to filter both banned experts and duplicate entries client-side.
🤖 Prompt for AI Agents