Skip to content
Merged
42 changes: 36 additions & 6 deletions contracts/identity-registry-contract/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use soroban_sdk::{Address, Env, Vec};
use soroban_sdk::{Address, Env, Vec, String};
use crate::storage;
use crate::events;
use crate::{error::RegistryError, types::ExpertStatus};
Expand Down Expand Up @@ -29,7 +29,9 @@ pub fn batch_add_experts(env:Env, experts: Vec<Address>) -> Result<(), RegistryE
if status == ExpertStatus::Verified {
return Err(RegistryError::AlreadyVerified);
}
storage::set_expert_record(&env, &expert, ExpertStatus::Verified);
// Default empty URI for batch adds
let empty_uri = String::from_str(&env, "");
storage::set_expert_record(&env, &expert, ExpertStatus::Verified, empty_uri);
events::emit_status_change(&env, expert, status, ExpertStatus::Verified, admin.clone());
}

Expand All @@ -50,14 +52,15 @@ pub fn batch_ban_experts(env: Env, experts: Vec<Address>) -> Result<(), Registry
if status == ExpertStatus::Banned {
return Err(RegistryError::AlreadyBanned);
}
storage::set_expert_record(&env, &expert, ExpertStatus::Banned);
let existing = storage::get_expert_record(&env, &expert);
storage::set_expert_record(&env, &expert, ExpertStatus::Banned, existing.data_uri);
events::emit_status_change(&env, expert, status, ExpertStatus::Banned, admin.clone());
}

Ok(())
}

