Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 4 additions & 4 deletions contracts/teachlink/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1493,19 +1493,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 ==========
Expand Down
52 changes: 27 additions & 25 deletions contracts/teachlink/src/reputation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,6 @@ pub fn update_participation(env: &Env, user: Address, points: u32) {
new_participation_score: reputation.participation_score,
updated_at: env.ledger().timestamp(),
}
.publish(env);
}

/// Updates a user's course progress and recalculates their completion rate.
///
Expand Down Expand Up @@ -117,29 +115,25 @@ pub fn update_course_progress(env: &Env, user: Address, is_completion: bool) {
if reputation.total_courses_started < reputation.total_courses_completed {
reputation.total_courses_started = reputation.total_courses_completed;
}
} else {
reputation.total_courses_started += 1;
}

// Recalculate completion rate in basis points.
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);
}

/// Records a quality rating for a user's contribution and updates their
/// running average contribution quality score.
Expand Down Expand Up @@ -183,16 +177,24 @@ pub fn rate_contribution(env: &Env, user: Address, rating: u32) {

set_reputation(env, &user, &reputation);

// 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);
}

/// Retrieves a user's reputation record from persistent storage.
///
Expand Down
31 changes: 19 additions & 12 deletions contracts/teachlink/src/rewards.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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,
Expand All @@ -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))]
Expand Down Expand Up @@ -184,9 +192,7 @@ impl Rewards {
Ok(())
}

// ==========================
// Claiming
// ==========================
// ===== Mutations (continued) =====

pub fn claim_rewards(env: &Env, user: Address) -> Result<(), RewardsError> {
#[cfg(not(test))]
Expand Down Expand Up @@ -263,9 +269,7 @@ impl Rewards {
)
}

// ==========================
// Admin Functions
// ==========================
// ===== Admin =====

/// Set reward rate for a specific reward type
pub fn set_reward_rate(
Expand Down Expand Up @@ -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<UserReward> {
let user_rewards: Map<Address, UserReward> = env
.storage()
Expand All @@ -325,17 +328,20 @@ 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()
.get(&TOTAL_REWARDS_ISSUED)
.unwrap_or(0)
}

#[must_use]
pub fn get_reward_rate(env: &Env, reward_type: String) -> Option<RewardRate> {
let reward_rates: Map<String, RewardRate> = env
.storage()
Expand All @@ -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()
Expand Down
15 changes: 15 additions & 0 deletions contracts/teachlink/src/score.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -91,6 +104,7 @@ impl ScoreManager {
}

/// Get valid course completions
#[must_use]
pub fn get_courses(env: &Env, user: Address) -> Vec<u64> {
env.storage()
.persistent()
Expand All @@ -99,6 +113,7 @@ impl ScoreManager {
}

/// Get user contributions
#[must_use]
pub fn get_contributions(env: &Env, user: Address) -> Vec<Contribution> {
env.storage()
.persistent()
Expand Down
141 changes: 141 additions & 0 deletions docs/MODULE_INTERFACE_STANDARDS.md
Original file line number Diff line number Diff line change
@@ -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<T, E>` where `E` is a typed
error from `errors.rs`.
- Pure getters may return `T` or `Option<T>` 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)
}
}
```
Loading