From c91ae0ff7deafeddfde88dcf314d272f6f0effbf Mon Sep 17 00:00:00 2001 From: twmoonboy <108098442+Mrchinedum@users.noreply.github.com> Date: Sun, 26 Apr 2026 17:31:59 +0000 Subject: [PATCH] feat: implement module interface standardization - Add docs/MODULE_INTERFACE_STANDARDS.md defining 8 rules: module-level doc comment, manager-struct pattern, section comments, #[must_use] on pure getters, error handling, auth pattern, compliance table, and a minimal example module - Refactor reputation.rs: free functions -> ReputationManager struct, add module doc, section comments (Mutations/Queries/Internal), #[must_use] on get_reputation - Add module doc, Mutations/Queries section comments, and #[must_use] on all 3 getters in score.rs - Add module doc, standardize section comments (Initialization/Mutations/Admin/Queries), and #[must_use] on all 5 getters in rewards.rs - Update all 4 reputation call sites in lib.rs from free functions to ReputationManager::* methods --- contracts/teachlink/src/events.rs | 9 +- contracts/teachlink/src/lib.rs | 8 +- contracts/teachlink/src/reputation.rs | 185 ++++++++++++++------------ contracts/teachlink/src/rewards.rs | 31 +++-- contracts/teachlink/src/score.rs | 15 +++ docs/MODULE_INTERFACE_STANDARDS.md | 141 ++++++++++++++++++++ 6 files changed, 286 insertions(+), 103 deletions(-) create mode 100644 docs/MODULE_INTERFACE_STANDARDS.md diff --git a/contracts/teachlink/src/events.rs b/contracts/teachlink/src/events.rs index 6ea1689b..e7e1802f 100644 --- a/contracts/teachlink/src/events.rs +++ b/contracts/teachlink/src/events.rs @@ -712,7 +712,14 @@ pub struct ChainMetricsUpdatedEvent { pub average_fee: i128, pub updated_at: u64, } - +Description +No comprehensive monitoring dashboard exists. + +Acceptance Criteria + Create real-time metrics + Track historical trends + Manage alerts + Provide insights /// Emitted when sustainability metrics are updated. #[contractevent] #[derive(Clone, Debug)] diff --git a/contracts/teachlink/src/lib.rs b/contracts/teachlink/src/lib.rs index 66c3dcf1..62719c93 100644 --- a/contracts/teachlink/src/lib.rs +++ b/contracts/teachlink/src/lib.rs @@ -1333,19 +1333,19 @@ impl TeachLinkBridge { // ========== Reputation Functions (main) ========== pub fn update_participation(env: Env, user: Address, points: u32) { - reputation::update_participation(&env, user, points); + reputation::ReputationManager::update_participation(&env, user, points); } pub fn update_course_progress(env: Env, user: Address, is_completion: bool) { - reputation::update_course_progress(&env, user, is_completion); + reputation::ReputationManager::update_course_progress(&env, user, is_completion); } pub fn rate_contribution(env: Env, user: Address, rating: u32) { - reputation::rate_contribution(&env, user, rating); + reputation::ReputationManager::rate_contribution(&env, user, rating); } pub fn get_user_reputation(env: Env, user: Address) -> types::UserReputation { - reputation::get_reputation(&env, &user) + reputation::ReputationManager::get_reputation(&env, &user) } // ========== Content Tokenization Functions ========== diff --git a/contracts/teachlink/src/reputation.rs b/contracts/teachlink/src/reputation.rs index dc7206b4..622d0900 100644 --- a/contracts/teachlink/src/reputation.rs +++ b/contracts/teachlink/src/reputation.rs @@ -1,3 +1,11 @@ +//! User reputation tracking. +//! +//! Responsibilities: +//! - Track participation score, course progress, and contribution quality +//! - Compute completion rate in basis points +//! - Emit events on every state change +//! - Expose read-only views for reputation data + use crate::events::{ ContributionRatedEvent, CourseProgressUpdatedEvent, ParticipationUpdatedEvent, }; @@ -7,102 +15,107 @@ use soroban_sdk::{symbol_short, Address, Env, Symbol}; const BASIS_POINTS: u32 = 10000; const REPUTATION: Symbol = symbol_short!("reptn"); -pub fn update_participation(env: &Env, user: Address, points: u32) { - user.require_auth(); - let mut reputation = get_reputation(env, &user); - reputation.participation_score += points; - reputation.last_update = env.ledger().timestamp(); - set_reputation(env, &user, &reputation); - - // Emit event - ParticipationUpdatedEvent { - user: user.clone(), - points_added: points, - new_participation_score: reputation.participation_score, - updated_at: env.ledger().timestamp(), +/// Manages user reputation scores. +pub struct ReputationManager; + +impl ReputationManager { + // ===== Mutations ===== + + /// Add participation points for a user. + pub fn update_participation(env: &Env, user: Address, points: u32) { + user.require_auth(); + let mut reputation = Self::get_reputation(env, &user); + reputation.participation_score += points; + reputation.last_update = env.ledger().timestamp(); + Self::set_reputation(env, &user, &reputation); + + ParticipationUpdatedEvent { + user: user.clone(), + points_added: points, + new_participation_score: reputation.participation_score, + updated_at: reputation.last_update, + } + .publish(env); } - .publish(env); -} -pub fn update_course_progress(env: &Env, user: Address, is_completion: bool) { - user.require_auth(); - let mut reputation = get_reputation(env, &user); - - if is_completion { - reputation.total_courses_completed += 1; - // Logic: You can't complete a course without starting it, - // but simple increment here assumes course started logic handled elsewhere or previously - if reputation.total_courses_started < reputation.total_courses_completed { - reputation.total_courses_started = reputation.total_courses_completed; + /// Record a course start or completion for a user. + pub fn update_course_progress(env: &Env, user: Address, is_completion: bool) { + user.require_auth(); + let mut reputation = Self::get_reputation(env, &user); + + if is_completion { + reputation.total_courses_completed += 1; + if reputation.total_courses_started < reputation.total_courses_completed { + reputation.total_courses_started = reputation.total_courses_completed; + } + } else { + reputation.total_courses_started += 1; } - } else { - reputation.total_courses_started += 1; - } - if reputation.total_courses_started > 0 { - reputation.completion_rate = - (reputation.total_courses_completed * BASIS_POINTS) / reputation.total_courses_started; - } + if reputation.total_courses_started > 0 { + reputation.completion_rate = (reputation.total_courses_completed * BASIS_POINTS) + / reputation.total_courses_started; + } - reputation.last_update = env.ledger().timestamp(); - set_reputation(env, &user, &reputation); + reputation.last_update = env.ledger().timestamp(); + Self::set_reputation(env, &user, &reputation); - // Emit event - CourseProgressUpdatedEvent { - user: user.clone(), - total_courses_started: reputation.total_courses_started, - total_courses_completed: reputation.total_courses_completed, - completion_rate: reputation.completion_rate, - updated_at: env.ledger().timestamp(), + CourseProgressUpdatedEvent { + user: user.clone(), + total_courses_started: reputation.total_courses_started, + total_courses_completed: reputation.total_courses_completed, + completion_rate: reputation.completion_rate, + updated_at: reputation.last_update, + } + .publish(env); } - .publish(env); -} -pub fn rate_contribution(env: &Env, user: Address, rating: u32) { - // Rating should be 0-5 scaled (e.g. 0-100 or 0-500) - // Here assuming 0-5 - assert!(rating <= 5, "Rating must be between 0 and 5"); - - let mut reputation = get_reputation(env, &user); - - let current_total_quality = reputation.contribution_quality * reputation.total_contributions; - reputation.total_contributions += 1; - - // Weighted Average - reputation.contribution_quality = - (current_total_quality + rating) / reputation.total_contributions; - reputation.last_update = env.ledger().timestamp(); - - set_reputation(env, &user, &reputation); + /// Record a contribution rating (0–5) for a user. + pub fn rate_contribution(env: &Env, user: Address, rating: u32) { + assert!(rating <= 5, "Rating must be between 0 and 5"); + + let mut reputation = Self::get_reputation(env, &user); + let current_total_quality = reputation.contribution_quality * reputation.total_contributions; + reputation.total_contributions += 1; + reputation.contribution_quality = + (current_total_quality + rating) / reputation.total_contributions; + reputation.last_update = env.ledger().timestamp(); + Self::set_reputation(env, &user, &reputation); + + ContributionRatedEvent { + user: user.clone(), + rating, + new_contribution_quality: reputation.contribution_quality, + total_contributions: reputation.total_contributions, + rated_at: reputation.last_update, + } + .publish(env); + } - // Emit event - ContributionRatedEvent { - user: user.clone(), - rating, - new_contribution_quality: reputation.contribution_quality, - total_contributions: reputation.total_contributions, - rated_at: env.ledger().timestamp(), + // ===== Queries ===== + + /// Return the reputation record for a user, defaulting to zeroes. + #[must_use] + pub fn get_reputation(env: &Env, user: &Address) -> UserReputation { + env.storage() + .persistent() + .get(&(REPUTATION, user.clone())) + .unwrap_or(UserReputation { + participation_score: 0, + completion_rate: 0, + contribution_quality: 0, + total_courses_started: 0, + total_courses_completed: 0, + total_contributions: 0, + last_update: 0, + }) } - .publish(env); -} -pub fn get_reputation(env: &Env, user: &Address) -> UserReputation { - env.storage() - .persistent() - .get(&(REPUTATION, user.clone())) - .unwrap_or(UserReputation { - participation_score: 0, - completion_rate: 0, - contribution_quality: 0, - total_courses_started: 0, - total_courses_completed: 0, - total_contributions: 0, - last_update: 0, - }) -} + // ===== Internal ===== -fn set_reputation(env: &Env, user: &Address, reputation: &UserReputation) { - env.storage() - .persistent() - .set(&(REPUTATION, user.clone()), reputation); + fn set_reputation(env: &Env, user: &Address, reputation: &UserReputation) { + env.storage() + .persistent() + .set(&(REPUTATION, user.clone()), reputation); + } } diff --git a/contracts/teachlink/src/rewards.rs b/contracts/teachlink/src/rewards.rs index 61f2aa6d..91e30abd 100644 --- a/contracts/teachlink/src/rewards.rs +++ b/contracts/teachlink/src/rewards.rs @@ -1,3 +1,11 @@ +//! Reward pool management and distribution. +//! +//! Responsibilities: +//! - Initialize and fund the reward pool +//! - Issue rewards to users (admin-gated) +//! - Allow users to claim pending rewards +//! - Expose read-only views for pool and user reward state + use crate::errors::RewardsError; use crate::events::{RewardClaimedEvent, RewardIssuedEvent, RewardPoolFundedEvent}; use crate::reentrancy; @@ -16,6 +24,8 @@ const MAX_REWARD_AMOUNT: i128 = 170141183460469231731687303715884105727; pub struct Rewards; impl Rewards { + // ===== Initialization ===== + /// Initialize the rewards system pub fn initialize_rewards( env: &Env, @@ -40,9 +50,7 @@ impl Rewards { Ok(()) } - // ========================== - // Pool Management - // ========================== + // ===== Mutations ===== pub fn fund_reward_pool(env: &Env, funder: Address, amount: i128) -> Result<(), RewardsError> { #[cfg(not(test))] @@ -184,9 +192,7 @@ impl Rewards { Ok(()) } - // ========================== - // Claiming - // ========================== + // ===== Mutations (continued) ===== pub fn claim_rewards(env: &Env, user: Address) -> Result<(), RewardsError> { #[cfg(not(test))] @@ -263,9 +269,7 @@ impl Rewards { ) } - // ========================== - // Admin Functions - // ========================== + // ===== Admin ===== /// Set reward rate for a specific reward type pub fn set_reward_rate( @@ -312,10 +316,9 @@ impl Rewards { env.storage().instance().set(&REWARDS_ADMIN, &new_admin); } - // ========================== - // View Functions - // ========================== + // ===== Queries ===== + #[must_use] pub fn get_user_rewards(env: &Env, user: Address) -> Option { let user_rewards: Map = env .storage() @@ -325,10 +328,12 @@ impl Rewards { user_rewards.get(user) } + #[must_use] pub fn get_reward_pool_balance(env: &Env) -> i128 { env.storage().instance().get(&REWARD_POOL).unwrap_or(0) } + #[must_use] pub fn get_total_rewards_issued(env: &Env) -> i128 { env.storage() .instance() @@ -336,6 +341,7 @@ impl Rewards { .unwrap_or(0) } + #[must_use] pub fn get_reward_rate(env: &Env, reward_type: String) -> Option { let reward_rates: Map = env .storage() @@ -345,6 +351,7 @@ impl Rewards { reward_rates.get(reward_type) } + #[must_use] pub fn get_rewards_admin(env: &Env) -> Address { // SAFETY: REWARDS_ADMIN is always set during initialize_rewards env.storage().instance().get(&REWARDS_ADMIN).unwrap() diff --git a/contracts/teachlink/src/score.rs b/contracts/teachlink/src/score.rs index a22ea291..49746d55 100644 --- a/contracts/teachlink/src/score.rs +++ b/contracts/teachlink/src/score.rs @@ -1,3 +1,11 @@ +//! Credit score calculation from on-chain activities. +//! +//! Responsibilities: +//! - Award points for course completions and contributions +//! - Maintain per-user score, course list, and contribution history +//! - Emit events on every state change +//! - Expose read-only views for scores and history + use crate::events::{ContributionRecordedEvent, CourseCompletedEvent, CreditScoreUpdatedEvent}; use crate::storage::{CONTRIBUTIONS, COURSE_COMPLETIONS, CREDIT_SCORE}; use crate::types::{Contribution, ContributionType}; @@ -6,6 +14,8 @@ use soroban_sdk::{Address, Bytes, Env, Vec}; pub struct ScoreManager; impl ScoreManager { + // ===== Mutations ===== + /// Update the user's score by adding points pub fn update_score(env: &Env, user: Address, points: u64) { // Use a tuple key (CREDIT_SCORE, user) for mapping user to score @@ -82,7 +92,10 @@ impl ScoreManager { .publish(env); } + // ===== Queries ===== + /// Get the user's current credit score + #[must_use] pub fn get_score(env: &Env, user: Address) -> u64 { env.storage() .persistent() @@ -91,6 +104,7 @@ impl ScoreManager { } /// Get valid course completions + #[must_use] pub fn get_courses(env: &Env, user: Address) -> Vec { env.storage() .persistent() @@ -99,6 +113,7 @@ impl ScoreManager { } /// Get user contributions + #[must_use] pub fn get_contributions(env: &Env, user: Address) -> Vec { env.storage() .persistent() diff --git a/docs/MODULE_INTERFACE_STANDARDS.md b/docs/MODULE_INTERFACE_STANDARDS.md new file mode 100644 index 00000000..1d200c03 --- /dev/null +++ b/docs/MODULE_INTERFACE_STANDARDS.md @@ -0,0 +1,141 @@ +# Module Interface Standards + +Every Rust module in `contracts/teachlink/src/` must follow this standard. + +--- + +## 1. Module-level doc comment + +Every file must open with a `//!` doc comment that states: +- What the module does (one sentence) +- Key responsibilities (bullet list) + +```rust +//! Reward pool management and distribution. +//! +//! Responsibilities: +//! - Initialize and fund the reward pool +//! - Issue rewards to users +//! - Allow users to claim pending rewards +//! - Expose read-only views for pool state +``` + +--- + +## 2. Manager-struct pattern + +All public logic must live on a zero-size manager struct, not as free +functions. This makes call sites unambiguous and enables future trait +extraction. + +```rust +// ✅ correct +pub struct RewardsManager; +impl RewardsManager { + pub fn initialize(env: &Env, ...) -> Result<(), RewardsError> { ... } +} + +// ❌ incorrect — free functions +pub fn initialize_rewards(env: &Env, ...) -> Result<(), RewardsError> { ... } +``` + +--- + +## 3. Section comments + +Group methods with `// ===== Section Name =====` comments. Required +sections (use only those that apply): + +``` +// ===== Initialization ===== +// ===== Mutations ===== +// ===== Admin ===== +// ===== Queries ===== +``` + +--- + +## 4. `#[must_use]` on pure getters + +Any method that returns a value and has no side effects must carry +`#[must_use]`. + +```rust +#[must_use] +pub fn get_score(env: &Env, user: Address) -> u64 { ... } +``` + +--- + +## 5. Error handling + +- State-changing functions return `Result` where `E` is a typed + error from `errors.rs`. +- Pure getters may return `T` or `Option` directly. + +--- + +## 6. Authorization pattern + +Require auth at the top of every state-changing function that acts on +behalf of a user or admin. Use `#[cfg(not(test))]` guards only when +the test harness cannot provide auth. + +```rust +pub fn update_participation(env: &Env, user: Address, points: u32) { + user.require_auth(); + // ... +} +``` + +--- + +## 7. Compliant modules (reference implementations) + +| Module | Manager struct | Module doc | Section comments | `#[must_use]` getters | +|---|---|---|---|---| +| `audit.rs` | `AuditManager` | ✅ | ✅ | — | +| `performance.rs` | `PerformanceManager` | ✅ | ✅ | — | +| `sustainability.rs` | `SustainabilityManager` | ✅ | ✅ | — | +| `reputation.rs` | `ReputationManager` | ✅ | ✅ | ✅ | +| `score.rs` | `ScoreManager` | ✅ | ✅ | ✅ | +| `rewards.rs` | `Rewards` | ✅ | ✅ | ✅ | + +--- + +## 8. Minimal example + +```rust +//! Example module following the interface standard. +//! +//! Responsibilities: +//! - Track a counter per user +//! - Expose a read-only view + +use soroban_sdk::{Address, Env, Symbol, symbol_short}; + +const COUNTER: Symbol = symbol_short!("counter"); + +pub struct ExampleManager; + +impl ExampleManager { + // ===== Mutations ===== + + pub fn increment(env: &Env, user: Address) { + user.require_auth(); + let key = (COUNTER, user.clone()); + let n: u32 = env.storage().persistent().get(&key).unwrap_or(0); + env.storage().persistent().set(&key, &(n + 1)); + } + + // ===== Queries ===== + + #[must_use] + pub fn get(env: &Env, user: Address) -> u32 { + env.storage() + .persistent() + .get(&(COUNTER, user)) + .unwrap_or(0) + } +} +```