pub fn verify_expert(env: &Env, expert: &Address) -> Result<(), RegistryError> {
pub fn verify_expert(env: &Env, expert: &Address, data_uri: String) -> Result<(), RegistryError> {
let admin = storage::get_admin(env).ok_or(RegistryError::NotInitialized)?;

admin.require_auth();
Expand All @@ -68,7 +71,12 @@ pub fn verify_expert(env: &Env, expert: &Address) -> Result<(), RegistryError> {
return Err(RegistryError::AlreadyVerified);
}

storage::set_expert_record(env, expert, ExpertStatus::Verified);
// Validate URI length (limit ~64 chars)
if data_uri.len() > 64 {
return Err(RegistryError::UriTooLong);
}

storage::set_expert_record(env, expert, ExpertStatus::Verified, data_uri);

events::emit_status_change(
env,
Expand All @@ -92,7 +100,9 @@ pub fn ban_expert(env: &Env, expert: &Address) -> Result<(), RegistryError> {
return Err(RegistryError::AlreadyBanned);
}

storage::set_expert_record(env, expert, ExpertStatus::Banned);
// Preserve existing data_uri when banning
let existing = storage::get_expert_record(env, expert);
storage::set_expert_record(env, expert, ExpertStatus::Banned, existing.data_uri);

events::emit_status_change(
env,
Expand All @@ -115,3 +125,23 @@ pub fn get_expert_status(env: &Env, expert: &Address) -> ExpertStatus {
pub fn is_verified(env: &Env, expert: &Address) -> bool {
storage::get_expert_status(env, expert) == ExpertStatus::Verified
}

/// Allow a verified expert to update their own profile URI
pub fn update_profile(env: &Env, expert: &Address, new_uri: String) -> Result<(), RegistryError> {
expert.require_auth();

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

let status = storage::get_expert_status(env, expert);
if status != ExpertStatus::Verified {
return Err(RegistryError::NotVerified);
}

// Update record preserving status
storage::set_expert_record(env, expert, status, new_uri.clone());
events::emit_profile_updated(env, expert.clone(), new_uri);
Ok(())
}
2 changes: 2 additions & 0 deletions contracts/identity-registry-contract/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ pub enum RegistryError {
AlreadyVerified = 5,
AlreadyBanned = 6,
ExpertVecMax = 7,
NotVerified = 8,
UriTooLong = 9,
}
20 changes: 18 additions & 2 deletions contracts/identity-registry-contract/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::types::ExpertStatus;
use soroban_sdk::{contracttype, Address, Env, Symbol};
use soroban_sdk::{contracttype, Address, Env, Symbol, String};

// The Event Data Structure
#[contracttype]
Expand All @@ -12,6 +12,7 @@ pub struct ExpertStatusChangedEvent {
}

// Helper function to emit the status change event
#[allow(deprecated)]
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

soroban-sdk events publish deprecated 2025

πŸ’‘ Result:

In Aug 2025, the Rust Soroban SDK (rs-soroban-sdk) deprecated the old Events::publish(...) API (i.e., env.events().publish(...)). The deprecation landed in PR #1524 (merged Aug 6, 2025) and is called out in the v23.0.0-rc.3 release notes (published Aug 28, 2025) as β€œDeprecate old events publish method”. [1][2]

Replacement (Protocol 23 / β€œWhisk” pattern): define events with the #[contractevent] macro and publish by calling publish(&env) on the event value/struct (so the event is included in the contract spec). [1][3]

Example from Stellar docs:

#[contractevent(topics = ["COUNTER", "increment"], data_format = "single-value")]
struct IncrementEvent { count: u32 }

IncrementEvent { count }.publish(&env);

[3]

Why: the #[contractevent] approach is now preferred because it embeds event definitions into the contract spec, enabling downstream tooling/bindings for events. [1]

Sources:
[1] stellar/rs-soroban-sdk#1524
[2] https://github.com/stellar/rs-soroban-sdk/releases/tag/v23.0.0-rc.3
[3] https://developers.stellar.org/docs/build/smart-contracts/example-contracts/events


🏁 Script executed:

fd -e toml -type f | head -20

Repository: LightForgeHub/SkillSphere-Contracts

Length of output: 249


🏁 Script executed:

find . -name "Cargo.toml" -type f

Repository: LightForgeHub/SkillSphere-Contracts

Length of output: 187


🏁 Script executed:

cat -n Cargo.toml 2>/dev/null || cat -n contracts/Cargo.toml 2>/dev/null || find . -name "Cargo.toml" -exec cat -n {} \;

Repository: LightForgeHub/SkillSphere-Contracts

Length of output: 743


🏁 Script executed:

cat -n contracts/identity-registry-contract/src/events.rs

Repository: LightForgeHub/SkillSphere-Contracts

Length of output: 1665


Refactor event emission to use the modern #[contractevent] macro pattern.

The env.events().publish() API was deprecated in soroban-sdk v23.0.0-rc.3 and is replaced by the #[contractevent] macro pattern. Your project uses soroban-sdk 23.1.1, where this deprecation is active. Remove the #[allow(deprecated)] suppression and refactor both ExpertStatusChangedEvent and ProfileUpdatedEvent to use #[contractevent]:

#[contractevent(topics = ["status_change"])]
pub struct ExpertStatusChangedEvent {
    pub expert: Address,
    pub old_status: ExpertStatus,
    pub new_status: ExpertStatus,
    pub admin: Address,
}

pub fn emit_status_change(env: &Env, expert: Address, old_status: ExpertStatus, new_status: ExpertStatus, admin: Address) {
    ExpertStatusChangedEvent { expert, old_status, new_status, admin }.publish(env);
}

This approach embeds event definitions in the contract spec and enables downstream tooling. Apply the same pattern to ProfileUpdatedEvent with topics = ["profile_updated"].

πŸ€– Prompt for AI Agents
In `@contracts/identity-registry-contract/src/events.rs` at line 15, Remove the
#[allow(deprecated)] suppression and refactor the events to use the new
#[contractevent] macro: annotate ExpertStatusChangedEvent with
#[contractevent(topics = ["status_change"])] and ProfileUpdatedEvent with
#[contractevent(topics = ["profile_updated"])], keep the same fields (Address,
ExpertStatus, etc.), then replace calls that used env.events().publish(...) with
the new pattern by constructing the event struct and calling .publish(env)
inside the existing emit functions (e.g., emit_status_change and
emit_profile_updated) so they accept &Env and call ExpertStatusChangedEvent {
... }.publish(env) and ProfileUpdatedEvent { ... }.publish(env) respectively.

pub fn emit_status_change(
env: &Env,
expert: Address,
Expand All @@ -26,7 +27,22 @@ pub fn emit_status_change(
admin,
};

// We publish with the topic "status_change" so indexers can find it easily
// published with the topic "status_change" so indexers can find it easily
env.events()
.publish((Symbol::new(env, "status_change"),), event);
}

// Event for profile URI updates
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProfileUpdatedEvent {
pub expert: Address,
pub new_uri: String,
}

#[allow(deprecated)]
pub fn emit_profile_updated(env: &Env, expert: Address, new_uri: String) {
let event = ProfileUpdatedEvent { expert, new_uri };
env.events()
.publish((Symbol::new(env, "profile_updated"),), event);
}
12 changes: 9 additions & 3 deletions contracts/identity-registry-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ mod types;

use crate::error::RegistryError;
use crate::types::ExpertStatus;
use soroban_sdk::{contract, contractimpl, Address, Env,Vec};
use soroban_sdk::{contract, contractimpl, Address, Env, Vec, String};

#[contract]
pub struct IdentityRegistryContract;
Expand All @@ -33,8 +33,9 @@ impl IdentityRegistryContract {
}

/// Add an expert to the whitelist (Admin only)
pub fn add_expert(env: Env, expert: Address) -> Result<(), RegistryError> {
contract::verify_expert(&env, &expert)
/// Also saves a profile data_uri reference (e.g., ipfs://...)
pub fn add_expert(env: Env, expert: Address, data_uri: String) -> Result<(), RegistryError> {
contract::verify_expert(&env, &expert, data_uri)
}

/// Ban an expert and revoke their verification status (Admin only)
Expand All @@ -52,4 +53,9 @@ impl IdentityRegistryContract {
pub fn is_verified(env: Env, expert: Address) -> bool {
contract::is_verified(&env, &expert)
}

/// Allow a verified expert to update their own profile URI
pub fn update_profile(env: Env, expert: Address, new_uri: String) -> Result<(), RegistryError> {
contract::update_profile(&env, &expert, new_uri)
}
}
14 changes: 8 additions & 6 deletions contracts/identity-registry-contract/src/storage.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::types::{ExpertRecord, ExpertStatus};
use soroban_sdk::{contracttype, Address, Env};
use soroban_sdk::{contracttype, Address, Env, String};

// 1. Data Keys
#[contracttype]
Expand All @@ -14,12 +14,12 @@ pub enum DataKey {
// 1 Year in seconds = 31,536,000
// 1 Year in ledgers = ~6,307,200 (approx)
//
// However, Soroban allows setting TTL logic relative to the current ledger.
// Soroban allows setting TTL logic relative to the current ledger.
// "Threshold": If remaining lifetime is less than this...
// "Extend": ...bump it up to this amount.

const LEDGERS_THRESHOLD: u32 = 1_000_000; // ~2 months
const LEDGERS_EXTEND_TO: u32 = 6_300_000; // ~1 year
const LEDGERS_THRESHOLD: u32 = 1_000_000; // 2 months
const LEDGERS_EXTEND_TO: u32 = 6_300_000; // 1 year

// ... [Admin Helpers] ...

Expand All @@ -40,13 +40,14 @@ pub fn get_admin(env: &Env) -> Option<Address> {

// ... [Expert Helpers] ...

/// Set the expert record with status and timestamp
pub fn set_expert_record(env: &Env, expert: &Address, status: ExpertStatus) {
/// Set the expert record with status, data_uri and timestamp
pub fn set_expert_record(env: &Env, expert: &Address, status: ExpertStatus, data_uri: String) {
let key = DataKey::Expert(expert.clone());

let record = ExpertRecord {
status,
updated_at: env.ledger().timestamp(),
data_uri,
};

// 1. Save the data
Expand Down Expand Up @@ -78,6 +79,7 @@ pub fn get_expert_record(env: &Env, expert: &Address) -> ExpertRecord {
.unwrap_or(ExpertRecord {
status: ExpertStatus::Unverified,
updated_at: 0,
data_uri: String::from_str(env, ""),
})
}

Expand Down
Loading