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
532 changes: 400 additions & 132 deletions contracts/credential-nft/src/lib.rs

Large diffs are not rendered by default.

192 changes: 142 additions & 50 deletions contracts/learn-token/src/lib.rs

Large diffs are not rendered by default.

82 changes: 44 additions & 38 deletions contracts/progress-tracker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,13 @@ impl ProgressTracker {
Self::add_course_to_tag_index(&env, &tag, &course_id);
}

events::course_created(&env, &course_id, total_modules, total_quizzes, module_ids.clone());
events::course_created(
&env,
&course_id,
total_modules,
total_quizzes,
module_ids.clone(),
);
}

/// Enroll a learner in a course.
Expand Down Expand Up @@ -542,7 +548,13 @@ impl ProgressTracker {
progress,
);

events::module_completed(env, learner, course_id, &module_id, progress.overall_progress);
events::module_completed(
env,
learner,
course_id,
&module_id,
progress.overall_progress,
);

// Notify indexers the moment eligibility flips to true, instead of
// requiring them to poll get_progress (#96).
Expand Down Expand Up @@ -939,14 +951,7 @@ impl ProgressTracker {
&progress,
);

events::quiz_retaken(
env,
learner,
course_id,
&quiz_id,
previous_score,
new_score,
);
events::quiz_retaken(env, learner, course_id, &quiz_id, previous_score, new_score);

