Problem Statement
course-registry::enroll (lines 158–177) writes DataKey::Progress(learner, id) = 0u32 but provides no inverse. A learner who enrolls by mistake is permanently enrolled — enroll panics with "Learner already enrolled" on retry — and the progress record persists in storage forever, paying rent indefinitely.
Why It Matters
- Storage rent on Soroban is real. Accumulating abandoned enrollments inflates state.
- Front-end UX: "I clicked enroll by accident" is unsolvable today.
- The codebase has no concept of learner-consented cancellation.
Technical Context
DataKey::Progress(Address, u32) is per (learner, course).
is_course_finished returns true for progress >= total_modules so unenrollment has to handle "in-progress vs complete."
Expected Outcome
- New
unenroll(env, learner, id) function:
- Learner auth required.
- Removes
Progress(learner, id) from storage.
- Emits
Unenrolled { learner, course_id } event.
- Panics
"Already completed" if progress >= course.total_modules (completion is irreversible).
- New
is_enrolled(env, learner, id) -> bool view helper.
- Tests: success, double-unenroll, completed-course scenario, unauthorized.
Acceptance Criteria
- All unit + integration tests pass.
- Storage slot for unenrolled learners is removed (asserted via
env.storage().persistent().has after call).
Implementation Notes
- Tie unenroll to badge status: if BadgeNFT has a badge for (learner, course), unenroll could be blocked since the credential exists.
- Consider a grace period to prevent griefing.
Files / Modules Affected
contracts/course-registry/src/lib.rs
contracts/course-registry/src/test.rs
contracts/course-registry/README.md
Dependencies
None.
Difficulty
Easy.
Estimated Effort
3–4 hours.
Suggested Labels
feature, ux, P2, course-registry, good-first-issue
Problem Statement
course-registry::enroll(lines 158–177) writesDataKey::Progress(learner, id) = 0u32but provides no inverse. A learner who enrolls by mistake is permanently enrolled —enrollpanics with"Learner already enrolled"on retry — and the progress record persists in storage forever, paying rent indefinitely.Why It Matters
Technical Context
DataKey::Progress(Address, u32)is per (learner, course).is_course_finishedreturnstrueforprogress >= total_modulesso unenrollment has to handle "in-progress vs complete."Expected Outcome
unenroll(env, learner, id)function:Progress(learner, id)from storage.Unenrolled { learner, course_id }event."Already completed"ifprogress >= course.total_modules(completion is irreversible).is_enrolled(env, learner, id) -> boolview helper.Acceptance Criteria
env.storage().persistent().hasafter call).Implementation Notes
Files / Modules Affected
contracts/course-registry/src/lib.rscontracts/course-registry/src/test.rscontracts/course-registry/README.mdDependencies
None.
Difficulty
Easy.
Estimated Effort
3–4 hours.
Suggested Labels
feature,ux,P2,course-registry,good-first-issue