diff --git a/contract/src/lib.rs b/contract/src/lib.rs index 83c52a2..9125e3e 100644 --- a/contract/src/lib.rs +++ b/contract/src/lib.rs @@ -57,6 +57,18 @@ use payment::{ validate_distribution as pay_validate_distribution, DistributionRule, DistributionStatus, }; +mod reputation; +use reputation::{ + apply_time_decay as rep_apply_time_decay, award_achievement as rep_award_achievement, + calculate_multiplier as rep_calculate_multiplier, + check_achievement_eligibility as rep_check_achievement_eligibility, + get_achievements as rep_get_achievements, get_reputation as rep_get_reputation, + get_tier as rep_get_tier, get_top_contributors_for_guild as rep_get_top_contributors, + initialize_profile as rep_initialize_profile, initialize_reputation_system, + update_guild_leaderboard as rep_update_leaderboard, update_reputation as rep_update_reputation, +}; +use reputation::types::{Achievement, ReputationEvent, ReputationProfile, ReputationTier}; + /// Stellar Guilds - Main Contract Entry Point /// /// This is the foundational contract for the Stellar Guilds platform. @@ -78,6 +90,7 @@ pub struct StellarGuildsContract; impl StellarGuildsContract { pub fn initialize(env: Env) -> bool { storage::initialize(&env); + initialize_reputation_system(&env); true } @@ -1073,6 +1086,145 @@ impl StellarGuildsContract { pub fn get_guild_bounties(env: Env, guild_id: u64) -> Vec { get_guild_bounties_list(&env, guild_id) } + + // ============ Reputation & Incentive Functions ============ + + /// Initialize a reputation profile for a contributor + /// + /// # Arguments + /// * `address` - The address of the contributor + /// + /// # Returns + /// The newly created reputation profile + pub fn initialize_reputation_profile(env: Env, address: Address) -> ReputationProfile { + rep_initialize_profile(&env, address) + } + + /// Update reputation based on an event + /// + /// # Arguments + /// * `address` - The address of the contributor + /// * `event` - The type of reputation event + /// * `value` - Context-specific value (complexity, significance, etc.) + /// + /// # Returns + /// The new reputation score + pub fn update_contributor_reputation( + env: Env, + address: Address, + event: ReputationEvent, + value: u32, + ) -> u32 { + rep_update_reputation(&env, address, event, value) + } + + /// Award an achievement to a contributor + /// + /// # Arguments + /// * `address` - The address of the contributor + /// * `achievement_id` - The ID of the achievement to award + /// + /// # Returns + /// true if the achievement was awarded successfully + pub fn award_contributor_achievement( + env: Env, + address: Address, + achievement_id: u64, + ) -> bool { + rep_award_achievement(&env, address, achievement_id) + } + + /// Get the reputation profile for a contributor + /// + /// # Arguments + /// * `address` - The address of the contributor + /// + /// # Returns + /// The contributor's reputation profile + pub fn get_contributor_reputation(env: Env, address: Address) -> ReputationProfile { + rep_get_reputation(&env, address) + } + + /// Get the reputation tier for a contributor + /// + /// # Arguments + /// * `address` - The address of the contributor + /// + /// # Returns + /// The contributor's current tier + pub fn get_contributor_tier(env: Env, address: Address) -> ReputationTier { + rep_get_tier(&env, address) + } + + /// Calculate the incentive multiplier for a contributor + /// + /// # Arguments + /// * `address` - The address of the contributor + /// + /// # Returns + /// The incentive multiplier in basis points (100 = 1.0x) + pub fn calculate_contributor_incentive_multiplier(env: Env, address: Address) -> u32 { + rep_calculate_multiplier(&env, address) + } + + /// Get top contributors for a guild + /// + /// # Arguments + /// * `guild_id` - The ID of the guild + /// * `limit` - Maximum number of contributors to return + /// + /// # Returns + /// List of top contributor addresses + pub fn get_guild_top_contributors(env: Env, guild_id: u64, limit: u32) -> Vec
{ + rep_get_top_contributors(&env, guild_id, limit) + } + + /// Check if a contributor is eligible for a specific achievement + /// + /// # Arguments + /// * `address` - The address of the contributor + /// * `achievement_id` - The ID of the achievement + /// + /// # Returns + /// true if the contributor is eligible + pub fn check_contributor_achievement_eligibility( + env: Env, + address: Address, + achievement_id: u64, + ) -> bool { + rep_check_achievement_eligibility(&env, address, achievement_id) + } + + /// Get all achievements earned by a contributor + /// + /// # Arguments + /// * `address` - The address of the contributor + /// + /// # Returns + /// List of earned achievements + pub fn get_contributor_achievements(env: Env, address: Address) -> Vec { + rep_get_achievements(&env, address) + } + + /// Update guild leaderboard for a contributor + /// + /// # Arguments + /// * `guild_id` - The ID of the guild + /// * `address` - The address of the contributor + pub fn update_contributor_leaderboard(env: Env, guild_id: u64, address: Address) { + rep_update_leaderboard(&env, guild_id, &address); + } + + /// Apply time decay to a contributor's reputation + /// + /// # Arguments + /// * `address` - The address of the contributor + /// + /// # Returns + /// The new reputation score after decay + pub fn apply_contributor_time_decay(env: Env, address: Address) -> u32 { + rep_apply_time_decay(&env, address) + } } #[cfg(test)] diff --git a/contract/src/reputation/achievements.rs b/contract/src/reputation/achievements.rs new file mode 100644 index 0000000..362c196 --- /dev/null +++ b/contract/src/reputation/achievements.rs @@ -0,0 +1,236 @@ +use soroban_sdk::{Env, String, Vec}; + +use super::storage::{get_achievement, get_all_achievements, get_next_achievement_id, set_achievement}; +use super::types::{Achievement, ReputationProfile}; + +// ============ Achievement Management ============ + +/// Create a new achievement definition +pub fn create_achievement( + env: &Env, + name: String, + description: String, + points: u32, + criteria: String, + min_tasks: u32, + min_success_rate: u32, +) -> u64 { + let achievement_id = get_next_achievement_id(env); + + let achievement = Achievement { + id: achievement_id, + name, + description, + points, + criteria, + min_tasks, + min_success_rate, + }; + + set_achievement(env, &achievement); + + achievement_id +} + +/// Check if a contributor is eligible for a specific achievement +pub fn check_eligibility( + env: &Env, + profile: &ReputationProfile, + achievement_id: u64, +) -> bool { + // Check if already awarded + if has_achievement(profile, achievement_id) { + return false; + } + + // Get achievement definition + let achievement = match get_achievement(env, achievement_id) { + Some(ach) => ach, + None => return false, + }; + + // Check criteria + let meets_task_requirement = profile.tasks_completed >= achievement.min_tasks; + let meets_success_rate = profile.success_rate >= achievement.min_success_rate; + + meets_task_requirement && meets_success_rate +} + +/// Check if profile already has an achievement +pub fn has_achievement(profile: &ReputationProfile, achievement_id: u64) -> bool { + for i in 0..profile.achievements.len() { + if let Some(id) = profile.achievements.get(i) { + if id == achievement_id { + return true; + } + } + } + false +} + +/// Get all achievements earned by a contributor +pub fn get_contributor_achievements( + env: &Env, + profile: &ReputationProfile, +) -> Vec { + let mut achievements = Vec::new(env); + + for i in 0..profile.achievements.len() { + if let Some(achievement_id) = profile.achievements.get(i) { + if let Some(achievement) = get_achievement(env, achievement_id) { + achievements.push_back(achievement); + } + } + } + + achievements +} + +/// Get all eligible achievements for a contributor +pub fn get_eligible_achievements( + env: &Env, + profile: &ReputationProfile, +) -> Vec { + let all_achievements = get_all_achievements(env); + let mut eligible = Vec::new(env); + + for i in 0..all_achievements.len() { + if let Some(achievement) = all_achievements.get(i) { + if check_eligibility(env, profile, achievement.id) { + eligible.push_back(achievement); + } + } + } + + eligible +} + +/// Initialize default achievements +pub fn initialize_default_achievements(env: &Env) { + // First Task Achievement + create_achievement( + env, + String::from_str(env, "First Steps"), + String::from_str(env, "Complete your first task"), + 10, + String::from_str(env, "Complete 1 task"), + 1, + 0, + ); + + // Task Veteran + create_achievement( + env, + String::from_str(env, "Task Veteran"), + String::from_str(env, "Complete 10 tasks"), + 50, + String::from_str(env, "Complete 10 tasks"), + 10, + 0, + ); + + // Task Master + create_achievement( + env, + String::from_str(env, "Task Master"), + String::from_str(env, "Complete 50 tasks"), + 200, + String::from_str(env, "Complete 50 tasks"), + 50, + 0, + ); + + // Perfect Record + create_achievement( + env, + String::from_str(env, "Perfect Record"), + String::from_str(env, "Maintain 100% success rate with 10+ tasks"), + 100, + String::from_str(env, "Complete 10 tasks with 100% success rate"), + 10, + 100, + ); + + // Reliable Contributor + create_achievement( + env, + String::from_str(env, "Reliable Contributor"), + String::from_str(env, "Maintain 95%+ success rate with 20+ tasks"), + 150, + String::from_str(env, "Complete 20 tasks with 95%+ success rate"), + 20, + 95, + ); + + // Century Club + create_achievement( + env, + String::from_str(env, "Century Club"), + String::from_str(env, "Complete 100 tasks"), + 500, + String::from_str(env, "Complete 100 tasks"), + 100, + 0, + ); + + // Dispute Resolver + create_achievement( + env, + String::from_str(env, "Dispute Resolver"), + String::from_str(env, "Win 5 disputes"), + 75, + String::from_str(env, "Win 5 disputes with 80%+ success"), + 5, + 80, + ); + + // Elite Contributor + create_achievement( + env, + String::from_str(env, "Elite Contributor"), + String::from_str(env, "Reach Diamond tier"), + 1000, + String::from_str(env, "Reach 5000+ reputation points"), + 50, + 90, + ); +} + +#[cfg(test)] +mod tests { + use super::*; + use soroban_sdk::{testutils::Address as _, Address, Env}; + use crate::reputation::types::ReputationTier; + + #[test] + fn test_has_achievement() { + let env = Env::default(); + let address = Address::generate(&env); + + let mut achievements = Vec::new(&env); + achievements.push_back(1u64); + achievements.push_back(5u64); + achievements.push_back(10u64); + + let profile = ReputationProfile { + address: address.clone(), + score: 100, + tier: ReputationTier::Silver, + tasks_completed: 10, + tasks_failed: 0, + success_rate: 100, + achievements, + last_activity: 0, + created_at: 0, + disputes_won: 0, + disputes_lost: 0, + milestones_completed: 0, + }; + + assert_eq!(has_achievement(&profile, 1), true); + assert_eq!(has_achievement(&profile, 5), true); + assert_eq!(has_achievement(&profile, 10), true); + assert_eq!(has_achievement(&profile, 2), false); + assert_eq!(has_achievement(&profile, 99), false); + } +} diff --git a/contract/src/reputation/integration_tests.rs b/contract/src/reputation/integration_tests.rs new file mode 100644 index 0000000..36fbe5b --- /dev/null +++ b/contract/src/reputation/integration_tests.rs @@ -0,0 +1,470 @@ +#[cfg(test)] +mod integration_tests { + use crate::reputation::types::{ReputationEvent, ReputationTier}; + use crate::reputation::{ + get_reputation, get_tier, initialize_profile, initialize_reputation_system, + update_reputation, update_guild_leaderboard, get_top_contributors_for_guild, + award_achievement, check_achievement_eligibility, + }; + use soroban_sdk::testutils::Address as _; + use soroban_sdk::{Address, Env}; + + /// Test integration between bounty completion and reputation updates + #[test] + fn test_bounty_completion_reputation_flow() { + let env = Env::default(); + env.budget().reset_unlimited(); + initialize_reputation_system(&env); + + let contributor = Address::generate(&env); + let guild_id = 1u64; + + // Simulate bounty workflow: + // 1. Contributor claims bounty + // 2. Submits work + // 3. Work is approved + // 4. Reputation is updated + + // Complete a bounty (complexity 3) + update_reputation( + &env, + contributor.clone(), + ReputationEvent::TaskCompleted, + 3, + ); + + let profile = get_reputation(&env, contributor.clone()); + assert_eq!(profile.tasks_completed, 1); + assert_eq!(profile.score, 30); // 10 * 3 + + // Update guild leaderboard + update_guild_leaderboard(&env, guild_id, &contributor); + + // Verify on leaderboard + let top = get_top_contributors_for_guild(&env, guild_id, 10); + assert_eq!(top.len(), 1); + assert_eq!(top.get(0).unwrap(), contributor); + } + + /// Test integration between milestone completion and reputation updates + #[test] + fn test_milestone_completion_reputation_flow() { + let env = Env::default(); + env.budget().reset_unlimited(); + initialize_reputation_system(&env); + + let contributor = Address::generate(&env); + let guild_id = 1u64; + + // Simulate milestone workflow: + // 1. Project created with milestones + // 2. Contributor completes milestone + // 3. Guild admin approves + // 4. Reputation is updated + + // Complete a significant milestone (significance 5) + update_reputation( + &env, + contributor.clone(), + ReputationEvent::MilestoneAchieved, + 5, + ); + + let profile = get_reputation(&env, contributor.clone()); + assert_eq!(profile.milestones_completed, 1); + assert_eq!(profile.score, 100); // 20 * 5 + + // Update leaderboard + update_guild_leaderboard(&env, guild_id, &contributor); + } + + /// Test combined bounty and milestone reputation flow + #[test] + fn test_combined_bounty_milestone_flow() { + let env = Env::default(); + env.budget().reset_unlimited(); + initialize_reputation_system(&env); + + let contributor = Address::generate(&env); + let guild_id = 1u64; + + // Complete 5 bounties + for i in 1..=5 { + update_reputation( + &env, + contributor.clone(), + ReputationEvent::TaskCompleted, + i, + ); + } + + // Complete 2 milestones + update_reputation( + &env, + contributor.clone(), + ReputationEvent::MilestoneAchieved, + 3, + ); + update_reputation( + &env, + contributor.clone(), + ReputationEvent::MilestoneAchieved, + 4, + ); + + let profile = get_reputation(&env, contributor.clone()); + + // Tasks: 10 + 20 + 30 + 40 + 50 = 150 + // Milestones: 60 + 80 = 140 + // Total: 290 + assert_eq!(profile.score, 290); + assert_eq!(profile.tasks_completed, 5); + assert_eq!(profile.milestones_completed, 2); + assert_eq!(get_tier(&env, contributor.clone()), ReputationTier::Silver); + + // Update leaderboard + update_guild_leaderboard(&env, guild_id, &contributor); + } + + /// Test dispute resolution impact on reputation + #[test] + fn test_dispute_resolution_flow() { + let env = Env::default(); + env.budget().reset_unlimited(); + initialize_reputation_system(&env); + + let contributor = Address::generate(&env); + + // Build up some reputation first + for _ in 0..5 { + update_reputation( + &env, + contributor.clone(), + ReputationEvent::TaskCompleted, + 3, + ); + } + + let profile_before = get_reputation(&env, contributor.clone()); + assert_eq!(profile_before.score, 150); + + // Simulate dispute - contributor wins + update_reputation(&env, contributor.clone(), ReputationEvent::DisputeWon, 0); + + let profile_after_win = get_reputation(&env, contributor.clone()); + assert_eq!(profile_after_win.score, 155); // +5 + assert_eq!(profile_after_win.disputes_won, 1); + + // Simulate another dispute - contributor loses + update_reputation(&env, contributor.clone(), ReputationEvent::DisputeLost, 0); + + let profile_after_loss = get_reputation(&env, contributor.clone()); + assert_eq!(profile_after_loss.score, 135); // -20 + assert_eq!(profile_after_loss.disputes_lost, 1); + } + + /// Test task failure impact on success rate and reputation + #[test] + fn test_task_failure_impact() { + let env = Env::default(); + env.budget().reset_unlimited(); + initialize_reputation_system(&env); + + let contributor = Address::generate(&env); + let guild_id = 1u64; + + // Complete 9 tasks successfully + for _ in 0..9 { + update_reputation( + &env, + contributor.clone(), + ReputationEvent::TaskCompleted, + 3, + ); + } + + let profile_before_failure = get_reputation(&env, contributor.clone()); + assert_eq!(profile_before_failure.tasks_completed, 9); + assert_eq!(profile_before_failure.success_rate, 100); + + // Fail 1 task + update_reputation(&env, contributor.clone(), ReputationEvent::TaskFailed, 0); + + let profile_after_failure = get_reputation(&env, contributor.clone()); + assert_eq!(profile_after_failure.tasks_completed, 9); + assert_eq!(profile_after_failure.tasks_failed, 1); + assert_eq!(profile_after_failure.success_rate, 90); // 9/10 = 90% + + // Update leaderboard + update_guild_leaderboard(&env, guild_id, &contributor); + } + + /// Test achievement auto-award based on task completion + #[test] + fn test_achievement_award_on_task_milestones() { + let env = Env::default(); + env.budget().reset_unlimited(); + initialize_reputation_system(&env); + + let contributor = Address::generate(&env); + + // Complete first task - eligible for "First Steps" (achievement ID 1) + update_reputation( + &env, + contributor.clone(), + ReputationEvent::TaskCompleted, + 1, + ); + + assert_eq!( + check_achievement_eligibility(&env, contributor.clone(), 1), + true + ); + + // Award the achievement + let awarded = award_achievement(&env, contributor.clone(), 1); + assert_eq!(awarded, true); + + let profile = get_reputation(&env, contributor.clone()); + assert_eq!(profile.achievements.len(), 1); + + // Continue to complete 9 more tasks (total 10) - eligible for "Task Veteran" (achievement ID 2) + for _ in 0..9 { + update_reputation( + &env, + contributor.clone(), + ReputationEvent::TaskCompleted, + 2, + ); + } + + assert_eq!( + check_achievement_eligibility(&env, contributor.clone(), 2), + true + ); + + // Award "Task Veteran" + let awarded = award_achievement(&env, contributor.clone(), 2); + assert_eq!(awarded, true); + + let profile = get_reputation(&env, contributor); + assert_eq!(profile.achievements.len(), 2); + } + + /// Test multi-guild leaderboard tracking + #[test] + fn test_multi_guild_leaderboard() { + let env = Env::default(); + env.budget().reset_unlimited(); + initialize_reputation_system(&env); + + let contributor1 = Address::generate(&env); + let contributor2 = Address::generate(&env); + let contributor3 = Address::generate(&env); + + let guild_1 = 1u64; + let guild_2 = 2u64; + + // Contributor 1 works in Guild 1 + update_reputation( + &env, + contributor1.clone(), + ReputationEvent::TaskCompleted, + 5, + ); // 50 points + update_guild_leaderboard(&env, guild_1, &contributor1); + + // Contributor 2 works in Guild 1 + update_reputation( + &env, + contributor2.clone(), + ReputationEvent::TaskCompleted, + 3, + ); // 30 points + update_guild_leaderboard(&env, guild_1, &contributor2); + + // Contributor 3 works in Guild 2 + update_reputation( + &env, + contributor3.clone(), + ReputationEvent::TaskCompleted, + 4, + ); // 40 points + update_guild_leaderboard(&env, guild_2, &contributor3); + + // Check Guild 1 leaderboard + let guild1_top = get_top_contributors_for_guild(&env, guild_1, 10); + assert_eq!(guild1_top.len(), 2); + + // Check Guild 2 leaderboard + let guild2_top = get_top_contributors_for_guild(&env, guild_2, 10); + assert_eq!(guild2_top.len(), 1); + assert_eq!(guild2_top.get(0).unwrap(), contributor3); + } + + /// Test reputation tier upgrade triggers during bounty completion + #[test] + fn test_tier_upgrade_during_bounty_completion() { + let env = Env::default(); + env.budget().reset_unlimited(); + initialize_reputation_system(&env); + + let contributor = Address::generate(&env); + + // Start at Bronze + assert_eq!(get_tier(&env, contributor.clone()), ReputationTier::Bronze); + + // Complete bounties to reach Silver (100 points) + for _ in 0..4 { + update_reputation( + &env, + contributor.clone(), + ReputationEvent::TaskCompleted, + 5, + ); // 50 each + } + + // Should now be Silver + assert_eq!(get_tier(&env, contributor.clone()), ReputationTier::Silver); + + // Complete more bounties to reach Gold (500 points) + for _ in 0..16 { + update_reputation( + &env, + contributor.clone(), + ReputationEvent::TaskCompleted, + 5, + ); + } + + // Should now be Gold + assert_eq!(get_tier(&env, contributor), ReputationTier::Gold); + } + + /// Test contributor progress tracking across multiple projects + #[test] + fn test_contributor_progress_tracking() { + let env = Env::default(); + env.budget().reset_unlimited(); + initialize_reputation_system(&env); + + let contributor = Address::generate(&env); + let guild_id = 1u64; + + // Project 1: 3 bounties + for i in 1..=3 { + update_reputation( + &env, + contributor.clone(), + ReputationEvent::TaskCompleted, + i, + ); + } + + // Project 2: 2 milestones + for i in 1..=2 { + update_reputation( + &env, + contributor.clone(), + ReputationEvent::MilestoneAchieved, + i, + ); + } + + // Project 3: 2 more bounties + for i in 1..=2 { + update_reputation( + &env, + contributor.clone(), + ReputationEvent::TaskCompleted, + i + 3, + ); + } + + let profile = get_reputation(&env, contributor.clone()); + + // Verify tracking + assert_eq!(profile.tasks_completed, 5); + assert_eq!(profile.milestones_completed, 2); + assert_eq!(profile.success_rate, 100); + + // Update leaderboard + update_guild_leaderboard(&env, guild_id, &contributor); + + // Verify appears on leaderboard + let top = get_top_contributors_for_guild(&env, guild_id, 10); + assert!(top.len() > 0); + } + + /// Test perfect record achievement eligibility + #[test] + fn test_perfect_record_achievement() { + let env = Env::default(); + env.budget().reset_unlimited(); + initialize_reputation_system(&env); + + let contributor = Address::generate(&env); + + // Complete 10 tasks with 100% success rate + for _ in 0..10 { + update_reputation( + &env, + contributor.clone(), + ReputationEvent::TaskCompleted, + 2, + ); + } + + let profile = get_reputation(&env, contributor.clone()); + assert_eq!(profile.tasks_completed, 10); + assert_eq!(profile.success_rate, 100); + + // Should be eligible for "Perfect Record" (achievement ID 4) + assert_eq!( + check_achievement_eligibility(&env, contributor, 4), + true + ); + } + + /// Test reputation consistency across multiple operations + #[test] + fn test_reputation_consistency() { + let env = Env::default(); + env.budget().reset_unlimited(); + initialize_reputation_system(&env); + + let contributor = Address::generate(&env); + let guild_id = 1u64; + + // Perform various operations + for _ in 0..5 { + update_reputation( + &env, + contributor.clone(), + ReputationEvent::TaskCompleted, + 3, + ); + update_guild_leaderboard(&env, guild_id, &contributor); + } + + let profile1 = get_reputation(&env, contributor.clone()); + + // Perform more operations + for _ in 0..3 { + update_reputation( + &env, + contributor.clone(), + ReputationEvent::MilestoneAchieved, + 2, + ); + update_guild_leaderboard(&env, guild_id, &contributor); + } + + let profile2 = get_reputation(&env, contributor.clone()); + + // Verify consistency + assert!(profile2.score > profile1.score); + assert_eq!(profile2.tasks_completed, profile1.tasks_completed); + assert_eq!(profile2.milestones_completed, 3); + } +} diff --git a/contract/src/reputation/mod.rs b/contract/src/reputation/mod.rs new file mode 100644 index 0000000..cee5667 --- /dev/null +++ b/contract/src/reputation/mod.rs @@ -0,0 +1,551 @@ +pub mod achievements; +pub mod scoring; +pub mod storage; +pub mod types; + +#[cfg(test)] +mod tests; + +#[cfg(test)] +mod integration_tests; + +use soroban_sdk::{Address, Env, Vec}; + +use self::achievements::{check_eligibility, get_contributor_achievements, has_achievement, initialize_default_achievements}; +use self::scoring::{ + apply_score_change, calculate_incentive_multiplier, calculate_score_change, + calculate_success_rate, calculate_tier, calculate_time_decay, +}; +use self::storage::{ + get_profile, get_top_contributors, has_profile, initialize_reputation_storage, set_profile, + update_leaderboard, get_achievement, +}; +use self::types::{ + Achievement, AchievementAwardedEvent, ProfileInitializedEvent, ReputationEvent, + ReputationProfile, ReputationTier, ReputationUpdatedEvent, TierUpgradedEvent, +}; + +// ============ Core Reputation Functions ============ + +/// Initialize a reputation profile for a contributor +/// +/// # Arguments +/// * `env` - The contract environment +/// * `address` - The address of the contributor +/// +/// # Returns +/// * `ReputationProfile` - The newly created profile +pub fn initialize_profile(env: &Env, address: Address) -> ReputationProfile { + // Check if profile already exists + if has_profile(env, &address) { + panic!("Profile already exists"); + } + + let current_time = env.ledger().timestamp(); + + let profile = ReputationProfile { + address: address.clone(), + score: 0, + tier: ReputationTier::Bronze, + tasks_completed: 0, + tasks_failed: 0, + success_rate: 100, // Default to 100% for new users + achievements: Vec::new(env), + last_activity: current_time, + created_at: current_time, + disputes_won: 0, + disputes_lost: 0, + milestones_completed: 0, + }; + + set_profile(env, &profile); + + // Emit event + env.events().publish( + (soroban_sdk::symbol_short!("REP_INIT"),), + ProfileInitializedEvent { + address: address.clone(), + initial_score: 0, + tier: ReputationTier::Bronze, + timestamp: current_time, + }, + ); + + profile +} + +/// Update reputation based on an event +/// +/// # Arguments +/// * `env` - The contract environment +/// * `address` - The address of the contributor +/// * `event` - The type of reputation event +/// * `value` - Context-specific value (complexity, significance, etc.) +/// +/// # Returns +/// * `u32` - The new reputation score +pub fn update_reputation( + env: &Env, + address: Address, + event: ReputationEvent, + value: u32, +) -> u32 { + // Get or create profile + let mut profile = match get_profile(env, &address) { + Some(p) => p, + None => initialize_profile(env, address.clone()), + }; + + let old_score = profile.score; + let old_tier = profile.tier; + + // Update activity tracking based on event + match event { + ReputationEvent::TaskCompleted => { + profile.tasks_completed += 1; + } + ReputationEvent::TaskFailed => { + profile.tasks_failed += 1; + } + ReputationEvent::DisputeWon => { + profile.disputes_won += 1; + } + ReputationEvent::DisputeLost => { + profile.disputes_lost += 1; + } + ReputationEvent::MilestoneAchieved => { + profile.milestones_completed += 1; + } + _ => {} + } + + // Recalculate success rate + profile.success_rate = calculate_success_rate(profile.tasks_completed, profile.tasks_failed); + + // Calculate score change + let score_change = calculate_score_change(event, value, profile.score); + + // Apply score change (with bounds checking) + profile.score = apply_score_change(profile.score, score_change); + + // Recalculate tier + profile.tier = calculate_tier(profile.score); + + // Update last activity + profile.last_activity = env.ledger().timestamp(); + + // Save updated profile + set_profile(env, &profile); + + // Emit reputation updated event + env.events().publish( + (soroban_sdk::symbol_short!("REP_UPD"),), + ReputationUpdatedEvent { + address: address.clone(), + event_type: event, + old_score, + new_score: profile.score, + old_tier, + new_tier: profile.tier, + timestamp: env.ledger().timestamp(), + }, + ); + + // Emit tier upgrade event if tier changed + if old_tier != profile.tier { + env.events().publish( + (soroban_sdk::symbol_short!("TIER_UP"),), + TierUpgradedEvent { + address, + old_tier, + new_tier: profile.tier, + current_score: profile.score, + timestamp: env.ledger().timestamp(), + }, + ); + } + + profile.score +} + +/// Award an achievement to a contributor +/// +/// # Arguments +/// * `env` - The contract environment +/// * `address` - The address of the contributor +/// * `achievement_id` - The ID of the achievement to award +/// +/// # Returns +/// * `bool` - true if the achievement was awarded successfully +pub fn award_achievement(env: &Env, address: Address, achievement_id: u64) -> bool { + // Get profile + let mut profile = match get_profile(env, &address) { + Some(p) => p, + None => return false, + }; + + // Check if already has achievement + if has_achievement(&profile, achievement_id) { + return false; + } + + // Check eligibility + if !check_eligibility(env, &profile, achievement_id) { + return false; + } + + // Get achievement details + let achievement = match get_achievement(env, achievement_id) { + Some(ach) => ach, + None => return false, + }; + + // Add achievement to profile + profile.achievements.push_back(achievement_id); + + // Add achievement points to reputation score + let old_score = profile.score; + profile.score = profile.score.saturating_add(achievement.points); + + // Recalculate tier + let old_tier = profile.tier; + profile.tier = calculate_tier(profile.score); + + // Save updated profile + set_profile(env, &profile); + + // Emit achievement awarded event + env.events().publish( + (soroban_sdk::symbol_short!("ACH_AWD"),), + AchievementAwardedEvent { + address: address.clone(), + achievement_id, + achievement_name: achievement.name.clone(), + points_awarded: achievement.points, + timestamp: env.ledger().timestamp(), + }, + ); + + // Emit tier upgrade event if tier changed + if old_tier != profile.tier { + env.events().publish( + (soroban_sdk::symbol_short!("TIER_UP"),), + TierUpgradedEvent { + address, + old_tier, + new_tier: profile.tier, + current_score: profile.score, + timestamp: env.ledger().timestamp(), + }, + ); + } + + true +} + +/// Get the reputation profile for a contributor +/// +/// # Arguments +/// * `env` - The contract environment +/// * `address` - The address of the contributor +/// +/// # Returns +/// * `ReputationProfile` - The contributor's reputation profile +pub fn get_reputation(env: &Env, address: Address) -> ReputationProfile { + match get_profile(env, &address) { + Some(profile) => profile, + None => panic!("Profile not found"), + } +} + +/// Get the reputation tier for a contributor +/// +/// # Arguments +/// * `env` - The contract environment +/// * `address` - The address of the contributor +/// +/// # Returns +/// * `ReputationTier` - The contributor's current tier +pub fn get_tier(env: &Env, address: Address) -> ReputationTier { + match get_profile(env, &address) { + Some(profile) => profile.tier, + None => ReputationTier::Bronze, + } +} + +/// Calculate the incentive multiplier for a contributor +/// +/// # Arguments +/// * `env` - The contract environment +/// * `address` - The address of the contributor +/// +/// # Returns +/// * `u32` - The incentive multiplier in basis points (100 = 1.0x) +pub fn calculate_multiplier(env: &Env, address: Address) -> u32 { + let tier = get_tier(env, address); + calculate_incentive_multiplier(tier) +} + +/// Get top contributors for a guild +/// +/// # Arguments +/// * `env` - The contract environment +/// * `guild_id` - The ID of the guild +/// * `limit` - Maximum number of contributors to return +/// +/// # Returns +/// * `Vec
` - List of top contributor addresses +pub fn get_top_contributors_for_guild(env: &Env, guild_id: u64, limit: u32) -> Vec
{ + get_top_contributors(env, guild_id, limit) +} + +/// Check if a contributor is eligible for a specific achievement +/// +/// # Arguments +/// * `env` - The contract environment +/// * `address` - The address of the contributor +/// * `achievement_id` - The ID of the achievement +/// +/// # Returns +/// * `bool` - true if the contributor is eligible +pub fn check_achievement_eligibility(env: &Env, address: Address, achievement_id: u64) -> bool { + match get_profile(env, &address) { + Some(profile) => check_eligibility(env, &profile, achievement_id), + None => false, + } +} + +/// Get all achievements earned by a contributor +/// +/// # Arguments +/// * `env` - The contract environment +/// * `address` - The address of the contributor +/// +/// # Returns +/// * `Vec` - List of earned achievements +pub fn get_achievements(env: &Env, address: Address) -> Vec { + match get_profile(env, &address) { + Some(profile) => get_contributor_achievements(env, &profile), + None => Vec::new(env), + } +} + +/// Update guild leaderboard for a contributor +/// +/// # Arguments +/// * `env` - The contract environment +/// * `guild_id` - The ID of the guild +/// * `address` - The address of the contributor +pub fn update_guild_leaderboard(env: &Env, guild_id: u64, address: &Address) { + if let Some(profile) = get_profile(env, address) { + update_leaderboard(env, guild_id, address, profile.score); + } +} + +/// Apply time decay to a contributor's reputation +/// +/// # Arguments +/// * `env` - The contract environment +/// * `address` - The address of the contributor +/// +/// # Returns +/// * `u32` - The new reputation score after decay +pub fn apply_time_decay(env: &Env, address: Address) -> u32 { + let mut profile = match get_profile(env, &address) { + Some(p) => p, + None => return 0, + }; + + let old_score = profile.score; + let decay_amount = calculate_time_decay(env, profile.last_activity, profile.score); + + if decay_amount > 0 { + let old_tier = profile.tier; + profile.score = if decay_amount > profile.score { + 0 + } else { + profile.score - decay_amount + }; + + profile.tier = calculate_tier(profile.score); + + set_profile(env, &profile); + + // Emit reputation updated event + env.events().publish( + (soroban_sdk::symbol_short!("REP_UPD"),), + ReputationUpdatedEvent { + address: address.clone(), + event_type: ReputationEvent::TimeDecay, + old_score, + new_score: profile.score, + old_tier, + new_tier: profile.tier, + timestamp: env.ledger().timestamp(), + }, + ); + } + + profile.score +} + +/// Initialize reputation system (call once during contract deployment) +pub fn initialize_reputation_system(env: &Env) { + initialize_reputation_storage(env); + initialize_default_achievements(env); +} + +#[cfg(test)] +mod tests { + use super::*; + use soroban_sdk::testutils::Address as _; + + #[test] + fn test_initialize_profile() { + let env = Env::default(); + env.budget().reset_unlimited(); + + initialize_reputation_system(&env); + + let address = Address::generate(&env); + let profile = initialize_profile(&env, address.clone()); + + assert_eq!(profile.address, address); + assert_eq!(profile.score, 0); + assert_eq!(profile.tier, ReputationTier::Bronze); + assert_eq!(profile.tasks_completed, 0); + assert_eq!(profile.tasks_failed, 0); + assert_eq!(profile.success_rate, 100); + } + + #[test] + #[should_panic(expected = "Profile already exists")] + fn test_initialize_profile_duplicate() { + let env = Env::default(); + env.budget().reset_unlimited(); + + initialize_reputation_system(&env); + + let address = Address::generate(&env); + initialize_profile(&env, address.clone()); + initialize_profile(&env, address); // Should panic + } + + #[test] + fn test_update_reputation_task_completed() { + let env = Env::default(); + env.budget().reset_unlimited(); + + initialize_reputation_system(&env); + + let address = Address::generate(&env); + + // Complete a task with complexity 3 + let new_score = update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 3); + + assert_eq!(new_score, 30); // 10 * 3 + + let profile = get_reputation(&env, address); + assert_eq!(profile.tasks_completed, 1); + assert_eq!(profile.success_rate, 100); + } + + #[test] + fn test_reputation_tier_progression() { + let env = Env::default(); + env.budget().reset_unlimited(); + + initialize_reputation_system(&env); + + let address = Address::generate(&env); + + // Start at Bronze + assert_eq!(get_tier(&env, address.clone()), ReputationTier::Bronze); + + // Complete tasks to reach Silver (100 points) + for _ in 0..4 { + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 5); + } + assert_eq!(get_tier(&env, address.clone()), ReputationTier::Silver); + + // Complete tasks to reach Gold (500 points) + for _ in 0..17 { + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 5); + } + assert_eq!(get_tier(&env, address.clone()), ReputationTier::Gold); + } + + #[test] + fn test_success_rate_calculation() { + let env = Env::default(); + env.budget().reset_unlimited(); + + initialize_reputation_system(&env); + + let address = Address::generate(&env); + + // Complete 9 tasks + for _ in 0..9 { + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 1); + } + + // Fail 1 task + update_reputation(&env, address.clone(), ReputationEvent::TaskFailed, 0); + + let profile = get_reputation(&env, address); + assert_eq!(profile.tasks_completed, 9); + assert_eq!(profile.tasks_failed, 1); + assert_eq!(profile.success_rate, 90); // 9/10 = 90% + } + + #[test] + fn test_incentive_multipliers() { + let env = Env::default(); + env.budget().reset_unlimited(); + + initialize_reputation_system(&env); + + let address = Address::generate(&env); + + // Bronze: 1.0x + assert_eq!(calculate_multiplier(&env, address.clone()), 100); + + // Reach Silver (100 points): 1.1x + for _ in 0..4 { + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 5); + } + assert_eq!(calculate_multiplier(&env, address.clone()), 110); + + // Reach Gold (500 points): 1.25x + for _ in 0..17 { + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 5); + } + assert_eq!(calculate_multiplier(&env, address.clone()), 125); + } + + #[test] + fn test_negative_reputation_bounded_at_zero() { + let env = Env::default(); + env.budget().reset_unlimited(); + + initialize_reputation_system(&env); + + let address = Address::generate(&env); + + // Start with 50 points + for _ in 0..2 { + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 5); + } + + let profile = get_reputation(&env, address.clone()); + assert_eq!(profile.score, 50); + + // Fail tasks multiple times + for _ in 0..10 { + update_reputation(&env, address.clone(), ReputationEvent::TaskFailed, 0); + } + + let profile = get_reputation(&env, address); + assert_eq!(profile.score, 0); // Should not go below 0 + } +} diff --git a/contract/src/reputation/scoring.rs b/contract/src/reputation/scoring.rs new file mode 100644 index 0000000..2a1c62b --- /dev/null +++ b/contract/src/reputation/scoring.rs @@ -0,0 +1,254 @@ +use soroban_sdk::Env; + +use super::types::{ReputationEvent, ReputationTier}; + +// ============ Reputation Scoring Rules ============ + +/// Calculate score change based on event type and value +/// +/// # Scoring Rules: +/// - Task completion: +10 to +50 (based on complexity) +/// - Milestone completion: +20 to +100 +/// - Dispute won: +5 +/// - Dispute lost: -20 +/// - Task failed/cancelled: -10 +/// - Time decay: -1% per month of inactivity +/// - Success rate bonus: up to +50% for >95% success +pub fn calculate_score_change( + event: ReputationEvent, + value: u32, + _current_score: u32, +) -> i32 { + match event { + ReputationEvent::TaskCompleted => { + // value represents complexity (1-5) + let base_points = 10u32; + let complexity_multiplier = if value > 0 && value <= 5 { value } else { 1 }; + (base_points * complexity_multiplier) as i32 + } + ReputationEvent::MilestoneAchieved => { + // value represents milestone significance (1-5) + let base_points = 20u32; + let significance_multiplier = if value > 0 && value <= 5 { value } else { 1 }; + (base_points * significance_multiplier) as i32 + } + ReputationEvent::DisputeWon => 5i32, + ReputationEvent::DisputeLost => -20i32, + ReputationEvent::TaskFailed => -10i32, + ReputationEvent::TimeDecay => { + // value represents months of inactivity + // -1% per month, but minimum -50% + let decay_percentage = if value > 50 { 50 } else { value }; + -(decay_percentage as i32) + } + ReputationEvent::SuccessRateBonus => { + // value represents success rate (0-100) + // Bonus for >95% success rate + if value >= 95 { + 50i32 // +50 points bonus + } else { + 0i32 + } + } + } +} + +/// Calculate reputation tier based on score +/// +/// # Tier Thresholds: +/// - Bronze: 0-99 points +/// - Silver: 100-499 points +/// - Gold: 500-1499 points +/// - Platinum: 1500-4999 points +/// - Diamond: 5000+ points +pub fn calculate_tier(score: u32) -> ReputationTier { + if score >= 5000 { + ReputationTier::Diamond + } else if score >= 1500 { + ReputationTier::Platinum + } else if score >= 500 { + ReputationTier::Gold + } else if score >= 100 { + ReputationTier::Silver + } else { + ReputationTier::Bronze + } +} + +/// Calculate incentive multiplier based on reputation tier +/// +/// # Multipliers (basis points, 100 = 1.0x): +/// - Bronze: 100 (1.0x) +/// - Silver: 110 (1.1x) +/// - Gold: 125 (1.25x) +/// - Platinum: 150 (1.5x) +/// - Diamond: 200 (2.0x) +pub fn calculate_incentive_multiplier(tier: ReputationTier) -> u32 { + match tier { + ReputationTier::Bronze => 100, + ReputationTier::Silver => 110, + ReputationTier::Gold => 125, + ReputationTier::Platinum => 150, + ReputationTier::Diamond => 200, + } +} + +/// Calculate success rate percentage +pub fn calculate_success_rate(tasks_completed: u32, tasks_failed: u32) -> u32 { + let total_tasks = tasks_completed + tasks_failed; + if total_tasks == 0 { + 100 // Default to 100% for new contributors + } else { + // Calculate percentage: (completed / total) * 100 + (tasks_completed * 100) / total_tasks + } +} + +/// Calculate time decay based on months of inactivity +/// +/// Returns the amount to subtract from score (always positive) +pub fn calculate_time_decay(env: &Env, last_activity: u64, current_score: u32) -> u32 { + let current_time = env.ledger().timestamp(); + + // Calculate months of inactivity + let seconds_inactive = if current_time > last_activity { + current_time - last_activity + } else { + 0 + }; + + let seconds_per_month: u64 = 30 * 24 * 60 * 60; // 30 days + let months_inactive = seconds_inactive / seconds_per_month; + + // -1% per month, capped at 50% + let decay_percentage = if months_inactive > 50 { + 50u32 + } else { + months_inactive as u32 + }; + + // Calculate decay amount + let decay_amount = (current_score * decay_percentage) / 100; + + decay_amount +} + +/// Apply score change with bounds checking (score cannot go below 0) +pub fn apply_score_change(current_score: u32, change: i32) -> u32 { + if change < 0 { + let decrease = (-change) as u32; + if decrease > current_score { + 0 // Can't go below 0 + } else { + current_score - decrease + } + } else { + current_score.saturating_add(change as u32) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_calculate_tier() { + assert_eq!(calculate_tier(0), ReputationTier::Bronze); + assert_eq!(calculate_tier(99), ReputationTier::Bronze); + assert_eq!(calculate_tier(100), ReputationTier::Silver); + assert_eq!(calculate_tier(499), ReputationTier::Silver); + assert_eq!(calculate_tier(500), ReputationTier::Gold); + assert_eq!(calculate_tier(1499), ReputationTier::Gold); + assert_eq!(calculate_tier(1500), ReputationTier::Platinum); + assert_eq!(calculate_tier(4999), ReputationTier::Platinum); + assert_eq!(calculate_tier(5000), ReputationTier::Diamond); + assert_eq!(calculate_tier(10000), ReputationTier::Diamond); + } + + #[test] + fn test_calculate_incentive_multiplier() { + assert_eq!( + calculate_incentive_multiplier(ReputationTier::Bronze), + 100 + ); + assert_eq!( + calculate_incentive_multiplier(ReputationTier::Silver), + 110 + ); + assert_eq!(calculate_incentive_multiplier(ReputationTier::Gold), 125); + assert_eq!( + calculate_incentive_multiplier(ReputationTier::Platinum), + 150 + ); + assert_eq!( + calculate_incentive_multiplier(ReputationTier::Diamond), + 200 + ); + } + + #[test] + fn test_calculate_success_rate() { + assert_eq!(calculate_success_rate(0, 0), 100); // New contributor + assert_eq!(calculate_success_rate(10, 0), 100); // Perfect record + assert_eq!(calculate_success_rate(50, 50), 50); // 50% success + assert_eq!(calculate_success_rate(95, 5), 95); // 95% success + assert_eq!(calculate_success_rate(1, 9), 10); // 10% success + } + + #[test] + fn test_apply_score_change() { + assert_eq!(apply_score_change(100, 50), 150); // Add points + assert_eq!(apply_score_change(100, -30), 70); // Subtract points + assert_eq!(apply_score_change(20, -50), 0); // Can't go below 0 + assert_eq!(apply_score_change(0, -10), 0); // Already at 0 + assert_eq!(apply_score_change(u32::MAX - 10, 20), u32::MAX); // Saturating add + } + + #[test] + fn test_score_change_calculations() { + // Task completed with various complexities + assert_eq!( + calculate_score_change(ReputationEvent::TaskCompleted, 1, 0), + 10 + ); + assert_eq!( + calculate_score_change(ReputationEvent::TaskCompleted, 3, 0), + 30 + ); + assert_eq!( + calculate_score_change(ReputationEvent::TaskCompleted, 5, 0), + 50 + ); + + // Milestone with various significance + assert_eq!( + calculate_score_change(ReputationEvent::MilestoneAchieved, 1, 0), + 20 + ); + assert_eq!( + calculate_score_change(ReputationEvent::MilestoneAchieved, 5, 0), + 100 + ); + + // Fixed events + assert_eq!(calculate_score_change(ReputationEvent::DisputeWon, 0, 0), 5); + assert_eq!( + calculate_score_change(ReputationEvent::DisputeLost, 0, 0), + -20 + ); + assert_eq!( + calculate_score_change(ReputationEvent::TaskFailed, 0, 0), + -10 + ); + + // Success rate bonus + assert_eq!( + calculate_score_change(ReputationEvent::SuccessRateBonus, 95, 0), + 50 + ); + assert_eq!( + calculate_score_change(ReputationEvent::SuccessRateBonus, 94, 0), + 0 + ); + } +} diff --git a/contract/src/reputation/storage.rs b/contract/src/reputation/storage.rs new file mode 100644 index 0000000..bb4f324 --- /dev/null +++ b/contract/src/reputation/storage.rs @@ -0,0 +1,158 @@ +use soroban_sdk::{symbol_short, Address, Env, Map, Vec}; + +use super::types::{Achievement, ReputationProfile}; + +// ============ Storage Keys ============ + +/// Key for reputation profile counter (generates new profile IDs) +const PROFILE_COUNTER: &str = "REP_CNT"; + +/// Key prefix for reputation profiles (maps Address -> ReputationProfile) +const PROFILE_PREFIX: &str = "REP_PROF"; + +/// Key for achievement counter (generates new achievement IDs) +const ACHIEVEMENT_COUNTER: &str = "ACH_CNT"; + +/// Key prefix for achievements (maps achievement_id -> Achievement) +const ACHIEVEMENT_PREFIX: &str = "ACH"; + +/// Key for guild leaderboard (maps guild_id -> Vec
) +const GUILD_LEADERBOARD_PREFIX: &str = "LEAD"; + +// ============ Storage Functions ============ + +/// Initialize storage counters +pub fn initialize_reputation_storage(env: &Env) { + env.storage() + .instance() + .set(&symbol_short!("REP_CNT"), &0u64); + env.storage() + .instance() + .set(&symbol_short!("ACH_CNT"), &0u64); +} + +/// Get or create reputation profile for an address +pub fn get_profile(env: &Env, address: &Address) -> Option { + let key = (symbol_short!("REP_PROF"), address.clone()); + env.storage().persistent().get(&key) +} + +/// Save reputation profile +pub fn set_profile(env: &Env, profile: &ReputationProfile) { + let key = (symbol_short!("REP_PROF"), profile.address.clone()); + env.storage().persistent().set(&key, profile); +} + +/// Check if profile exists +pub fn has_profile(env: &Env, address: &Address) -> bool { + let key = (symbol_short!("REP_PROF"), address.clone()); + env.storage().persistent().has(&key) +} + +/// Get achievement by ID +pub fn get_achievement(env: &Env, achievement_id: u64) -> Option { + let key = (symbol_short!("ACH"), achievement_id); + env.storage().persistent().get(&key) +} + +/// Save achievement +pub fn set_achievement(env: &Env, achievement: &Achievement) { + let key = (symbol_short!("ACH"), achievement.id); + env.storage().persistent().set(&key, achievement); +} + +/// Get next achievement ID and increment counter +pub fn get_next_achievement_id(env: &Env) -> u64 { + let current: u64 = env + .storage() + .instance() + .get(&symbol_short!("ACH_CNT")) + .unwrap_or(0); + let next = current + 1; + env.storage() + .instance() + .set(&symbol_short!("ACH_CNT"), &next); + next +} + +/// Get all achievements +pub fn get_all_achievements(env: &Env) -> Vec { + let count: u64 = env + .storage() + .instance() + .get(&symbol_short!("ACH_CNT")) + .unwrap_or(0); + + let mut achievements = Vec::new(env); + for id in 1..=count { + if let Some(achievement) = get_achievement(env, id) { + achievements.push_back(achievement); + } + } + achievements +} + +/// Add address to guild leaderboard (sorted by score) +pub fn update_leaderboard(env: &Env, guild_id: u64, address: &Address, score: u32) { + let key = (symbol_short!("LEAD"), guild_id); + + // Get existing leaderboard or create new + let mut leaderboard: Map = env + .storage() + .persistent() + .get(&key) + .unwrap_or(Map::new(env)); + + // Update score for this address + leaderboard.set(address.clone(), score); + + // Save updated leaderboard + env.storage().persistent().set(&key, &leaderboard); +} + +/// Get top contributors for a guild +pub fn get_top_contributors(env: &Env, guild_id: u64, limit: u32) -> Vec
{ + let key = (symbol_short!("LEAD"), guild_id); + + let leaderboard: Map = env + .storage() + .persistent() + .get(&key) + .unwrap_or(Map::new(env)); + + // Convert to vec for sorting + let mut entries: Vec<(Address, u32)> = Vec::new(env); + let keys = leaderboard.keys(); + + for i in 0..keys.len() { + let addr = keys.get(i).unwrap(); + let score = leaderboard.get(addr.clone()).unwrap(); + entries.push_back((addr, score)); + } + + // Sort by score (descending) - bubble sort for simplicity + let len = entries.len(); + for i in 0..len { + for j in 0..(len - i - 1) { + let curr = entries.get(j).unwrap(); + let next = entries.get(j + 1).unwrap(); + if curr.1 < next.1 { + // Swap + let temp = curr.clone(); + entries.set(j, next); + entries.set(j + 1, temp); + } + } + } + + // Return top N addresses + let mut result = Vec::new(env); + let max_results = if limit < entries.len() { limit } else { entries.len() }; + + for i in 0..max_results { + let entry = entries.get(i).unwrap(); + result.push_back(entry.0); + } + + result +} diff --git a/contract/src/reputation/tests.rs b/contract/src/reputation/tests.rs new file mode 100644 index 0000000..d87fe12 --- /dev/null +++ b/contract/src/reputation/tests.rs @@ -0,0 +1,551 @@ +#[cfg(test)] +mod reputation_tests { + use crate::reputation::types::{ReputationEvent, ReputationTier}; + use crate::reputation::{ + award_achievement, check_achievement_eligibility, get_achievements, get_reputation, + get_tier, get_top_contributors_for_guild, initialize_profile, initialize_reputation_system, + update_guild_leaderboard, update_reputation, calculate_multiplier, apply_time_decay, + }; + use soroban_sdk::testutils::Address as _; + use soroban_sdk::{Address, Env}; + + fn setup() -> Env { + let env = Env::default(); + env.budget().reset_unlimited(); + initialize_reputation_system(&env); + env + } + + // ============ Profile Initialization Tests ============ + + #[test] + fn test_initialize_new_profile() { + let env = setup(); + let address = Address::generate(&env); + + let profile = initialize_profile(&env, address.clone()); + + assert_eq!(profile.address, address); + assert_eq!(profile.score, 0); + assert_eq!(profile.tier, ReputationTier::Bronze); + assert_eq!(profile.tasks_completed, 0); + assert_eq!(profile.tasks_failed, 0); + assert_eq!(profile.success_rate, 100); + assert_eq!(profile.achievements.len(), 0); + assert_eq!(profile.disputes_won, 0); + assert_eq!(profile.disputes_lost, 0); + assert_eq!(profile.milestones_completed, 0); + } + + #[test] + #[should_panic(expected = "Profile already exists")] + fn test_initialize_duplicate_profile() { + let env = setup(); + let address = Address::generate(&env); + + initialize_profile(&env, address.clone()); + initialize_profile(&env, address); // Should panic + } + + // ============ Task Completion Tests ============ + + #[test] + fn test_task_completion_simple() { + let env = setup(); + let address = Address::generate(&env); + + // Complete a simple task (complexity 1) + let score = update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 1); + assert_eq!(score, 10); + + let profile = get_reputation(&env, address); + assert_eq!(profile.tasks_completed, 1); + assert_eq!(profile.success_rate, 100); + } + + #[test] + fn test_task_completion_complex() { + let env = setup(); + let address = Address::generate(&env); + + // Complete a complex task (complexity 5) + let score = update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 5); + assert_eq!(score, 50); // 10 * 5 + + let profile = get_reputation(&env, address); + assert_eq!(profile.tasks_completed, 1); + } + + #[test] + fn test_multiple_task_completions() { + let env = setup(); + let address = Address::generate(&env); + + // Complete 5 tasks with varying complexity + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 1); // +10 + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 2); // +20 + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 3); // +30 + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 4); // +40 + let final_score = update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 5); // +50 + + assert_eq!(final_score, 150); // Total: 10+20+30+40+50 + + let profile = get_reputation(&env, address); + assert_eq!(profile.tasks_completed, 5); + assert_eq!(profile.tier, ReputationTier::Silver); // Should be Silver tier + } + + // ============ Task Failure Tests ============ + + #[test] + fn test_task_failure() { + let env = setup(); + let address = Address::generate(&env); + + // Complete a task first + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 3); // +30 + + // Fail a task + let score = update_reputation(&env, address.clone(), ReputationEvent::TaskFailed, 0); // -10 + assert_eq!(score, 20); + + let profile = get_reputation(&env, address); + assert_eq!(profile.tasks_completed, 1); + assert_eq!(profile.tasks_failed, 1); + assert_eq!(profile.success_rate, 50); // 1 success / 2 total = 50% + } + + #[test] + fn test_reputation_cannot_go_below_zero() { + let env = setup(); + let address = Address::generate(&env); + + // Start with 30 points + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 3); + + // Fail multiple tasks to try to go negative + update_reputation(&env, address.clone(), ReputationEvent::TaskFailed, 0); // -10 + update_reputation(&env, address.clone(), ReputationEvent::TaskFailed, 0); // -10 + update_reputation(&env, address.clone(), ReputationEvent::TaskFailed, 0); // -10 + let final_score = update_reputation(&env, address.clone(), ReputationEvent::TaskFailed, 0); // -10 + + assert_eq!(final_score, 0); // Should stop at 0, not go negative + + let profile = get_reputation(&env, address); + assert_eq!(profile.score, 0); + } + + // ============ Tier Progression Tests ============ + + #[test] + fn test_tier_progression_bronze_to_silver() { + let env = setup(); + let address = Address::generate(&env); + + // Start at Bronze + assert_eq!(get_tier(&env, address.clone()), ReputationTier::Bronze); + + // Reach Silver (100 points) + for _ in 0..4 { + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 5); // +50 each + } + + assert_eq!(get_tier(&env, address), ReputationTier::Silver); + } + + #[test] + fn test_tier_progression_to_gold() { + let env = setup(); + let address = Address::generate(&env); + + // Reach Gold (500 points) + for _ in 0..10 { + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 5); // +50 each + } + + assert_eq!(get_tier(&env, address.clone()), ReputationTier::Gold); + + let profile = get_reputation(&env, address); + assert_eq!(profile.score, 500); + } + + #[test] + fn test_tier_progression_to_platinum() { + let env = setup(); + let address = Address::generate(&env); + + // Reach Platinum (1500 points) + for _ in 0..30 { + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 5); // +50 each + } + + assert_eq!(get_tier(&env, address.clone()), ReputationTier::Platinum); + + let profile = get_reputation(&env, address); + assert_eq!(profile.score, 1500); + } + + #[test] + fn test_tier_progression_to_diamond() { + let env = setup(); + let address = Address::generate(&env); + + // Reach Diamond (5000 points) + for _ in 0..100 { + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 5); // +50 each + } + + assert_eq!(get_tier(&env, address.clone()), ReputationTier::Diamond); + + let profile = get_reputation(&env, address); + assert_eq!(profile.score, 5000); + } + + // ============ Milestone Tests ============ + + #[test] + fn test_milestone_achievement() { + let env = setup(); + let address = Address::generate(&env); + + // Complete milestone with significance 3 + let score = update_reputation(&env, address.clone(), ReputationEvent::MilestoneAchieved, 3); + assert_eq!(score, 60); // 20 * 3 + + let profile = get_reputation(&env, address); + assert_eq!(profile.milestones_completed, 1); + } + + #[test] + fn test_multiple_milestones() { + let env = setup(); + let address = Address::generate(&env); + + // Complete 5 milestones + for i in 1..=5 { + update_reputation(&env, address.clone(), ReputationEvent::MilestoneAchieved, i); + } + + let profile = get_reputation(&env, address); + assert_eq!(profile.milestones_completed, 5); + // Score: 20 + 40 + 60 + 80 + 100 = 300 + assert_eq!(profile.score, 300); + } + + // ============ Dispute Tests ============ + + #[test] + fn test_dispute_won() { + let env = setup(); + let address = Address::generate(&env); + + let score = update_reputation(&env, address.clone(), ReputationEvent::DisputeWon, 0); + assert_eq!(score, 5); + + let profile = get_reputation(&env, address); + assert_eq!(profile.disputes_won, 1); + } + + #[test] + fn test_dispute_lost() { + let env = setup(); + let address = Address::generate(&env); + + // Earn some points first + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 5); // +50 + + // Lose a dispute + let score = update_reputation(&env, address.clone(), ReputationEvent::DisputeLost, 0); + assert_eq!(score, 30); // 50 - 20 + + let profile = get_reputation(&env, address); + assert_eq!(profile.disputes_lost, 1); + } + + // ============ Success Rate Tests ============ + + #[test] + fn test_success_rate_perfect() { + let env = setup(); + let address = Address::generate(&env); + + // Complete 10 tasks with no failures + for _ in 0..10 { + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 1); + } + + let profile = get_reputation(&env, address); + assert_eq!(profile.success_rate, 100); + } + + #[test] + fn test_success_rate_mixed() { + let env = setup(); + let address = Address::generate(&env); + + // Complete 8 tasks + for _ in 0..8 { + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 1); + } + + // Fail 2 tasks + for _ in 0..2 { + update_reputation(&env, address.clone(), ReputationEvent::TaskFailed, 0); + } + + let profile = get_reputation(&env, address); + assert_eq!(profile.success_rate, 80); // 8/10 = 80% + } + + // ============ Incentive Multiplier Tests ============ + + #[test] + fn test_incentive_multiplier_bronze() { + let env = setup(); + let address = Address::generate(&env); + + initialize_profile(&env, address.clone()); + + let multiplier = calculate_multiplier(&env, address); + assert_eq!(multiplier, 100); // 1.0x + } + + #[test] + fn test_incentive_multiplier_silver() { + let env = setup(); + let address = Address::generate(&env); + + // Reach Silver tier (100 points) + for _ in 0..4 { + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 5); + } + + let multiplier = calculate_multiplier(&env, address); + assert_eq!(multiplier, 110); // 1.1x + } + + #[test] + fn test_incentive_multiplier_gold() { + let env = setup(); + let address = Address::generate(&env); + + // Reach Gold tier (500 points) + for _ in 0..10 { + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 5); + } + + let multiplier = calculate_multiplier(&env, address); + assert_eq!(multiplier, 125); // 1.25x + } + + #[test] + fn test_incentive_multiplier_platinum() { + let env = setup(); + let address = Address::generate(&env); + + // Reach Platinum tier (1500 points) + for _ in 0..30 { + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 5); + } + + let multiplier = calculate_multiplier(&env, address); + assert_eq!(multiplier, 150); // 1.5x + } + + #[test] + fn test_incentive_multiplier_diamond() { + let env = setup(); + let address = Address::generate(&env); + + // Reach Diamond tier (5000 points) + for _ in 0..100 { + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 5); + } + + let multiplier = calculate_multiplier(&env, address); + assert_eq!(multiplier, 200); // 2.0x + } + + // ============ Achievement Tests ============ + + #[test] + fn test_award_first_task_achievement() { + let env = setup(); + let address = Address::generate(&env); + + // Complete first task + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 1); + + // Award "First Steps" achievement (ID 1) + let awarded = award_achievement(&env, address.clone(), 1); + assert_eq!(awarded, true); + + let achievements = get_achievements(&env, address.clone()); + assert_eq!(achievements.len(), 1); + + let profile = get_reputation(&env, address); + assert_eq!(profile.achievements.len(), 1); + } + + #[test] + fn test_cannot_award_same_achievement_twice() { + let env = setup(); + let address = Address::generate(&env); + + // Complete first task + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 1); + + // Award achievement + award_achievement(&env, address.clone(), 1); + + // Try to award again + let awarded = award_achievement(&env, address.clone(), 1); + assert_eq!(awarded, false); + } + + #[test] + fn test_check_achievement_eligibility() { + let env = setup(); + let address = Address::generate(&env); + + // Not eligible initially + assert_eq!(check_achievement_eligibility(&env, address.clone(), 1), false); + + // Complete first task + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 1); + + // Now eligible for "First Steps" + assert_eq!(check_achievement_eligibility(&env, address, 1), true); + } + + #[test] + fn test_achievement_adds_reputation_points() { + let env = setup(); + let address = Address::generate(&env); + + // Complete first task (10 points) + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 1); + + let profile_before = get_reputation(&env, address.clone()); + let score_before = profile_before.score; + + // Award achievement (adds 10 points for "First Steps") + award_achievement(&env, address.clone(), 1); + + let profile_after = get_reputation(&env, address); + assert_eq!(profile_after.score, score_before + 10); + } + + // ============ Leaderboard Tests ============ + + #[test] + fn test_update_leaderboard() { + let env = setup(); + let guild_id = 1u64; + let address1 = Address::generate(&env); + let address2 = Address::generate(&env); + let address3 = Address::generate(&env); + + // Give contributors different scores + update_reputation(&env, address1.clone(), ReputationEvent::TaskCompleted, 5); // 50 + update_reputation(&env, address2.clone(), ReputationEvent::TaskCompleted, 10); // 50 (complexity capped at 5, so 50) + update_reputation(&env, address3.clone(), ReputationEvent::TaskCompleted, 3); // 30 + + // Update leaderboard + update_guild_leaderboard(&env, guild_id, &address1); + update_guild_leaderboard(&env, guild_id, &address2); + update_guild_leaderboard(&env, guild_id, &address3); + + // Get top contributors + let top = get_top_contributors_for_guild(&env, guild_id, 3); + + assert_eq!(top.len(), 3); + // Should be sorted by score (descending) + assert_eq!(top.get(0).unwrap(), address1); // or address2, both have 50 + assert_eq!(top.get(2).unwrap(), address3); // Lowest score + } + + #[test] + fn test_leaderboard_limit() { + let env = setup(); + let guild_id = 1u64; + + // Create 5 contributors + for i in 0..5 { + let addr = Address::generate(&env); + update_reputation(&env, addr.clone(), ReputationEvent::TaskCompleted, (i + 1) as u32); + update_guild_leaderboard(&env, guild_id, &addr); + } + + // Get top 3 + let top = get_top_contributors_for_guild(&env, guild_id, 3); + assert_eq!(top.len(), 3); + } + + // ============ Integration Tests ============ + + #[test] + fn test_full_contributor_lifecycle() { + let env = setup(); + let address = Address::generate(&env); + + // Start as new contributor + initialize_profile(&env, address.clone()); + assert_eq!(get_tier(&env, address.clone()), ReputationTier::Bronze); + + // Complete 10 tasks + for _ in 0..10 { + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 3); + } + + // Should now be Silver tier with 300 points + assert_eq!(get_tier(&env, address.clone()), ReputationTier::Silver); + + let profile = get_reputation(&env, address.clone()); + assert_eq!(profile.tasks_completed, 10); + assert_eq!(profile.score, 300); + + // Award achievement for completing 10 tasks (ID 2: "Task Veteran") + let awarded = award_achievement(&env, address.clone(), 2); + assert_eq!(awarded, true); + + // Check total achievements + let achievements = get_achievements(&env, address); + assert_eq!(achievements.len(), 1); + } + + #[test] + fn test_reputation_with_mixed_events() { + let env = setup(); + let address = Address::generate(&env); + + // Complete 5 tasks + for _ in 0..5 { + update_reputation(&env, address.clone(), ReputationEvent::TaskCompleted, 2); + } + + // Complete 2 milestones + update_reputation(&env, address.clone(), ReputationEvent::MilestoneAchieved, 3); + update_reputation(&env, address.clone(), ReputationEvent::MilestoneAchieved, 4); + + // Win a dispute + update_reputation(&env, address.clone(), ReputationEvent::DisputeWon, 0); + + // Fail 1 task + update_reputation(&env, address.clone(), ReputationEvent::TaskFailed, 0); + + let profile = get_reputation(&env, address); + + // Verify tracking + assert_eq!(profile.tasks_completed, 5); + assert_eq!(profile.tasks_failed, 1); + assert_eq!(profile.milestones_completed, 2); + assert_eq!(profile.disputes_won, 1); + + // Calculate expected score: (5 * 20) + (3 * 20) + (4 * 20) + 5 - 10 = 100 + 60 + 80 + 5 - 10 = 235 + assert_eq!(profile.score, 235); + + // Success rate: 5 successes / 6 total tasks = 83% + assert_eq!(profile.success_rate, 83); + } +} diff --git a/contract/src/reputation/types.rs b/contract/src/reputation/types.rs new file mode 100644 index 0000000..1a16bb1 --- /dev/null +++ b/contract/src/reputation/types.rs @@ -0,0 +1,129 @@ +use soroban_sdk::{contracttype, Address, String, Vec}; + +/// Reputation tier levels determining contributor status and benefits +#[contracttype] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum ReputationTier { + Bronze = 0, // 0-99 points + Silver = 1, // 100-499 points + Gold = 2, // 500-1499 points + Platinum = 3, // 1500-4999 points + Diamond = 4, // 5000+ points +} + +/// Events that trigger reputation changes +#[contracttype] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ReputationEvent { + /// Task completed successfully + TaskCompleted, + /// Milestone reached + MilestoneAchieved, + /// Won a dispute + DisputeWon, + /// Lost a dispute + DisputeLost, + /// Task failed or cancelled + TaskFailed, + /// Time decay applied + TimeDecay, + /// Success rate bonus applied + SuccessRateBonus, +} + +/// Achievement/badge definition +#[contracttype] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Achievement { + /// Unique achievement identifier + pub id: u64, + /// Achievement name + pub name: String, + /// Detailed description + pub description: String, + /// Reputation points awarded + pub points: u32, + /// Criteria for earning (e.g., "Complete 10 tasks") + pub criteria: String, + /// Minimum tasks required + pub min_tasks: u32, + /// Minimum success rate (0-100) + pub min_success_rate: u32, +} + +/// Contributor reputation profile with full tracking +#[contracttype] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ReputationProfile { + /// Contributor address + pub address: Address, + /// Current reputation score + pub score: u32, + /// Current reputation tier + pub tier: ReputationTier, + /// Total tasks completed + pub tasks_completed: u32, + /// Total tasks failed + pub tasks_failed: u32, + /// Success rate (0-100) + pub success_rate: u32, + /// Achievements earned (by achievement ID) + pub achievements: Vec, + /// Last activity timestamp (for decay calculation) + pub last_activity: u64, + /// Creation timestamp + pub created_at: u64, + /// Disputes won + pub disputes_won: u32, + /// Disputes lost + pub disputes_lost: u32, + /// Milestones completed + pub milestones_completed: u32, +} + +// ============ Events ============ + +/// Event emitted when a reputation profile is initialized +#[contracttype] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ProfileInitializedEvent { + pub address: Address, + pub initial_score: u32, + pub tier: ReputationTier, + pub timestamp: u64, +} + +/// Event emitted when reputation score changes +#[contracttype] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ReputationUpdatedEvent { + pub address: Address, + pub event_type: ReputationEvent, + pub old_score: u32, + pub new_score: u32, + pub old_tier: ReputationTier, + pub new_tier: ReputationTier, + pub timestamp: u64, +} + +/// Event emitted when achievement is awarded +#[contracttype] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AchievementAwardedEvent { + pub address: Address, + pub achievement_id: u64, + pub achievement_name: String, + pub points_awarded: u32, + pub timestamp: u64, +} + +/// Event emitted when reputation tier changes +#[contracttype] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TierUpgradedEvent { + pub address: Address, + pub old_tier: ReputationTier, + pub new_tier: ReputationTier, + pub current_score: u32, + pub timestamp: u64, +}