diff --git a/contracts/learn-token/src/lib.rs b/contracts/learn-token/src/lib.rs index 29e4443..bcf4f23 100644 --- a/contracts/learn-token/src/lib.rs +++ b/contracts/learn-token/src/lib.rs @@ -5,7 +5,7 @@ mod storage; use chainlearn_shared::{BASE_REWARD_PER_POINT, MAX_QUIZ_SCORE}; use soroban_sdk::{ - contract, contracterror, contractimpl, Address, Env, String as SorobanString, Symbol, + contract, contracterror, contractimpl, Address, Env, IntoVal, String as SorobanString, Symbol, }; use soroban_token_sdk::metadata::TokenMetadata; @@ -377,9 +377,14 @@ impl LearnToken { } // Verify score by querying the progress-tracker contract. - // We construct the client once per call to avoid redundant loading gas overhead. - let client = get_progress_client(&env); - let score = client.get_quiz_score(&learner, &course_id, &quiz_id); + // Use env.invoke_contract directly to avoid the gas cost of + // ProgressTrackerClient::new() on every invocation (#133). + let progress_tracker = storage::get_progress_tracker(&env); + let score: u32 = env.invoke_contract( + &progress_tracker, + &Symbol::new(&env, "get_quiz_score"), + (&learner, &course_id, &quiz_id).into_val(&env), + ); if score == 0 { panic!("score must be greater than 0"); diff --git a/contracts/progress-tracker/src/lib.rs b/contracts/progress-tracker/src/lib.rs index ff75789..75c5eb6 100644 --- a/contracts/progress-tracker/src/lib.rs +++ b/contracts/progress-tracker/src/lib.rs @@ -264,10 +264,9 @@ impl ProgressTracker { let was_eligible = progress.eligible_for_credential; - progress.overall_progress = - rewards::calculate_progress(&env, &learner, &course_id, &course, &progress); + progress.overall_progress = rewards::calculate_progress(&course, &progress); progress.eligible_for_credential = - rewards::is_eligible_for_credential(&env, &learner, &course_id, &course, &progress); + rewards::is_eligible_for_credential(&course, &progress); env.storage().persistent().set( &ProgressTrackerDataKey::Progress(learner.clone(), course_id.clone()), @@ -369,10 +368,9 @@ impl ProgressTracker { // Recalculate from the updated in-memory aggregates, so everything is // known before the single storage write below. - progress.overall_progress = - rewards::calculate_progress(&env, &learner, &course_id, &course, &progress); + progress.overall_progress = rewards::calculate_progress(&course, &progress); progress.eligible_for_credential = - rewards::is_eligible_for_credential(&env, &learner, &course_id, &course, &progress); + rewards::is_eligible_for_credential(&course, &progress); // Single write with all updated fields env.storage().persistent().set( diff --git a/contracts/progress-tracker/src/rewards.rs b/contracts/progress-tracker/src/rewards.rs index be18d91..deabf3a 100644 --- a/contracts/progress-tracker/src/rewards.rs +++ b/contracts/progress-tracker/src/rewards.rs @@ -1,27 +1,13 @@ use chainlearn_shared::MIN_CREDENTIAL_SCORE; -use soroban_sdk::{Address, Env, Symbol, Vec}; -use crate::types::{Course, ProgressInfo, ProgressTrackerDataKey}; +use crate::types::{Course, ProgressInfo}; /// Count how many modules a learner has completed in a course. -pub fn count_completed_modules( - env: &Env, - learner: &Address, - course_id: &Symbol, - modules: &Vec, -) -> u32 { - let mut count = 0u32; - for module_id in modules.iter() { - let key = ProgressTrackerDataKey::ModuleCompleted( - learner.clone(), - course_id.clone(), - module_id.clone(), - ); - if env.storage().persistent().has(&key) { - count += 1; - } - } - count +/// +/// Uses the `modules_completed_bitmap` on [`ProgressInfo`] so the call is O(1) +/// instead of iterating every module's storage key. +pub fn count_completed_modules(progress: &ProgressInfo) -> u32 { + progress.modules_completed_bitmap.count_ones() } /// Calculate the overall progress percentage for a learner in a course. @@ -29,15 +15,9 @@ pub fn count_completed_modules( /// Progress is weighted: /// - 70% from module completion (proportion of modules completed) /// - 30% from quiz performance (average quiz score / 100) -pub fn calculate_progress( - env: &Env, - learner: &Address, - course_id: &Symbol, - course: &Course, - progress: &ProgressInfo, -) -> u32 { +pub fn calculate_progress(course: &Course, progress: &ProgressInfo) -> u32 { let module_progress = if course.total_modules > 0 { - let completed = count_completed_modules(env, learner, course_id, &course.module_ids); + let completed = count_completed_modules(progress); (completed * 70) / course.total_modules } else { 0 @@ -58,6 +38,9 @@ pub fn calculate_progress( } /// Calculate the average quiz score for a learner in a course from `ProgressInfo`. +/// +/// Uses the running sum (`total_quiz_score`) and count (`quizzes_submitted`) +/// maintained in [`ProgressInfo`], so this is O(1) — no Vec iteration. pub fn average_quiz_score(progress: &ProgressInfo) -> u32 { if progress.quizzes_submitted == 0 { return 0; @@ -68,14 +51,11 @@ pub fn average_quiz_score(progress: &ProgressInfo) -> u32 { /// Determine if a learner is eligible for a credential. pub fn is_eligible_for_credential( - env: &Env, - learner: &Address, - course_id: &Symbol, course: &Course, progress: &ProgressInfo, ) -> bool { // Check all modules completed - let completed = count_completed_modules(env, learner, course_id, &course.module_ids); + let completed = count_completed_modules(progress); if completed < course.total_modules { return false; }