// Notify indexers the moment eligibility flips to true, instead of
// requiring them to poll get_progress (#96).
Expand Down Expand Up @@ -2079,10 +2084,7 @@ impl ProgressTracker {
pub fn revoke_delegation(env: Env, learner: Address) {
learner.require_auth();

types::remove_entry(
&env,
&ProgressTrackerDataKey::DelegatedTo(learner.clone()),
);
types::remove_entry(&env, &ProgressTrackerDataKey::DelegatedTo(learner.clone()));

events::delegation_revoked(&env, &learner);
}
Expand Down Expand Up @@ -4632,8 +4634,7 @@ mod tests {
// The test name says "succeeds after unpause" but the sequence above never
// paused the contract. Pause first, then unpause before the delegate
// retake call to actually exercise the post-unpause path.
client.emergency_pause();
client.unpause();
client.pause(&admin);
client.unpause(&admin);
// Delegate path still works once the pause lifts, mirroring the
// direct path above.
Expand Down Expand Up @@ -5098,17 +5099,26 @@ mod tests {

// Update difficulty
client.set_course_difficulty(&course_id, &1);
assert_eq!(client.get_course_created_at(&course_id), original_created_at);
assert_eq!(
client.get_course_created_at(&course_id),
original_created_at
);

// Update tags
let mut tags = Vec::new(&env);
tags.push_back(Symbol::new(&env, "rust"));
client.set_course_tags(&course_id, &tags);
assert_eq!(client.get_course_created_at(&course_id), original_created_at);
assert_eq!(
client.get_course_created_at(&course_id),
original_created_at
);

// Update content hash
client.set_course_content_hash(&course_id, &Symbol::new(&env, "hash1"));
assert_eq!(client.get_course_created_at(&course_id), original_created_at);
assert_eq!(
client.get_course_created_at(&course_id),
original_created_at
);
}

#[test]
Expand Down Expand Up @@ -5318,22 +5328,20 @@ mod tests {

client.pause(&pauser);
env.as_contract(&contract_id, || {
assert!(
env.storage()
.persistent()
.get::<_, bool>(&ProgressTrackerDataKey::Paused)
.unwrap_or(false)
);
assert!(env
.storage()
.persistent()
.get::<_, bool>(&ProgressTrackerDataKey::Paused)
.unwrap_or(false));
});

client.unpause(&pauser);
env.as_contract(&contract_id, || {
assert!(
!env.storage()
.persistent()
.get::<_, bool>(&ProgressTrackerDataKey::Paused)
.unwrap_or(false)
);
assert!(!env
.storage()
.persistent()
.get::<_, bool>(&ProgressTrackerDataKey::Paused)
.unwrap_or(false));
});
}

Expand All @@ -5346,12 +5354,11 @@ mod tests {

client.pause(&admin);
env.as_contract(&contract_id, || {
assert!(
env.storage()
.persistent()
.get::<_, bool>(&ProgressTrackerDataKey::Paused)
.unwrap_or(false)
);
assert!(env
.storage()
.persistent()
.get::<_, bool>(&ProgressTrackerDataKey::Paused)
.unwrap_or(false));
});
}

Expand Down Expand Up @@ -5414,4 +5421,3 @@ mod tests {
assert!(client.has_role(&pauser, &AdminRole::Pauser));
}
}

16 changes: 9 additions & 7 deletions tests/integration/credential_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ fn test_end_to_end_credential_flow() {

// Mint credential (average score = (85+75)/2 = 80)
let metadata_uri = Symbol::new(env, "ipfs_QmCredential123");
let cred_id = credential_client.mint_credential(learner, &course_id, &80, &metadata_uri);
let cred_id =
credential_client.mint_credential(&setup.admin, learner, &course_id, &80, &metadata_uri);

// Verify
let info = credential_client.verify_credential(&cred_id);
Expand All @@ -50,7 +51,7 @@ fn test_public_verification() {
fixtures::complete_full_course(env, learner, &course_id, &progress_client);
let uri = Symbol::new(env, "ipfs_Qm123");

let cred_id = credential_client.mint_credential(learner, &course_id, &80, &uri);
let cred_id = credential_client.mint_credential(&setup.admin, learner, &course_id, &80, &uri);

// Anyone can verify (no auth required for reads)
let info = credential_client.verify_credential(&cred_id);
Expand Down Expand Up @@ -83,8 +84,8 @@ fn test_multiple_course_credentials() {
progress_client.complete_module(learner, &course_id_2, &Symbol::new(env, "mod_sol_1"));
progress_client.submit_quiz_score(learner, &course_id_2, &Symbol::new(env, "quiz_sol_1"), &90);

let cred1 = credential_client.mint_credential(learner, &course_id_1, &80, &uri);
let cred2 = credential_client.mint_credential(learner, &course_id_2, &90, &uri);
let cred1 = credential_client.mint_credential(&setup.admin, learner, &course_id_1, &80, &uri);
let cred2 = credential_client.mint_credential(&setup.admin, learner, &course_id_2, &90, &uri);

let creds = credential_client.get_credentials_for(learner, &0, &10);
assert_eq!(creds.len(), 2);
Expand All @@ -107,7 +108,7 @@ fn test_revoked_credential_shows_revoked_status() {
fixtures::complete_full_course(env, learner, &course_id, &progress_client);
let uri = Symbol::new(env, "ipfs_meta");

let cred_id = credential_client.mint_credential(learner, &course_id, &80, &uri);
let cred_id = credential_client.mint_credential(&setup.admin, learner, &course_id, &80, &uri);
assert!(credential_client.is_credential_valid(&cred_id));

credential_client.revoke_credential(&cred_id);
Expand All @@ -132,7 +133,8 @@ fn test_credential_metadata_uri() {
fixtures::complete_full_course(env, learner, &course_id, &progress_client);
let expected_uri = Symbol::new(env, "ipfs_QmTestMetadata");

let cred_id = credential_client.mint_credential(learner, &course_id, &80, &expected_uri);
let cred_id =
credential_client.mint_credential(&setup.admin, learner, &course_id, &80, &expected_uri);

let info = credential_client.verify_credential(&cred_id);
assert_eq!(info.metadata_uri, expected_uri);
Expand All @@ -159,7 +161,7 @@ fn test_credential_has_issuance_timestamp() {
fixtures::complete_full_course(env, learner, &course_id, &progress_client);
let uri = Symbol::new(env, "ipfs_meta");

let cred_id = credential_client.mint_credential(learner, &course_id, &80, &uri);
let cred_id = credential_client.mint_credential(&setup.admin, learner, &course_id, &80, &uri);

let info = credential_client.verify_credential(&cred_id);
assert_eq!(info.issued_at, 1700000000);
Expand Down
48 changes: 34 additions & 14 deletions tests/integration/security_double_spending_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@
//! ChainLearn contracts. Double-spending is a critical vulnerability — these
//! tests prove it is impossible through every contract surface.

use credential_nft::{ContractError as CredentialContractError, CredentialNft, CredentialNftClient};
use credential_nft::{
ContractError as CredentialContractError, CredentialNft, CredentialNftClient,
};
use learn_token::LearnTokenClient;
use progress_tracker::{ProgressTracker, ProgressTrackerClient};
use soroban_sdk::{
testutils::Address as _, Address, Env, String as SorobanString, Symbol, Vec,
};
use soroban_sdk::{testutils::Address as _, Address, Env, String as SorobanString, Symbol, Vec};

fn setup_env(env: &Env) -> (Address, LearnTokenClient<'static>, CredentialNftClient<'static>, ProgressTrackerClient<'static>) {
fn setup_env(
env: &Env,
) -> (
Address,
LearnTokenClient<'static>,
CredentialNftClient<'static>,
ProgressTrackerClient<'static>,
) {
let admin = Address::generate(env);

// Register and initialize ProgressTracker
Expand Down Expand Up @@ -158,7 +165,6 @@ fn test_double_claim_in_batch_is_skipped() {
// ── Double-mint credential ──────────────────────────────────────────────────

#[test]
#[should_panic(expected = "credential already exists for this learner and course")]
fn test_double_mint_credential_is_prevented() {
let env = Env::default();
let (admin, _token_client, credential_client, progress_client) = setup_env(&env);
Expand All @@ -173,7 +179,12 @@ fn test_double_mint_credential_is_prevented() {
credential_client.mint_credential(&admin, &learner, &course_id, &85, &metadata_uri);

// Second mint for the same learner+course must be rejected
credential_client.mint_credential(&admin, &learner, &course_id, &85, &metadata_uri);
let err = credential_client
.try_mint_credential(&admin, &learner, &course_id, &85, &metadata_uri)
.err()
.expect("should fail")
.expect("typed error");
assert_eq!(err, CredentialContractError::DuplicateCredential);
}

#[test]
Expand All @@ -187,7 +198,8 @@ fn test_double_mint_credential_does_not_corrupt_state() {
let metadata_uri = Symbol::new(&env, "ipfs_Qm123");
create_course_and_complete(&env, &progress_client, &learner, &course_id, 85);

let cred_id = credential_client.mint_credential(&admin, &learner, &course_id, &85, &metadata_uri);
let cred_id =
credential_client.mint_credential(&admin, &learner, &course_id, &85, &metadata_uri);
let total_before = credential_client.get_total_credentials_count();

// Second mint reverts — state must be unchanged
Expand All @@ -196,7 +208,10 @@ fn test_double_mint_credential_does_not_corrupt_state() {
}));
assert!(result.is_err(), "double mint should revert");

assert_eq!(credential_client.get_total_credentials_count(), total_before);
assert_eq!(
credential_client.get_total_credentials_count(),
total_before
);
let info = credential_client.verify_credential(&cred_id);
assert_eq!(info.learner, learner);
assert_eq!(info.score, 85);
Expand Down Expand Up @@ -281,7 +296,8 @@ fn test_double_enroll_does_not_corrupt_state() {

let progress_after = progress_client.get_progress(&learner, &course_id);
assert_eq!(
progress_before.overall_progress, progress_after.overall_progress
progress_before.overall_progress,
progress_after.overall_progress
);
assert_eq!(
progress_before.modules_completed_bitmap,
Expand Down Expand Up @@ -376,10 +392,14 @@ fn test_double_revoke_credential_is_prevented() {
let course_id = Symbol::new(&env, "course_1");
let metadata_uri = Symbol::new(&env, "ipfs_Qm123");
create_course_and_complete(&env, &progress_client, &learner, &course_id, 85);
let cred_id = credential_client.mint_credential(&admin, &learner, &course_id, &85, &metadata_uri);
let cred_id =
credential_client.mint_credential(&admin, &learner, &course_id, &85, &metadata_uri);
credential_client.revoke_credential(&cred_id);
let err = credential_client.try_revoke_credential(&cred_id)
.err().expect("should fail").expect("typed error");
let err = credential_client
.try_revoke_credential(&cred_id)
.err()
.expect("should fail")
.expect("typed error");
assert_eq!(err, CredentialContractError::AlreadyInitialized);
}

Expand All @@ -388,7 +408,7 @@ fn test_double_revoke_credential_is_prevented() {
#[test]
fn test_claim_reward_then_mint_credential_are_independent() {
let env = Env::default();
let (_admin, token_client, credential_client, progress_client) = setup_env(&env);
let (admin, token_client, credential_client, progress_client) = setup_env(&env);
let learner = Address::generate(&env);
env.mock_all_auths();

Expand Down
Loading
Loading