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
13 changes: 9 additions & 4 deletions contracts/learn-token/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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");
Expand Down
10 changes: 4 additions & 6 deletions contracts/progress-tracker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down Expand Up @@ -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(
Expand Down
44 changes: 12 additions & 32 deletions contracts/progress-tracker/src/rewards.rs
Original file line number Diff line number Diff line change
@@ -1,43 +1,23 @@
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<Symbol>,
) -> 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.
///
/// 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
Expand All @@ -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;
Expand All @@ -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;
}
Expand Down