From 04851d72770d6977e0b7ac760e7a3ca75908aa17 Mon Sep 17 00:00:00 2001 From: DooseWayo Date: Mon, 31 Aug 2026 00:50:06 +0100 Subject: [PATCH] well implemented --- contracts/progress-tracker/src/lib.rs | 110 +++++++++++++++++++++++- contracts/progress-tracker/src/types.rs | 32 +++++++ 2 files changed, 140 insertions(+), 2 deletions(-) diff --git a/contracts/progress-tracker/src/lib.rs b/contracts/progress-tracker/src/lib.rs index 3cbf5b4..2023fcc 100644 --- a/contracts/progress-tracker/src/lib.rs +++ b/contracts/progress-tracker/src/lib.rs @@ -6,8 +6,8 @@ pub mod types; use chainlearn_shared::ContractMetadata; use soroban_sdk::{contract, contracterror, contractimpl, symbol_short, Address, Env, Symbol, Vec}; pub use types::{ - Course, LearnerStats, ProgressExport, ProgressInfo, ProgressTrackerDataKey, QuizResult, - VersionedContractMetadata, + Achievement, AchievementType, Course, LearnerStats, ProgressExport, ProgressInfo, + ProgressTrackerDataKey, QuizResult, VersionedContractMetadata, }; /// Sentinel meaning "no content hash set"; enrollment skips verification (#235). @@ -510,6 +510,25 @@ impl ProgressTracker { (Symbol::new(env, "credential_eligible"),), (learner, course_id), ); + + // Award FirstCourse achievement when learner becomes eligible for credential + Self::earn_achievement( + env, + learner, + AchievementType::FirstCourse, + Some(course_id.clone()), + ); + + // Check for CourseMaster achievement (5 courses completed) + let stats = Self::get_learner_stats_internal(env, learner); + if stats.courses_completed >= 5 { + Self::earn_achievement( + env, + learner, + AchievementType::CourseMaster, + None, + ); + } } } @@ -1450,6 +1469,93 @@ impl ProgressTracker { } } + /// Earn an achievement for a learner. + /// + /// Internal function that checks if an achievement has already been earned + /// before awarding it. Achievements are stored per learner and can be + /// queried via `get_achievements`. + /// + /// # Arguments + /// * `env` - The Soroban environment + /// * `learner` - The learner address + /// * `achievement_type` - Type of achievement to award + /// * `course_id` - Optional course ID associated with the achievement + fn earn_achievement( + env: &Env, + learner: &Address, + achievement_type: AchievementType, + course_id: Option, + ) { + // Check if achievement already earned + let achievement_key = ProgressTrackerDataKey::AchievementEarned( + learner.clone(), + achievement_type.clone(), + ); + if env.storage().persistent().has(&achievement_key) { + return; // Already earned, skip + } + + let timestamp = env.ledger().timestamp(); + let achievement = Achievement { + achievement_type: achievement_type.clone(), + earned_at: timestamp, + course_id: course_id.clone(), + }; + + // Store that this achievement type has been earned + env.storage() + .persistent() + .set(&achievement_key, &true); + + // Add to learner's achievements list + let achievements_key = ProgressTrackerDataKey::Achievements(learner.clone()); + let mut achievements: Vec = env + .storage() + .persistent() + .get(&achievements_key) + .unwrap_or_else(|| Vec::new(env)); + achievements.push_back(achievement); + env.storage() + .persistent() + .set(&achievements_key, &achievements); + + // Emit achievement earned event + env.events().publish( + (Symbol::new(env, "achievement_earned"),), + (learner, &achievement_type, course_id, timestamp), + ); + } + + /// Get all achievements earned by a learner. + /// + /// Returns achievements in chronological order (oldest first). + /// + /// # Arguments + /// * `learner` - The learner address + /// + /// # Returns + /// List of achievements earned by the learner. + pub fn get_achievements(env: Env, learner: Address) -> Vec { + env.storage() + .persistent() + .get(&ProgressTrackerDataKey::Achievements(learner)) + .unwrap_or_else(|| Vec::new(&env)) + } + + /// Check whether a learner has earned a specific achievement type. + /// + /// # Arguments + /// * `learner` - The learner address + /// * `achievement_type` - Type of achievement to check + /// + /// # Returns + /// `true` if the learner has earned the achievement. + pub fn has_achievement(env: Env, learner: Address, achievement_type: AchievementType) -> bool { + env.storage() + .persistent() + .has(&ProgressTrackerDataKey::AchievementEarned(learner, achievement_type)) + } + /// Check whether a course has been registered via `create_course` (#108). /// /// A cheap existence check -- unlike `get_course`, it never deserializes diff --git a/contracts/progress-tracker/src/types.rs b/contracts/progress-tracker/src/types.rs index 48431ce..6c15428 100644 --- a/contracts/progress-tracker/src/types.rs +++ b/contracts/progress-tracker/src/types.rs @@ -140,6 +140,34 @@ pub struct VersionedContractMetadata { pub version: u32, } +/// Represents an achievement earned by a learner. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Achievement { + /// Type of achievement earned. + pub achievement_type: AchievementType, + /// Timestamp when the achievement was earned. + pub earned_at: u64, + /// Optional course ID associated with the achievement (for course-specific achievements). + pub course_id: Option, +} + +/// Types of achievements that learners can earn. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum AchievementType { + /// First course completed (credential earned). + FirstCourse, + /// Perfect score (100%) on any quiz. + PerfectScore, + /// Completed a course in less than the average time (speed learner). + SpeedLearner, + /// Completed 5 courses. + CourseMaster, + /// Achieved 100% completion on a course (all modules, perfect quiz scores). + PerfectCourse, +} + /// Storage keys for the progress tracker contract. #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -161,4 +189,8 @@ pub enum ProgressTrackerDataKey { /// The address a learner has delegated progress-tracking to, if any /// (#222). Absent when the learner has no active delegation. DelegatedTo(Address), + /// Achievements earned by a learner. + Achievements(Address), + /// Achievement earned by a specific learner and achievement type (for deduplication). + AchievementEarned(Address, AchievementType), }