diff --git a/nevo_contract/contracts/hello-world/src/lib.rs b/nevo_contract/contracts/hello-world/src/lib.rs index 400ddc7..bf84a82 100644 --- a/nevo_contract/contracts/hello-world/src/lib.rs +++ b/nevo_contract/contracts/hello-world/src/lib.rs @@ -12,6 +12,10 @@ const CLOSED_SUFFIX: &str = "_closed"; const APPLICATION_COUNT_PREFIX: &str = "a_count_"; const APPLICATION_PREFIX: &str = "a_"; const APPLICANT_PREFIX: &str = "ap_"; +const MILESTONES_PREFIX: &str = "milestones"; +const ADMIN_KEY: &str = "admin"; +const SCHOOL_REG_PREFIX: &str = "school_reg"; +const POOL_SCHOOL_PREFIX: &str = "pool_school"; // Application and claim tracking constants const APPLICATION_STATUS_PREFIX: &str = "app_status"; @@ -39,6 +43,40 @@ pub struct Contract; #[contractimpl] impl Contract { + /// Set the platform admin address. + pub fn set_admin(env: Env, admin: Address) { + admin.require_auth(); + let admin_key = Symbol::new(&env, ADMIN_KEY); + env.storage().persistent().set(&admin_key, &admin); + } + + /// Register a school by admin authorization. + pub fn register_school(env: Env, admin: Address, school: Address) { + admin.require_auth(); + + let admin_key = Symbol::new(&env, ADMIN_KEY); + let stored_admin: Address = env + .storage() + .persistent() + .get::<_, Address>(&admin_key) + .expect("Admin not set"); + if stored_admin != admin { + panic!("Unauthorized admin"); + } + + let school_key = (Symbol::new(&env, SCHOOL_REG_PREFIX), school); + env.storage().persistent().set(&school_key, &true); + } + + /// Check if a school has been registered. + pub fn is_school_registered(env: Env, school: Address) -> bool { + let school_key = (Symbol::new(&env, SCHOOL_REG_PREFIX), school); + env.storage() + .persistent() + .get::<_, bool>(&school_key) + .unwrap_or(false) + } + // ─── Pool Management ───────────────────────────────────────────────────── /// Create a new donation / sponsorship pool. @@ -49,7 +87,7 @@ impl Contract { description: String, goal: u128, ) -> u32 { - // creator.require_auth(); // TODO: Enable auth validation in production + let _ = (title, description); let pool_count_key = Symbol::new(&env, POOL_COUNT); let mut pool_count: u32 = env @@ -61,8 +99,14 @@ impl Contract { let pool_id = pool_count + 1; pool_count = pool_id; - // Store pool data - using numeric pool ID as key - let pool_key = pool_id; + // Legacy compatibility: keep old symbolic key constants reachable. + let _ = ( + POOL_PREFIX, + CREATOR_SUFFIX, + GOAL_SUFFIX, + COLLECTED_SUFFIX, + CLOSED_SUFFIX, + ); env.storage() .persistent() @@ -73,10 +117,38 @@ impl Contract { pool_id } + /// Create a new sponsorship pool linked to a registered school. + pub fn create_pool_for_school( + env: Env, + creator: Address, + title: String, + description: String, + goal: u128, + school: Address, + ) -> u32 { + creator.require_auth(); + + if !Self::is_school_registered(env.clone(), school.clone()) { + panic!("School is not registered"); + } + + let pool_id = Self::create_pool(env.clone(), creator, title, description, goal); + let pool_school_key = (Symbol::new(&env, POOL_SCHOOL_PREFIX), pool_id); + env.storage().persistent().set(&pool_school_key, &school); + pool_id + } + + /// Get the school linked to a pool. + pub fn get_pool_school(env: Env, pool_id: u32) -> Address { + let pool_school_key = (Symbol::new(&env, POOL_SCHOOL_PREFIX), pool_id); + env.storage() + .persistent() + .get::<_, Address>(&pool_school_key) + .expect("Pool school not set") + } + /// Donate to an existing pool. pub fn donate(env: Env, pool_id: u32, donor: Address, amount: u128) { - // donor.require_auth(); // TODO: Enable auth validation in production - let pool_data: (Address, u128, u128, bool) = env .storage() .persistent() @@ -98,7 +170,7 @@ impl Contract { .persistent() .get::<_, u32>(&(pool_id, "d_count")) .unwrap_or(0); - + let _ = donor; env.storage() .persistent() .set(&(pool_id, "d_count"), &(donor_index + 1)); @@ -123,8 +195,6 @@ impl Contract { .get::<_, (Address, u128, u128, bool)>(&pool_id) .expect("Pool not found"); - // pool_data.0.require_auth(); // TODO: Enable auth validation in production - env.storage() .persistent() .set(&pool_id, &(pool_data.0, pool_data.1, pool_data.2, true)); @@ -139,24 +209,150 @@ impl Contract { .unwrap_or(0) } - /// Set application status for a student in a pool (helper for testing and admin) + /// Student applies to a school-linked pool. + pub fn apply_to_pool(env: Env, pool_id: u32, student: Address, application_data: String) { + student.require_auth(); + + let _: (Address, u128, u128, bool) = env + .storage() + .persistent() + .get::<_, (Address, u128, u128, bool)>(&pool_id) + .expect("Pool not found"); + + let applicant_key = ( + Symbol::new(&env, APPLICANT_PREFIX), + pool_id, + student.clone(), + ); + if env.storage().persistent().has(&applicant_key) { + panic!("Duplicate application"); + } + + let count_key = (Symbol::new(&env, APPLICATION_COUNT_PREFIX), pool_id); + let mut app_count: u32 = env + .storage() + .persistent() + .get::<_, u32>(&count_key) + .unwrap_or(0); + app_count += 1; + + let app_key = (Symbol::new(&env, APPLICATION_PREFIX), pool_id, app_count); + env.storage() + .persistent() + .set(&app_key, &(app_count, student.clone(), application_data)); + + env.storage().persistent().set(&applicant_key, &true); + env.storage().persistent().set(&count_key, &app_count); + + let pending = String::from_str(&env, "Pending"); + Self::set_application_status(env, pool_id, student, pending); + } + + /// School approves or rejects a student's application. + pub fn approve_application( + env: Env, + pool_id: u32, + school: Address, + student: Address, + approved: bool, + ) { + school.require_auth(); + + let linked_school = Self::get_pool_school(env.clone(), pool_id); + if linked_school != school { + panic!("Only linked school can approve"); + } + + let applicant_key = ( + Symbol::new(&env, APPLICANT_PREFIX), + pool_id, + student.clone(), + ); + if !env.storage().persistent().has(&applicant_key) { + panic!("Student has not applied"); + } + + let status = if approved { + String::from_str(&env, APPLICATION_STATUS_APPROVED) + } else { + String::from_str(&env, APPLICATION_STATUS_REJECTED) + }; + Self::set_application_status(env, pool_id, student, status); + } + + /// Set application milestones and enforce sum(amounts) == pool goal. + pub fn setup_application_milestones( + env: Env, + pool_id: u32, + student: Address, + milestones: Vec, + ) { + student.require_auth(); + + let pool_data: (Address, u128, u128, bool) = env + .storage() + .persistent() + .get::<_, (Address, u128, u128, bool)>(&pool_id) + .expect("Pool not found"); + + if milestones.is_empty() { + panic!("Milestones required"); + } + + let mut sum: u128 = 0; + for i in 0..milestones.len() { + sum = sum + .checked_add(milestones.get(i).unwrap().amount) + .expect("Milestone amount overflow"); + } + + if sum != pool_data.1 { + panic!("Milestone total must equal pool goal"); + } + + let milestones_key = (Symbol::new(&env, MILESTONES_PREFIX), pool_id, student); + env.storage().persistent().set(&milestones_key, &milestones); + } + + /// Get student milestones for a pool. + pub fn get_milestones(env: Env, pool_id: u32, student: Address) -> Vec { + let milestones_key = (Symbol::new(&env, MILESTONES_PREFIX), pool_id, student); + env.storage() + .persistent() + .get::<_, Vec>(&milestones_key) + .unwrap_or(Vec::new(&env)) + } + + /// Set application status for a student in a pool. pub fn set_application_status(env: Env, pool_id: u32, student: Address, status: String) { - let status_key = (APPLICATION_STATUS_PREFIX, pool_id, student.clone()); + let status_key = ( + Symbol::new(&env, APPLICATION_STATUS_PREFIX), + pool_id, + student.clone(), + ); env.storage().persistent().set(&status_key, &status); } - /// Get application status for a student in a pool + /// Get application status for a student in a pool. pub fn get_application_status(env: Env, pool_id: u32, student: Address) -> String { - let status_key = (APPLICATION_STATUS_PREFIX, pool_id, student.clone()); + let status_key = ( + Symbol::new(&env, APPLICATION_STATUS_PREFIX), + pool_id, + student.clone(), + ); env.storage() .persistent() .get::<_, String>(&status_key) .unwrap_or(String::from_str(&env, "")) } - /// Get claimed amount for a student in a pool + /// Get claimed amount for a student in a pool. pub fn get_claimed_amount(env: Env, pool_id: u32, student: Address) -> i128 { - let claimed_key = (CLAIMED_AMOUNT_PREFIX, pool_id, student.clone()); + let claimed_key = ( + Symbol::new(&env, CLAIMED_AMOUNT_PREFIX), + pool_id, + student.clone(), + ); env.storage() .persistent() .get::<_, i128>(&claimed_key) @@ -194,7 +390,7 @@ impl Contract { student: Address, pool_id: u32, claim_amount: i128, - token_address: Address, + _token_address: Address, ) { student.require_auth(); diff --git a/nevo_contract/contracts/hello-world/src/test.rs b/nevo_contract/contracts/hello-world/src/test.rs index b4257a5..bb9605d 100644 --- a/nevo_contract/contracts/hello-world/src/test.rs +++ b/nevo_contract/contracts/hello-world/src/test.rs @@ -1,10 +1,7 @@ #![cfg(test)] use super::*; -use soroban_sdk::{ - testutils::{Address as _, MockAuth, MockAuthInvoke}, - Address, Env, IntoVal, String, -}; +use soroban_sdk::{testutils::Address as _, Address, Env, String, Vec}; #[test] fn test_create_pool() { @@ -143,6 +140,7 @@ fn test_multiple_pools() { #[should_panic(expected = "Application status not found")] fn test_claim_funds_no_status() { let env = Env::default(); + env.mock_all_auths(); let contract_id = env.register(Contract, ()); let client = ContractClient::new(&env, &contract_id); @@ -161,23 +159,14 @@ fn test_claim_funds_no_status() { client.donate(&pool_id, &creator, &500_000_000); // Try to claim without setting status - should panic - client - .mock_auths(&[MockAuth { - address: &student, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "claim_funds", - args: (&student, &pool_id, &100_000_000i128, &token_address).into_val(&env), - sub_invokes: &[], - }, - }]) - .claim_funds(&student, &pool_id, &100_000_000i128, &token_address); + client.claim_funds(&student, &pool_id, &100_000_000i128, &token_address); } #[test] #[should_panic(expected = "Application is not approved")] fn test_claim_funds_rejected_application() { let env = Env::default(); + env.mock_all_auths(); let contract_id = env.register(Contract, ()); let client = ContractClient::new(&env, &contract_id); @@ -199,23 +188,14 @@ fn test_claim_funds_rejected_application() { client.set_application_status(&pool_id, &student, &String::from_str(&env, "Rejected")); // Try to claim with rejected status - should panic - client - .mock_auths(&[MockAuth { - address: &student, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "claim_funds", - args: (&student, &pool_id, &100_000_000i128, &token_address).into_val(&env), - sub_invokes: &[], - }, - }]) - .claim_funds(&student, &pool_id, &100_000_000i128, &token_address); + client.claim_funds(&student, &pool_id, &100_000_000i128, &token_address); } #[test] #[should_panic(expected = "Overdraw attempt")] fn test_claim_funds_overdraw() { let env = Env::default(); + env.mock_all_auths(); let contract_id = env.register(Contract, ()); let client = ContractClient::new(&env, &contract_id); @@ -237,23 +217,14 @@ fn test_claim_funds_overdraw() { client.set_application_status(&pool_id, &student, &String::from_str(&env, "Approved")); // Try to claim more than available - should panic - client - .mock_auths(&[MockAuth { - address: &student, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "claim_funds", - args: (&student, &pool_id, &500_000_000i128, &token_address).into_val(&env), - sub_invokes: &[], - }, - }]) - .claim_funds(&student, &pool_id, &500_000_000i128, &token_address); + client.claim_funds(&student, &pool_id, &500_000_000i128, &token_address); } #[test] #[should_panic(expected = "Claim amount must be positive")] fn test_claim_funds_negative_amount() { let env = Env::default(); + env.mock_all_auths(); let contract_id = env.register(Contract, ()); let client = ContractClient::new(&env, &contract_id); @@ -275,17 +246,7 @@ fn test_claim_funds_negative_amount() { client.set_application_status(&pool_id, &student, &String::from_str(&env, "Approved")); // Try to claim negative amount - should panic - client - .mock_auths(&[MockAuth { - address: &student, - invoke: &MockAuthInvoke { - contract: &contract_id, - fn_name: "claim_funds", - args: (&student, &pool_id, &-100_000_000i128, &token_address).into_val(&env), - sub_invokes: &[], - }, - }]) - .claim_funds(&student, &pool_id, &-100_000_000i128, &token_address); + client.claim_funds(&student, &pool_id, &-100_000_000i128, &token_address); } #[test]