From 4d36a56748925d7d75747db5f430c73349782495 Mon Sep 17 00:00:00 2001 From: Caesarr Date: Sun, 31 May 2026 16:32:33 +0100 Subject: [PATCH 1/2] Changes made Project files updated Closed #53 --- contracts/course-registry/Cargo.toml | 1 + contracts/course-registry/src/lib.rs | 35 ++++++++++- contracts/course-registry/src/test.rs | 85 ++++++++++++++++++++++++++ contracts/course-registry/src/types.rs | 1 + 4 files changed, 121 insertions(+), 1 deletion(-) diff --git a/contracts/course-registry/Cargo.toml b/contracts/course-registry/Cargo.toml index 07f1602..ecb59e5 100644 --- a/contracts/course-registry/Cargo.toml +++ b/contracts/course-registry/Cargo.toml @@ -13,6 +13,7 @@ doctest = false [dependencies] soroban-sdk = { workspace = true } +reward-pool = { path = "../reward-pool" } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/contracts/course-registry/src/lib.rs b/contracts/course-registry/src/lib.rs index 8eea5ec..9498246 100644 --- a/contracts/course-registry/src/lib.rs +++ b/contracts/course-registry/src/lib.rs @@ -3,6 +3,7 @@ use soroban_sdk::{contract, contractevent, contractimpl, Address, BytesN, Env}; pub mod types; use types::{Course, DataKey}; +use reward_pool::RewardPoolClient; #[contract] pub struct CourseRegistry; @@ -188,6 +189,22 @@ impl CourseRegistry { CourseStatusChanged { id, active }.publish(&env); } + /// Sets the RewardPool contract address. Only callable by the Protocol Admin. + pub fn set_reward_pool(env: Env, admin: Address, reward_pool: Address) { + admin.require_auth(); + + let stored_admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .expect("Contract not initialized"); + assert!(admin == stored_admin, "Unauthorized: Caller is not the protocol admin"); + + env.storage() + .instance() + .set(&DataKey::RewardPool, &reward_pool); + } + /// Returns true if the learner has completed all modules in the course. pub fn is_course_finished(env: Env, learner: Address, id: u32) -> bool { let course: Course = env @@ -281,11 +298,27 @@ impl CourseRegistry { // 8. Emit ModuleCompleted event ModuleCompleted { - learner, + learner: learner.clone(), course_id: id, new_progress, } .publish(&env); + // 9. If learner finished the course, trigger reward distribution + if new_progress == course.total_modules { + // Fetch RewardPool address from instance storage + let reward_pool_addr: Address = env + .storage() + .instance() + .get(&DataKey::RewardPool) + .expect("RewardPool not set"); + + let reward_pool = RewardPoolClient::new(&env, &reward_pool_addr); + + // Base reward in token's smallest unit (e.g., 10 USDC) + let base_reward: i128 = 10_0000000; + + reward_pool.distribute_reward(&env.current_contract_address(), &learner, &base_reward); + } } } diff --git a/contracts/course-registry/src/test.rs b/contracts/course-registry/src/test.rs index 560773f..ec90239 100644 --- a/contracts/course-registry/src/test.rs +++ b/contracts/course-registry/src/test.rs @@ -6,6 +6,8 @@ use soroban_sdk::{ }; use crate::{CourseRegistry, CourseRegistryClient, DataKey}; +use reward_pool::RewardPoolClient; +use soroban_sdk::token; // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -563,3 +565,86 @@ fn test_get_progress_tracks_completion() { client.complete_module(&admin, &learner, &course_id); assert_eq!(client.get_progress(&learner, &course_id), 3); } + +#[test] +fn test_complete_module_triggers_reward_success() { + let env = Env::default(); + env.mock_all_auths(); + + // Register CourseRegistry + let cr_contract_id = env.register(CourseRegistry, ()); + let cr_client = CourseRegistryClient::new(&env, &cr_contract_id); + + // Register RewardPool + let rp_contract_id = env.register(reward_pool::RewardPool, ()); + let rp_client = RewardPoolClient::new(&env, &rp_contract_id); + + let admin = Address::generate(&env); + let instructor = Address::generate(&env); + let learner = Address::generate(&env); + + // Create a mock token and initialize reward pool + let token_id = env.register_stellar_asset_contract_v2(admin.clone()); + cr_client.initialize(&admin); + rp_client.initialize(&admin, &token_id.address()); + + // Set reward pool address on CourseRegistry and whitelist CourseRegistry in RewardPool + cr_client.set_reward_pool(&admin, &rp_client.address); + rp_client.add_approved_spender(&admin, &cr_client.address); + + // Create course and enroll learner + let course_id = cr_client.create_course(&admin, &instructor, &3, &dummy_hash(&env)); + cr_client.enroll(&learner, &course_id); + + // Fund reward pool with tokens + let token_client = token::StellarAssetClient::new(&env, &token_id.address()); + token_client.mint(&rp_client.address, &100000000i128); + + // Complete modules - final completion should trigger distribution + cr_client.complete_module(&admin, &learner, &course_id); + cr_client.complete_module(&admin, &learner, &course_id); + cr_client.complete_module(&admin, &learner, &course_id); + + // Verify learner received base reward (10_0000000) + assert_eq!(token_client.balance(&learner), 10_0000000i128); +} + +#[test] +#[should_panic(expected = "Caller is not an authorized spender")] +fn test_complete_module_fails_when_not_whitelisted() { + let env = Env::default(); + env.mock_all_auths(); + + // Register CourseRegistry + let cr_contract_id = env.register(CourseRegistry, ()); + let cr_client = CourseRegistryClient::new(&env, &cr_contract_id); + + // Register RewardPool + let rp_contract_id = env.register(reward_pool::RewardPool, ()); + let rp_client = RewardPoolClient::new(&env, &rp_contract_id); + + let admin = Address::generate(&env); + let instructor = Address::generate(&env); + let learner = Address::generate(&env); + + // Create a mock token and initialize reward pool + let token_id = env.register_stellar_asset_contract_v2(admin.clone()); + cr_client.initialize(&admin); + rp_client.initialize(&admin, &token_id.address()); + + // Set reward pool address on CourseRegistry but DO NOT whitelist CourseRegistry in RewardPool + cr_client.set_reward_pool(&admin, &rp_client.address); + + // Create course and enroll learner + let course_id = cr_client.create_course(&admin, &instructor, &3, &dummy_hash(&env)); + cr_client.enroll(&learner, &course_id); + + // Fund reward pool with tokens + let token_client = token::StellarAssetClient::new(&env, &token_id.address()); + token_client.mint(&rp_client.address, &100000000i128); + + // Complete modules - final completion should attempt distribution and panic + cr_client.complete_module(&admin, &learner, &course_id); + cr_client.complete_module(&admin, &learner, &course_id); + cr_client.complete_module(&admin, &learner, &course_id); +} diff --git a/contracts/course-registry/src/types.rs b/contracts/course-registry/src/types.rs index b283978..530269f 100644 --- a/contracts/course-registry/src/types.rs +++ b/contracts/course-registry/src/types.rs @@ -16,4 +16,5 @@ pub enum DataKey { Progress(Address, u32), CourseCount, Admin, + RewardPool, } From 99e79e3bcb624986e8434d0d8baa0d89e337de19 Mon Sep 17 00:00:00 2001 From: Caesarr Date: Fri, 5 Jun 2026 16:33:55 +0100 Subject: [PATCH 2/2] Fixed the indetation problem --- contracts/course-registry/src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/course-registry/src/types.rs b/contracts/course-registry/src/types.rs index 530269f..c60fe94 100644 --- a/contracts/course-registry/src/types.rs +++ b/contracts/course-registry/src/types.rs @@ -16,5 +16,5 @@ pub enum DataKey { Progress(Address, u32), CourseCount, Admin, - RewardPool, + RewardPool, }