Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions contracts/identity-registry-contract/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,40 @@ pub fn update_profile(env: &Env, expert: &Address, new_uri: String) -> Result<()
events::emit_profile_updated(env, expert.clone(), new_uri);
Ok(())
}

/// Batch update expert profiles (Admin only)
/// Allows admins to update multiple expert metadata URIs in a single transaction
pub fn batch_update_profiles(
env: &Env,
updates: Vec<(Address, String, u32)>,
) -> Result<(), RegistryError> {
// Limit batch size to prevent DoS
if updates.len() > 20 {
return Err(RegistryError::ExpertVecMax);
}

let admin = storage::get_admin(env).ok_or(RegistryError::NotInitialized)?;
admin.require_auth();

for update in updates {
let (expert, new_uri, status_u32) = update;

// Validate URI length
if new_uri.len() > 64 {
return Err(RegistryError::UriTooLong);
}

// Convert u32 to ExpertStatus
let status = match status_u32 {
0 => ExpertStatus::Unverified,
1 => ExpertStatus::Verified,
2 => ExpertStatus::Banned,
_ => return Err(RegistryError::NotVerified), // Invalid status value
};
Comment on lines +184 to +189
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Return a dedicated error for malformed status values.

Line 188 maps an out-of-range status_u32 to RegistryError::NotVerified, which is indistinguishable from a real domain error. Callers cannot tell β€œbad request” from β€œexpert is not verified,” so retries and client-side validation become ambiguous. A separate InvalidStatus-style error would make this API much safer to consume.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/identity-registry-contract/src/contract.rs` around lines 184 - 189,
The match that maps status_u32 to ExpertStatus currently returns
RegistryError::NotVerified on the wildcard arm, conflating malformed input with
a domain state; change the wildcard branch to return a new, dedicated error
variant (e.g., RegistryError::InvalidStatus or RegistryError::MalformedStatus)
instead of NotVerified, add that variant to the RegistryError enum (with any
required Display/From impls), and update any call sites/tests that expect the
old error so callers can distinguish client-side bad input from an actual "not
verified" domain result; the change should be applied where status_u32 is
converted to status (the match producing status) and in the RegistryError type
definition and conversions.


// Update the expert record
storage::set_expert_record(env, &expert, status, new_uri);
}
Comment on lines +175 to +193
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't bypass the registry side effects in the batch path.

On Line 192, this loop only calls storage::set_expert_record, and that helper just overwrites the stored record (contracts/identity-registry-contract/src/storage.rs:46-64). If one of these updates promotes an address to Verified, the expert is never added to the directory index, and none of the status_change / profile_updated events used by the single-record flows are emitted. That leaves get_total_experts / get_expert_by_index and any event-driven sync consumer out of step with the state written by this entrypoint. Please preserve the same indexing/event behavior here, or explicitly restrict this API to URI-only edits.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/identity-registry-contract/src/contract.rs` around lines 175 - 193,
The batch loop currently calls storage::set_expert_record which only overwrites
storage and skips directory indexing and event emission; update the loop to
reuse the same single-record mutation path that maintains the directory index
and emits the status_change/profile_updated events (the same logic used by the
single-record flow) instead of calling storage::set_expert_record directly, so
get_total_experts/get_expert_by_index and event-driven consumers remain
consistent β€” alternatively, if batch must be limited to URI-only edits,
explicitly validate and restrict this endpoint to URI changes and document that
it will not modify index or emit events.


Ok(())
}
9 changes: 9 additions & 0 deletions contracts/identity-registry-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,13 @@ impl IdentityRegistryContract {
pub fn update_profile(env: Env, expert: Address, new_uri: String) -> Result<(), RegistryError> {
contract::update_profile(&env, &expert, new_uri)
}

/// Batch update expert profiles (Admin only)
/// Allows admins to update multiple expert metadata URIs in a single transaction
pub fn batch_update_profiles(
env: Env,
updates: Vec<(Address, String, u32)>,
) -> Result<(), RegistryError> {
contract::batch_update_profiles(&env, updates)
}
}
122 changes: 122 additions & 0 deletions contracts/identity-registry-contract/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -581,3 +581,125 @@ fn test_expert_directory_via_batch_add() {
assert_eq!(client.get_expert_by_index(&1u64), expert2);
assert_eq!(client.get_expert_by_index(&2u64), expert3);
}

#[test]
fn test_batch_update_profiles() {
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);
client.init(&admin);

// Create 5 experts and verify them
let expert1 = Address::generate(&env);
let expert2 = Address::generate(&env);
let expert3 = Address::generate(&env);
let expert4 = Address::generate(&env);
let expert5 = Address::generate(&env);

let uri1 = String::from_str(&env, "ipfs://original1");
let uri2 = String::from_str(&env, "ipfs://original2");
let uri3 = String::from_str(&env, "ipfs://original3");
let uri4 = String::from_str(&env, "ipfs://original4");
let uri5 = String::from_str(&env, "ipfs://original5");

client.add_expert(&expert1, &uri1);
client.add_expert(&expert2, &uri2);
client.add_expert(&expert3, &uri3);
client.add_expert(&expert4, &uri4);
client.add_expert(&expert5, &uri5);

// Prepare batch updates with new URIs
let new_uri1 = String::from_str(&env, "ipfs://updated1");
let new_uri2 = String::from_str(&env, "ipfs://updated2");
let new_uri3 = String::from_str(&env, "ipfs://updated3");
let new_uri4 = String::from_str(&env, "ipfs://updated4");
let new_uri5 = String::from_str(&env, "ipfs://updated5");

let updates = vec![
&env,
(expert1.clone(), new_uri1.clone(), 1u32), // Verified
(expert2.clone(), new_uri2.clone(), 1u32), // Verified
(expert3.clone(), new_uri3.clone(), 1u32), // Verified
(expert4.clone(), new_uri4.clone(), 1u32), // Verified
(expert5.clone(), new_uri5.clone(), 1u32), // Verified
];

// Execute batch update
client.batch_update_profiles(&updates);

// Verify all 5 profiles have the new URIs
env.as_contract(&contract_id, || {
let rec1 = storage::get_expert_record(&env, &expert1);
let rec2 = storage::get_expert_record(&env, &expert2);
let rec3 = storage::get_expert_record(&env, &expert3);
let rec4 = storage::get_expert_record(&env, &expert4);
let rec5 = storage::get_expert_record(&env, &expert5);

assert_eq!(rec1.data_uri, new_uri1);
assert_eq!(rec2.data_uri, new_uri2);
assert_eq!(rec3.data_uri, new_uri3);
assert_eq!(rec4.data_uri, new_uri4);
assert_eq!(rec5.data_uri, new_uri5);

// Verify all remain verified
assert_eq!(rec1.status, ExpertStatus::Verified);
assert_eq!(rec2.status, ExpertStatus::Verified);
assert_eq!(rec3.status, ExpertStatus::Verified);
assert_eq!(rec4.status, ExpertStatus::Verified);
assert_eq!(rec5.status, ExpertStatus::Verified);
});
}

#[test]
#[should_panic(expected = "Error(Contract, #7)")]
fn test_batch_update_profiles_max_vec() {
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);
client.init(&admin);

// Create updates exceeding the limit (>20)
let mut updates = vec![&env];
for _ in 0..21 {
let expert = Address::generate(&env);
let uri = String::from_str(&env, "ipfs://test");
updates.push_back((expert, uri, 1u32));
}

// This should fail with ExpertVecMax error
client.batch_update_profiles(&updates);
}

#[test]
fn test_batch_update_profiles_uri_too_long() {
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);
client.init(&admin);

let expert = Address::generate(&env);
let uri = String::from_str(&env, "ipfs://initial");
client.add_expert(&expert, &uri);

// Create update with URI that's too long (>64 chars)
let long_str = "a".repeat(65);
let long_uri = String::from_str(&env, long_str.as_str());

let updates = vec![&env, (expert.clone(), long_uri, 1u32)];

// This should fail with UriTooLong error
let result = client.try_batch_update_profiles(&updates);
assert_eq!(result, Err(Ok(RegistryError::UriTooLong)));
}
Loading