diff --git a/contracts/geev-core/src/giveaway.rs b/contracts/geev-core/src/giveaway.rs index 75eb136..275f45b 100644 --- a/contracts/geev-core/src/giveaway.rs +++ b/contracts/geev-core/src/giveaway.rs @@ -1,5 +1,8 @@ +#![allow(clippy::too_many_arguments)] use crate::profile::ProfileContract; -use crate::types::{DataKey, Error, Giveaway, GiveawayStatus, ParticipantVerification}; +use crate::types::{ + DataKey, Error, Giveaway, GiveawayStatus, ParticipantVerification, SelectionMethod, +}; use crate::utils::with_reentrancy_guard; use soroban_sdk::{ contract, contractevent, contractimpl, panic_with_error, token, Address, Env, String, Vec, @@ -42,6 +45,31 @@ impl GiveawayContract { duration_seconds: u64, winner_count: u32, verification: Option, + ) -> u64 { + Self::create_giveaway_with_selection( + env, + creator, + token, + amount, + title, + duration_seconds, + winner_count, + verification, + SelectionMethod::Random, + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn create_giveaway_with_selection( + env: Env, + creator: Address, + token: Address, + amount: i128, + title: String, + duration_seconds: u64, + winner_count: u32, + verification: Option, + selection_method: SelectionMethod, ) -> u64 { creator.require_auth(); @@ -83,6 +111,7 @@ impl GiveawayContract { winners: Vec::new(&env), verification_type, min_reputation, + selection_method, }; if let Some(verification) = &verification { @@ -170,24 +199,17 @@ impl GiveawayContract { pub fn pick_winner(env: Env, giveaway_id: u64) -> Address { let giveaway_key = DataKey::Giveaway(giveaway_id); - let mut giveaway: Giveaway = env + let giveaway: Giveaway = env .storage() .persistent() .get(&giveaway_key) .unwrap_or_else(|| panic_with_error!(&env, Error::GiveawayNotFound)); - if giveaway.status != GiveawayStatus::Active { + if giveaway.selection_method != SelectionMethod::Random { panic_with_error!(&env, Error::InvalidStatus); } - if env.ledger().timestamp() <= giveaway.end_time { - panic_with_error!(&env, Error::GiveawayStillActive); - } - if giveaway.participant_count == 0 { - panic_with_error!(&env, Error::NoParticipants); - } - if giveaway.participant_count < giveaway.winner_count { - panic_with_error!(&env, Error::InsufficientParticipants); - } + + Self::ensure_ready_for_selection(&env, &giveaway); let random_seed = env.prng().gen::(); let mut selected_indexes: Vec = Vec::new(&env); @@ -220,50 +242,9 @@ impl GiveawayContract { .get(&participant_key) .unwrap_or_else(|| panic_with_error!(&env, Error::InvalidIndex)); winners.push_back(winner_address.clone()); - - // Publish the approximate prize share for each winner. - let fee_key = DataKey::Fee; - let fee_bps: u32 = env.storage().instance().get(&fee_key).unwrap_or(100); - let fee_amount = giveaway - .amount - .checked_mul(fee_bps as i128) - .and_then(|v| v.checked_div(10_000)) - .unwrap_or_else(|| panic_with_error!(&env, Error::ArithmeticOverflow)); - let net_prize = giveaway - .amount - .checked_sub(fee_amount) - .unwrap_or_else(|| panic_with_error!(&env, Error::ArithmeticOverflow)); - let winner_count = target_count as i128; - let base_prize = net_prize - .checked_div(winner_count) - .unwrap_or_else(|| panic_with_error!(&env, Error::ArithmeticOverflow)); - let prize_amount = if i == 0 { - net_prize - .checked_sub( - base_prize - .checked_mul(winner_count - 1) - .unwrap_or_else(|| panic_with_error!(&env, Error::ArithmeticOverflow)), - ) - .unwrap_or_else(|| panic_with_error!(&env, Error::ArithmeticOverflow)) - } else { - base_prize - }; - - GiveawayWinnerSelected { - winner: winner_address.clone(), - giveaway_id, - prize_amount, - } - .publish(&env); } - giveaway.winners = winners.clone(); - giveaway.status = GiveawayStatus::Claimable; - env.storage().persistent().set(&giveaway_key, &giveaway); - - winners - .first() - .unwrap_or_else(|| panic_with_error!(&env, Error::NoParticipants)) + Self::finalize_winners(&env, &giveaway_key, giveaway, winners) } pub fn distribute_prize(env: Env, giveaway_id: u64) { @@ -414,4 +395,230 @@ impl GiveawayContract { env.storage().persistent().set(&collected_fees_key, &0i128); } } + + // Helper function to ensure the giveaway is ready for selection + fn ensure_ready_for_selection(env: &Env, giveaway: &Giveaway) { + if giveaway.status != GiveawayStatus::Active { + panic_with_error!(env, Error::InvalidStatus); + } + if env.ledger().timestamp() <= giveaway.end_time { + panic_with_error!(env, Error::GiveawayStillActive); + } + if giveaway.participant_count == 0 { + panic_with_error!(env, Error::NoParticipants); + } + if giveaway.participant_count < giveaway.winner_count { + panic_with_error!(env, Error::InsufficientParticipants); + } + } + + // Helper function to check if caller is creator or admin + fn ensure_creator_or_admin(env: &Env, caller: &Address, giveaway: &Giveaway) { + if *caller == giveaway.creator { + return; + } + let admin_key = DataKey::Admin; + let admin: Option
= env.storage().instance().get(&admin_key); + if let Some(admin) = admin { + if *caller == admin { + return; + } + } + panic_with_error!(env, Error::NotCreator); + } + + // Helper function to validate manual winners are all valid participants + fn validate_manual_winners(env: &Env, giveaway_id: u64, winners: &Vec
) { + for winner in winners.iter() { + let has_entered_key = DataKey::HasEntered(giveaway_id, winner.clone()); + let has_entered: bool = env + .storage() + .persistent() + .get(&has_entered_key) + .unwrap_or(false); + if !has_entered { + panic_with_error!(env, Error::InvalidIndex); + } + } + // Check for duplicates + for i in 0..winners.len() { + for j in i + 1..winners.len() { + if winners.get_unchecked(i) == winners.get_unchecked(j) { + panic_with_error!(env, Error::InvalidIndex); + } + } + } + } + + // Helper function to select winners by merit (reputation) + fn select_merit_winners( + env: &Env, + giveaway_id: u64, + winner_count: u32, + participant_count: u32, + ) -> Vec
{ + let mut participants_with_reputation = Vec::new(env); + for i in 0..participant_count { + let participant_key = DataKey::ParticipantIndex(giveaway_id, i); + let participant: Address = env.storage().persistent().get(&participant_key).unwrap(); + let reputation = ProfileContract::get_reputation(env.clone(), participant.clone()); + participants_with_reputation.push_back((participant, reputation)); + } + // Sort by reputation descending (very simple sort) + let mut sorted: Vec<(Address, u64)> = Vec::new(env); + for pr in participants_with_reputation.iter() { + let pr_clone = pr.clone(); + let mut inserted = false; + for i in 0..sorted.len() { + let existing = sorted.get_unchecked(i); + if pr_clone.1 > existing.1 { + let mut temp = Vec::new(env); + for j in 0..i { + temp.push_back(sorted.get_unchecked(j)); + } + temp.push_back(pr_clone.clone()); + for j in i..sorted.len() { + temp.push_back(sorted.get_unchecked(j)); + } + sorted = temp; + inserted = true; + break; + } + } + if !inserted { + sorted.push_back(pr_clone); + } + } + // Take top N winners + let mut winners = Vec::new(env); + for i in 0..winner_count { + winners.push_back(sorted.get_unchecked(i).0.clone()); + } + winners + } + + // Helper function to finalize winners and emit events + fn finalize_winners( + env: &Env, + giveaway_key: &DataKey, + mut giveaway: Giveaway, + winners: Vec
, + ) -> Address { + // Emit winner events + let fee_key = DataKey::Fee; + let fee_bps: u32 = env.storage().instance().get(&fee_key).unwrap_or(100); + let fee_amount = giveaway + .amount + .checked_mul(fee_bps as i128) + .and_then(|v| v.checked_div(10_000)) + .unwrap_or_else(|| panic_with_error!(env, Error::ArithmeticOverflow)); + let net_prize = giveaway + .amount + .checked_sub(fee_amount) + .unwrap_or_else(|| panic_with_error!(env, Error::ArithmeticOverflow)); + let winner_count = giveaway.winner_count as i128; + let base_prize = net_prize + .checked_div(winner_count) + .unwrap_or_else(|| panic_with_error!(env, Error::ArithmeticOverflow)); + + for (index, winner) in winners.iter().enumerate() { + let prize_amount = if index == 0 { + net_prize + .checked_sub( + base_prize + .checked_mul(winner_count - 1) + .unwrap_or_else(|| panic_with_error!(env, Error::ArithmeticOverflow)), + ) + .unwrap_or_else(|| panic_with_error!(env, Error::ArithmeticOverflow)) + } else { + base_prize + }; + GiveawayWinnerSelected { + winner: winner.clone(), + giveaway_id: giveaway.id, + prize_amount, + } + .publish(env); + } + + giveaway.winners = winners.clone(); + giveaway.status = GiveawayStatus::Claimable; + env.storage().persistent().set(giveaway_key, &giveaway); + + winners + .first() + .unwrap_or_else(|| panic_with_error!(env, Error::NoParticipants)) + } + + /// Finalize a giveaway with manually selected winners + /// + /// # Arguments + /// * `env` - The contract environment + /// * `caller` - The address calling this function (must be creator or admin) + /// * `giveaway_id` - The ID of the giveaway to finalize + /// * `winners` - The list of winner addresses + pub fn finalize_manual_winners( + env: Env, + caller: Address, + giveaway_id: u64, + winners: Vec
, + ) -> Address { + caller.require_auth(); + + let giveaway_key = DataKey::Giveaway(giveaway_id); + let giveaway: Giveaway = env + .storage() + .persistent() + .get(&giveaway_key) + .unwrap_or_else(|| panic_with_error!(&env, Error::GiveawayNotFound)); + + // Validate + Self::ensure_creator_or_admin(&env, &caller, &giveaway); + Self::ensure_ready_for_selection(&env, &giveaway); + + if giveaway.selection_method != SelectionMethod::Manual { + panic_with_error!(&env, Error::InvalidStatus); + } + + if winners.len() != giveaway.winner_count { + panic_with_error!(&env, Error::InvalidWinnerCount); + } + + Self::validate_manual_winners(&env, giveaway_id, &winners); + + Self::finalize_winners(&env, &giveaway_key, giveaway, winners) + } + + /// Finalize a giveaway with merit-based selected winners (by reputation) + /// + /// # Arguments + /// * `env` - The contract environment + /// * `caller` - The address calling this function (must be creator or admin) + /// * `giveaway_id` - The ID of the giveaway to finalize + pub fn finalize_merit_winners(env: Env, caller: Address, giveaway_id: u64) -> Address { + caller.require_auth(); + + let giveaway_key = DataKey::Giveaway(giveaway_id); + let giveaway: Giveaway = env + .storage() + .persistent() + .get(&giveaway_key) + .unwrap_or_else(|| panic_with_error!(&env, Error::GiveawayNotFound)); + + // Validate + Self::ensure_creator_or_admin(&env, &caller, &giveaway); + Self::ensure_ready_for_selection(&env, &giveaway); + + if giveaway.selection_method != SelectionMethod::Merit { + panic_with_error!(&env, Error::InvalidStatus); + } + + let winners = Self::select_merit_winners( + &env, + giveaway_id, + giveaway.winner_count, + giveaway.participant_count, + ); + Self::finalize_winners(&env, &giveaway_key, giveaway, winners) + } } diff --git a/contracts/geev-core/src/test.rs b/contracts/geev-core/src/test.rs index 3e73664..6b3996a 100644 --- a/contracts/geev-core/src/test.rs +++ b/contracts/geev-core/src/test.rs @@ -1593,7 +1593,7 @@ fn test_flag_counts_are_independent_per_id() { // ── auto-suspension tests ───────────────────────────────────────────────────── use crate::governance::FLAG_THRESHOLD; -use crate::types::GiveawayStatus; +use crate::types::{GiveawayStatus, SelectionMethod}; /// Seed a minimal active Giveaway directly into contract storage. fn seed_active_giveaway(env: &Env, contract_id: &Address, giveaway_id: u64, token: &Address) { @@ -1611,6 +1611,7 @@ fn seed_active_giveaway(env: &Env, contract_id: &Address, giveaway_id: u64, toke winners: Vec::new(env), verification_type: 0, min_reputation: 0, + selection_method: SelectionMethod::Random, }; env.as_contract(contract_id, || { env.storage() @@ -1782,6 +1783,7 @@ fn test_enter_suspended_giveaway_fails() { winners: Vec::new(&env), verification_type: 0, min_reputation: 0, + selection_method: SelectionMethod::Random, }, ); }); @@ -1946,514 +1948,269 @@ fn test_reputation_accumulates_across_giveaways() { }); } -// TODO: The following dispute resolution tests require contract updates: -// 1. Add HelpRequestStatus::Disputed, ResolvedRelease, ResolvedRefund to types.rs -// 2. Add GiveawayStatus::Disputed, ResolvedRelease, ResolvedRefund to types.rs -// 3. Add get_request(), dispute_request(), resolve_dispute() to mutual_aid.rs -// 4. Add dispute_giveaway(), resolve_giveaway_dispute() to giveaway.rs -// Once those are implemented, uncomment the tests below. - -// // ─── Dispute Resolution Tests ───────────────────────────────────────────── -// -// #[test] -// fn test_dispute_help_request_by_creator() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let contract_id = env.register(MutualAidContract, ()); -// let client = MutualAidContractClient::new(&env, &contract_id); -// -// let token_admin = Address::generate(&env); -// let mock_token = env -// .register_stellar_asset_contract_v2(token_admin.clone()) -// .address(); -// let token_admin_client = token::StellarAssetClient::new(&env, &mock_token); -// -// let creator = Address::generate(&env); -// let donor = Address::generate(&env); -// token_admin_client.mint(&donor, &500); -// -// let request_id = 1u64; -// let goal = 1000i128; -// -// client.post_help_request(&creator, &request_id, &goal, &mock_token); -// client.donate(&donor, &request_id, &500); -// client.dispute_request(&request_id, &creator); -// -// let request = client.get_request(&request_id); -// assert_eq!(request.status, HelpRequestStatus::Disputed); -// } -// -// #[test] -// fn test_dispute_help_request_by_donor() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let contract_id = env.register(MutualAidContract, ()); -// let client = MutualAidContractClient::new(&env, &contract_id); -// -// let token_admin = Address::generate(&env); -// let mock_token = env -// .register_stellar_asset_contract_v2(token_admin.clone()) -// .address(); -// let token_admin_client = token::StellarAssetClient::new(&env, &mock_token); -// -// let creator = Address::generate(&env); -// let donor = Address::generate(&env); -// token_admin_client.mint(&donor, &500); -// -// let request_id = 1u64; -// let goal = 1000i128; -// -// client.post_help_request(&creator, &request_id, &goal, &mock_token); -// client.donate(&donor, &request_id, &500); -// client.dispute_request(&request_id, &donor); -// -// let request = client.get_request(&request_id); -// assert_eq!(request.status, HelpRequestStatus::Disputed); -// } -// -// #[test] -// #[should_panic(expected = "InvalidStatus")] -// fn test_donate_blocked_while_disputed() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let contract_id = env.register(MutualAidContract, ()); -// let client = MutualAidContractClient::new(&env, &contract_id); -// -// let token_admin = Address::generate(&env); -// let mock_token = env -// .register_stellar_asset_contract_v2(token_admin.clone()) -// .address(); -// let token_admin_client = token::StellarAssetClient::new(&env, &mock_token); -// -// let creator = Address::generate(&env); -// let donor = Address::generate(&env); -// let donor2 = Address::generate(&env); -// token_admin_client.mint(&donor, &500); -// token_admin_client.mint(&donor2, &500); -// -// let request_id = 1u64; -// let goal = 1000i128; -// -// client.post_help_request(&creator, &request_id, &goal, &mock_token); -// client.donate(&donor, &request_id, &500); -// client.dispute_request(&request_id, &creator); -// -// // Should panic — donations blocked while disputed -// client.donate(&donor2, &request_id, &500); -// } -// -// #[test] -// #[should_panic(expected = "InvalidStatus")] -// fn test_cancel_blocked_while_disputed() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let contract_id = env.register(MutualAidContract, ()); -// let client = MutualAidContractClient::new(&env, &contract_id); -// -// let token_admin = Address::generate(&env); -// let mock_token = env -// .register_stellar_asset_contract_v2(token_admin.clone()) -// .address(); -// -// let creator = Address::generate(&env); -// let request_id = 1u64; -// let goal = 1000i128; -// -// client.post_help_request(&creator, &request_id, &goal, &mock_token); -// client.dispute_request(&request_id, &creator); -// -// // Should panic — cancel blocked while disputed -// client.cancel_request(&request_id, &creator); -// } -// -// #[test] -// #[should_panic(expected = "InvalidStatus")] -// fn test_claim_refund_blocked_while_disputed() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let contract_id = env.register(MutualAidContract, ()); -// let client = MutualAidContractClient::new(&env, &contract_id); -// -// let token_admin = Address::generate(&env); -// let mock_token = env -// .register_stellar_asset_contract_v2(token_admin.clone()) -// .address(); -// let token_admin_client = token::StellarAssetClient::new(&env, &mock_token); -// -// let creator = Address::generate(&env); -// let donor = Address::generate(&env); -// token_admin_client.mint(&donor, &500); -// -// let request_id = 1u64; -// let goal = 1000i128; -// -// client.post_help_request(&creator, &request_id, &goal, &mock_token); -// client.donate(&donor, &request_id, &500); -// client.dispute_request(&request_id, &creator); -// -// // Should panic — refund blocked while disputed -// client.claim_refund(&donor, &request_id); -// } -// -// #[test] -// fn test_admin_resolves_dispute_release_funds() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let contract_id = env.register(MutualAidContract, ()); -// let client = MutualAidContractClient::new(&env, &contract_id); -// -// let token_admin = Address::generate(&env); -// let mock_token = env -// .register_stellar_asset_contract_v2(token_admin.clone()) -// .address(); -// let token_client = token::Client::new(&env, &mock_token); -// let token_admin_client = token::StellarAssetClient::new(&env, &mock_token); -// -// let admin = Address::generate(&env); -// let creator = Address::generate(&env); -// let donor = Address::generate(&env); -// token_admin_client.mint(&donor, &500); -// -// env.as_contract(&contract_id, || { -// env.storage().instance().set(&DataKey::Admin, &admin); -// }); -// -// let request_id = 1u64; -// let goal = 1000i128; -// -// client.post_help_request(&creator, &request_id, &goal, &mock_token); -// client.donate(&donor, &request_id, &500); -// client.dispute_request(&request_id, &creator); -// -// // Admin resolves — release funds to creator -// client.resolve_dispute(&request_id, &true); -// -// let request = client.get_request(&request_id); -// assert_eq!(request.status, HelpRequestStatus::ResolvedRelease); -// assert_eq!(token_client.balance(&creator), 500); -// } -// -// #[test] -// fn test_admin_resolves_dispute_refund() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let contract_id = env.register(MutualAidContract, ()); -// let client = MutualAidContractClient::new(&env, &contract_id); -// -// let token_admin = Address::generate(&env); -// let mock_token = env -// .register_stellar_asset_contract_v2(token_admin.clone()) -// .address(); -// let token_client = token::Client::new(&env, &mock_token); -// let token_admin_client = token::StellarAssetClient::new(&env, &mock_token); -// -// let admin = Address::generate(&env); -// let creator = Address::generate(&env); -// let donor = Address::generate(&env); -// token_admin_client.mint(&donor, &500); -// -// env.as_contract(&contract_id, || { -// env.storage().instance().set(&DataKey::Admin, &admin); -// }); -// -// let request_id = 1u64; -// let goal = 1000i128; -// -// client.post_help_request(&creator, &request_id, &goal, &mock_token); -// client.donate(&donor, &request_id, &500); -// client.dispute_request(&request_id, &creator); -// -// // Admin resolves — enable refunds -// client.resolve_dispute(&request_id, &false); -// -// let request = client.get_request(&request_id); -// assert_eq!(request.status, HelpRequestStatus::ResolvedRefund); -// -// client.claim_refund(&donor, &request_id); -// assert_eq!(token_client.balance(&donor), 500); -// } -// -// #[test] -// #[should_panic(expected = "NotAdmin")] -// fn test_non_admin_cannot_resolve_dispute() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let contract_id = env.register(MutualAidContract, ()); -// let client = MutualAidContractClient::new(&env, &contract_id); -// -// let token_admin = Address::generate(&env); -// let mock_token = env -// .register_stellar_asset_contract_v2(token_admin.clone()) -// .address(); -// let token_admin_client = token::StellarAssetClient::new(&env, &mock_token); -// -// let admin = Address::generate(&env); -// let creator = Address::generate(&env); -// let donor = Address::generate(&env); -// token_admin_client.mint(&donor, &500); -// -// env.as_contract(&contract_id, || { -// env.storage().instance().set(&DataKey::Admin, &admin); -// }); -// -// let request_id = 1u64; -// let goal = 1000i128; -// -// client.post_help_request(&creator, &request_id, &goal, &mock_token); -// client.donate(&donor, &request_id, &500); -// client.dispute_request(&request_id, &creator); -// -// // Random address tries to resolve — should panic -// client.resolve_dispute(&request_id, &true); -// } -// -// // ─── Giveaway Dispute Tests ─────────────────────────────────────────────── -// -// #[test] -// fn test_dispute_giveaway_by_creator() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let contract_id = env.register(GiveawayContract, ()); -// let client = GiveawayContractClient::new(&env, &contract_id); -// -// let token_admin = Address::generate(&env); -// let mock_token = env -// .register_stellar_asset_contract_v2(token_admin.clone()) -// .address(); -// let token_admin_client = token::StellarAssetClient::new(&env, &mock_token); -// -// let creator = Address::generate(&env); -// let participant = Address::generate(&env); -// token_admin_client.mint(&creator, &1000); -// -// env.as_contract(&contract_id, || { -// env.storage() -// .instance() -// .set(&DataKey::AllowedToken(mock_token.clone()), &true); -// }); -// -// let giveaway_id = client.create_giveaway( -// &creator, -// &mock_token, -// &500, -// &String::from_str(&env, "Test"), -// &60, -// &1, -// &None, -// ); -// -// client.enter_giveaway(&participant, &giveaway_id); -// client.dispute_giveaway(&giveaway_id, &creator); -// -// let giveaway: Giveaway = env.as_contract(&contract_id, || { -// env.storage() -// .persistent() -// .get(&DataKey::Giveaway(giveaway_id)) -// .unwrap() -// }); -// assert_eq!(giveaway.status, GiveawayStatus::Disputed); -// } -// -// #[test] -// #[should_panic(expected = "InvalidStatus")] -// fn test_enter_giveaway_blocked_while_disputed() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let contract_id = env.register(GiveawayContract, ()); -// let client = GiveawayContractClient::new(&env, &contract_id); -// -// let token_admin = Address::generate(&env); -// let mock_token = env -// .register_stellar_asset_contract_v2(token_admin.clone()) -// .address(); -// let token_admin_client = token::StellarAssetClient::new(&env, &mock_token); -// -// let creator = Address::generate(&env); -// let participant = Address::generate(&env); -// let latecomer = Address::generate(&env); -// token_admin_client.mint(&creator, &1000); -// -// env.as_contract(&contract_id, || { -// env.storage() -// .instance() -// .set(&DataKey::AllowedToken(mock_token.clone()), &true); -// }); -// -// let giveaway_id = client.create_giveaway( -// &creator, -// &mock_token, -// &500, -// &String::from_str(&env, "Test"), -// &60, -// &1, -// &None, -// ); -// -// client.enter_giveaway(&participant, &giveaway_id); -// client.dispute_giveaway(&giveaway_id, &creator); -// -// // Should panic — entry blocked while disputed -// client.enter_giveaway(&latecomer, &giveaway_id); -// } -// -// #[test] -// #[should_panic(expected = "InvalidStatus")] -// fn test_pick_winner_blocked_while_disputed() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let contract_id = env.register(GiveawayContract, ()); -// let client = GiveawayContractClient::new(&env, &contract_id); -// -// let token_admin = Address::generate(&env); -// let mock_token = env -// .register_stellar_asset_contract_v2(token_admin.clone()) -// .address(); -// let token_admin_client = token::StellarAssetClient::new(&env, &mock_token); -// -// let creator = Address::generate(&env); -// let participant = Address::generate(&env); -// token_admin_client.mint(&creator, &1000); -// -// env.as_contract(&contract_id, || { -// env.storage() -// .instance() -// .set(&DataKey::AllowedToken(mock_token.clone()), &true); -// }); -// -// let giveaway_id = client.create_giveaway( -// &creator, -// &mock_token, -// &500, -// &String::from_str(&env, "Test"), -// &60, -// &1, -// &None, -// ); -// -// client.enter_giveaway(&participant, &giveaway_id); -// client.dispute_giveaway(&giveaway_id, &creator); -// -// env.ledger().with_mut(|li| { -// li.timestamp += 100; -// }); -// -// // Should panic — winner selection blocked while disputed -// client.pick_winner(&giveaway_id); -// } -// -// #[test] -// fn test_admin_resolves_giveaway_dispute_release() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let contract_id = env.register(GiveawayContract, ()); -// let client = GiveawayContractClient::new(&env, &contract_id); -// -// let token_admin = Address::generate(&env); -// let mock_token = env -// .register_stellar_asset_contract_v2(token_admin.clone()) -// .address(); -// let token_client = token::Client::new(&env, &mock_token); -// let token_admin_client = token::StellarAssetClient::new(&env, &mock_token); -// -// let admin = Address::generate(&env); -// let creator = Address::generate(&env); -// let participant = Address::generate(&env); -// token_admin_client.mint(&creator, &1000); -// -// env.as_contract(&contract_id, || { -// env.storage() -// .instance() -// .set(&DataKey::AllowedToken(mock_token.clone()), &true); -// env.storage().instance().set(&DataKey::Admin, &admin); -// }); -// -// let giveaway_id = client.create_giveaway( -// &creator, -// &mock_token, -// &500, -// &String::from_str(&env, "Test"), -// &60, -// &1, -// &None, -// ); -// -// client.enter_giveaway(&participant, &giveaway_id); -// client.dispute_giveaway(&giveaway_id, &creator); -// -// // Admin resolves — release funds (distribute prizes) -// client.resolve_giveaway_dispute(&giveaway_id, &true); -// -// let giveaway: Giveaway = env.as_contract(&contract_id, || { -// env.storage() -// .persistent() -// .get(&DataKey::Giveaway(giveaway_id)) -// .unwrap() -// }); -// assert_eq!(giveaway.status, GiveawayStatus::ResolvedRelease); -// assert_eq!(token_client.balance(&participant), 500); -// } -// -// #[test] -// fn test_admin_resolves_giveaway_dispute_refund() { -// let env = Env::default(); -// env.mock_all_auths(); -// -// let contract_id = env.register(GiveawayContract, ()); -// let client = GiveawayContractClient::new(&env, &contract_id); -// -// let token_admin = Address::generate(&env); -// let mock_token = env -// .register_stellar_asset_contract_v2(token_admin.clone()) -// .address(); -// let token_client = token::Client::new(&env, &mock_token); -// let token_admin_client = token::StellarAssetClient::new(&env, &mock_token); -// -// let admin = Address::generate(&env); -// let creator = Address::generate(&env); -// let participant = Address::generate(&env); -// token_admin_client.mint(&creator, &1000); -// -// env.as_contract(&contract_id, || { -// env.storage() -// .instance() -// .set(&DataKey::AllowedToken(mock_token.clone()), &true); -// env.storage().instance().set(&DataKey::Admin, &admin); -// }); -// -// let giveaway_id = client.create_giveaway( -// &creator, -// &mock_token, -// &500, -// &String::from_str(&env, "Test"), -// &60, -// &1, -// &None, -// ); -// -// client.enter_giveaway(&participant, &giveaway_id); -// client.dispute_giveaway(&giveaway_id, &creator); -// -// // Admin resolves — refund creator -// client.resolve_giveaway_dispute(&giveaway_id, &false); -// -// let giveaway: Giveaway = env.as_contract(&contract_id, || { -// env.storage() -// .persistent() -// .get(&DataKey::Giveaway(giveaway_id)) -// .unwrap() -// }); -// assert_eq!(giveaway.status, GiveawayStatus::ResolvedRefund); -// assert_eq!(token_client.balance(&creator), 1000); -// } +#[test] +fn test_manual_winner_selection() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(GiveawayContract, ()); + let client = GiveawayContractClient::new(&env, &contract_id); + + let token_admin = Address::generate(&env); + let mock_token = env + .register_stellar_asset_contract_v2(token_admin.clone()) + .address(); + let token_admin_client = token::StellarAssetClient::new(&env, &mock_token); + + let creator = Address::generate(&env); + let participant1 = Address::generate(&env); + let participant2 = Address::generate(&env); + let participant3 = Address::generate(&env); + token_admin_client.mint(&creator, &1000); + + env.as_contract(&contract_id, || { + env.storage() + .instance() + .set(&DataKey::AllowedToken(mock_token.clone()), &true); + }); + + let giveaway_id = client.create_giveaway_with_selection( + &creator, + &mock_token, + &500, + &String::from_str(&env, "Manual Test"), + &60, + &2, + &None, + &SelectionMethod::Manual, + ); + + client.enter_giveaway(&participant1, &giveaway_id); + client.enter_giveaway(&participant2, &giveaway_id); + client.enter_giveaway(&participant3, &giveaway_id); + + env.ledger().with_mut(|li| li.timestamp += 100); + + let mut winners = Vec::new(&env); + winners.push_back(participant2.clone()); + winners.push_back(participant3.clone()); + + let winner = client.finalize_manual_winners(&creator, &giveaway_id, &winners); + + assert!(winner == participant2 || winner == participant3); + + let stored_winners: Vec
= env.as_contract(&contract_id, || { + let g: Giveaway = env + .storage() + .persistent() + .get(&DataKey::Giveaway(giveaway_id)) + .unwrap(); + g.winners + }); + + assert_eq!(stored_winners.len(), 2); + assert!(stored_winners.contains(&participant2)); + assert!(stored_winners.contains(&participant3)); +} + +#[test] +#[should_panic] +fn test_manual_winner_selection_fails_non_creator() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(GiveawayContract, ()); + let client = GiveawayContractClient::new(&env, &contract_id); + + let token_admin = Address::generate(&env); + let mock_token = env + .register_stellar_asset_contract_v2(token_admin.clone()) + .address(); + let token_admin_client = token::StellarAssetClient::new(&env, &mock_token); + + let creator = Address::generate(&env); + let random_user = Address::generate(&env); + let participant = Address::generate(&env); + token_admin_client.mint(&creator, &1000); + + env.as_contract(&contract_id, || { + env.storage() + .instance() + .set(&DataKey::AllowedToken(mock_token.clone()), &true); + }); + + let giveaway_id = client.create_giveaway_with_selection( + &creator, + &mock_token, + &500, + &String::from_str(&env, "Manual Test"), + &60, + &1, + &None, + &SelectionMethod::Manual, + ); + + client.enter_giveaway(&participant, &giveaway_id); + env.ledger().with_mut(|li| li.timestamp += 100); + + let mut winners = Vec::new(&env); + winners.push_back(participant.clone()); + client.finalize_manual_winners(&random_user, &giveaway_id, &winners); +} + +#[test] +fn test_merit_winner_selection() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(GiveawayContract, ()); + let client = GiveawayContractClient::new(&env, &contract_id); + + let token_admin = Address::generate(&env); + let mock_token = env + .register_stellar_asset_contract_v2(token_admin.clone()) + .address(); + let token_admin_client = token::StellarAssetClient::new(&env, &mock_token); + + let creator = Address::generate(&env); + let participant1 = Address::generate(&env); // rep 10 + let participant2 = Address::generate(&env); // rep 50 + let participant3 = Address::generate(&env); // rep 30 + token_admin_client.mint(&creator, &1000); + + env.as_contract(&contract_id, || { + env.storage() + .instance() + .set(&DataKey::AllowedToken(mock_token.clone()), &true); + env.storage() + .persistent() + .set(&DataKey::Reputation(participant1.clone()), &10u64); + env.storage() + .persistent() + .set(&DataKey::Reputation(participant2.clone()), &50u64); + env.storage() + .persistent() + .set(&DataKey::Reputation(participant3.clone()), &30u64); + }); + + let giveaway_id = client.create_giveaway_with_selection( + &creator, + &mock_token, + &500, + &String::from_str(&env, "Merit Test"), + &60, + &2, + &None, + &SelectionMethod::Merit, + ); + + client.enter_giveaway(&participant1, &giveaway_id); + client.enter_giveaway(&participant2, &giveaway_id); + client.enter_giveaway(&participant3, &giveaway_id); + + env.ledger().with_mut(|li| li.timestamp += 100); + + let winner = client.finalize_merit_winners(&creator, &giveaway_id); + assert_eq!(winner, participant2); + + let stored_winners: Vec
= env.as_contract(&contract_id, || { + let g: Giveaway = env + .storage() + .persistent() + .get(&DataKey::Giveaway(giveaway_id)) + .unwrap(); + g.winners + }); + + assert_eq!(stored_winners.len(), 2); + assert_eq!(stored_winners.get(0).unwrap(), participant2); + assert_eq!(stored_winners.get(1).unwrap(), participant3); +} + +#[test] +#[should_panic] +fn test_pick_winner_fails_on_manual_giveaway() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(GiveawayContract, ()); + let client = GiveawayContractClient::new(&env, &contract_id); + + let token_admin = Address::generate(&env); + let mock_token = env + .register_stellar_asset_contract_v2(token_admin.clone()) + .address(); + let token_admin_client = token::StellarAssetClient::new(&env, &mock_token); + + let creator = Address::generate(&env); + let participant = Address::generate(&env); + token_admin_client.mint(&creator, &1000); + + env.as_contract(&contract_id, || { + env.storage() + .instance() + .set(&DataKey::AllowedToken(mock_token.clone()), &true); + }); + + let giveaway_id = client.create_giveaway_with_selection( + &creator, + &mock_token, + &500, + &String::from_str(&env, "Test"), + &60, + &1, + &None, + &SelectionMethod::Manual, + ); + + client.enter_giveaway(&participant, &giveaway_id); + env.ledger().with_mut(|li| li.timestamp += 100); + client.pick_winner(&giveaway_id); +} + +#[test] +fn test_admin_can_finalize_manual_winners() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(GiveawayContract, ()); + let client = GiveawayContractClient::new(&env, &contract_id); + + let token_admin = Address::generate(&env); + let admin = Address::generate(&env); + let mock_token = env + .register_stellar_asset_contract_v2(token_admin.clone()) + .address(); + let token_admin_client = token::StellarAssetClient::new(&env, &mock_token); + + let creator = Address::generate(&env); + let participant = Address::generate(&env); + token_admin_client.mint(&creator, &1000); + + client.init(&admin, &100); + + env.as_contract(&contract_id, || { + env.storage() + .instance() + .set(&DataKey::AllowedToken(mock_token.clone()), &true); + }); + + let giveaway_id = client.create_giveaway_with_selection( + &creator, + &mock_token, + &500, + &String::from_str(&env, "Admin Test"), + &60, + &1, + &None, + &SelectionMethod::Manual, + ); + + client.enter_giveaway(&participant, &giveaway_id); + env.ledger().with_mut(|li| li.timestamp += 100); + + let mut winners = Vec::new(&env); + winners.push_back(participant.clone()); + + client.finalize_manual_winners(&admin, &giveaway_id, &winners); +} diff --git a/contracts/geev-core/src/types.rs b/contracts/geev-core/src/types.rs index 4d1a329..4621397 100644 --- a/contracts/geev-core/src/types.rs +++ b/contracts/geev-core/src/types.rs @@ -54,6 +54,14 @@ pub struct ParticipantVerification { pub uses_reputation: bool, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[contracttype] +pub enum SelectionMethod { + Random = 0, + Manual = 1, + Merit = 2, +} + #[derive(Clone)] #[contracttype] pub struct Giveaway { @@ -69,6 +77,7 @@ pub struct Giveaway { pub winners: Vec
, pub verification_type: u32, pub min_reputation: u64, + pub selection_method: SelectionMethod, } #[derive(Clone, Copy, Debug, PartialEq, Eq)]