From b5a094bfb03b9f95641fc86a6191ff378b05cd0d Mon Sep 17 00:00:00 2001 From: Henry Peters Date: Fri, 20 Feb 2026 09:21:06 +0100 Subject: [PATCH 1/2] feat: feature/issue-102-get-top-contributor-for-campaign --- contract/contract/src/base/types.rs | 4 + contract/contract/src/crowdfunding.rs | 25 +++++ .../contract/src/interfaces/crowdfunding.rs | 5 + contract/contract/test/crowdfunding_test.rs | 96 +++++++++++++++++++ 4 files changed, 130 insertions(+) diff --git a/contract/contract/src/base/types.rs b/contract/contract/src/base/types.rs index 44140c9..a7702ae 100644 --- a/contract/contract/src/base/types.rs +++ b/contract/contract/src/base/types.rs @@ -93,6 +93,8 @@ pub struct CampaignMetrics { pub total_raised: i128, pub contributor_count: u32, pub last_donation_at: u64, + pub max_donation: i128, + pub top_contributor: Option
, } impl Default for CampaignMetrics { @@ -107,6 +109,8 @@ impl CampaignMetrics { total_raised: 0, contributor_count: 0, last_donation_at: 0, + max_donation: 0, + top_contributor: None, } } } diff --git a/contract/contract/src/crowdfunding.rs b/contract/contract/src/crowdfunding.rs index 78120df..75d0cc5 100644 --- a/contract/contract/src/crowdfunding.rs +++ b/contract/contract/src/crowdfunding.rs @@ -171,6 +171,25 @@ impl CrowdfundingTrait for CrowdfundingContract { .unwrap_or(0) } + fn get_top_contributor_for_campaign( + env: Env, + campaign_id: BytesN<32>, + ) -> Result { + // Validate campaign exists + Self::get_campaign(env.clone(), campaign_id.clone())?; + + let metrics_key = StorageKey::CampaignMetrics(campaign_id); + let metrics: CampaignMetrics = env + .storage() + .instance() + .get(&metrics_key) + .unwrap_or_default(); + + metrics + .top_contributor + .ok_or(CrowdfundingError::CampaignNotFound) + } + fn get_all_campaigns(env: Env) -> Vec> { env.storage() .instance() @@ -301,6 +320,12 @@ impl CrowdfundingTrait for CrowdfundingContract { metrics.total_raised += amount; metrics.last_donation_at = env.ledger().timestamp(); + // Track top contributor (whale donor) + if amount > metrics.max_donation { + metrics.max_donation = amount; + metrics.top_contributor = Some(donor.clone()); + } + // Track unique donor let donor_key = StorageKey::CampaignDonor(campaign_id.clone(), donor.clone()); if !env.storage().instance().has(&donor_key) { diff --git a/contract/contract/src/interfaces/crowdfunding.rs b/contract/contract/src/interfaces/crowdfunding.rs index 0accbcc..e323a88 100644 --- a/contract/contract/src/interfaces/crowdfunding.rs +++ b/contract/contract/src/interfaces/crowdfunding.rs @@ -82,6 +82,11 @@ pub trait CrowdfundingTrait { fn get_global_raised_total(env: Env) -> i128; + fn get_top_contributor_for_campaign( + env: Env, + campaign_id: BytesN<32>, + ) -> Result; + fn initialize( env: Env, admin: Address, diff --git a/contract/contract/test/crowdfunding_test.rs b/contract/contract/test/crowdfunding_test.rs index 91d57df..83d81bb 100644 --- a/contract/contract/test/crowdfunding_test.rs +++ b/contract/contract/test/crowdfunding_test.rs @@ -2754,3 +2754,99 @@ fn test_withdraw_platform_fees_insufficient_fees() { let res = client.try_withdraw_platform_fees(&admin, &100); assert_eq!(res, Err(Ok(CrowdfundingError::InsufficientFees))); } + +#[test] +fn test_get_top_contributor_for_campaign() { + let env = Env::default(); + let (client, _, token_address) = setup_test(&env); + + let token_admin_client = soroban_sdk::token::StellarAssetClient::new(&env, &token_address); + + let creator = Address::generate(&env); + let campaign_id = create_test_campaign_id(&env, 201); + client.create_campaign( + &campaign_id, + &String::from_str(&env, "Whale Donor Test"), + &creator, + &10000i128, + &(env.ledger().timestamp() + 1000), + &token_address, + ); + + // Setup multiple donors with different donation amounts + let donor1 = Address::generate(&env); + token_admin_client.mint(&donor1, &5000i128); + + let donor2 = Address::generate(&env); + token_admin_client.mint(&donor2, &5000i128); + + let donor3 = Address::generate(&env); + token_admin_client.mint(&donor3, &5000i128); + + // First donation: donor1 gives 100 XLM + client.donate(&campaign_id, &donor1, &token_address, &100i128); + assert_eq!(client.get_top_contributor_for_campaign(&campaign_id), donor1); + + // Second donation: donor2 gives 500 XLM (new top contributor) + client.donate(&campaign_id, &donor2, &token_address, &500i128); + assert_eq!(client.get_top_contributor_for_campaign(&campaign_id), donor2); + + // Third donation: donor3 gives 250 XLM (donor2 still top) + client.donate(&campaign_id, &donor3, &token_address, &250i128); + assert_eq!(client.get_top_contributor_for_campaign(&campaign_id), donor2); + + // Fourth donation: donor1 gives 400 more (single donation is 400, less than current max 500) + // donor2 remains top since 400 < 500 + client.donate(&campaign_id, &donor1, &token_address, &400i128); + assert_eq!(client.get_top_contributor_for_campaign(&campaign_id), donor2); + + // Fifth donation: donor1 gives 600 (single donation exceeds current max) + // donor1 becomes new top + let donor1_new = Address::generate(&env); + token_admin_client.mint(&donor1_new, &10000i128); + client.donate(&campaign_id, &donor1_new, &token_address, &600i128); + assert_eq!(client.get_top_contributor_for_campaign(&campaign_id), donor1_new); +} + +#[test] +fn test_get_top_contributor_for_nonexistent_campaign() { + let env = Env::default(); + let (client, _, _) = setup_test(&env); + + let campaign_id = create_test_campaign_id(&env, 202); + let result = client.try_get_top_contributor_for_campaign(&campaign_id); + assert_eq!(result, Err(Ok(CrowdfundingError::CampaignNotFound))); +} + +#[test] +fn test_get_top_contributor_single_donor() { + let env = Env::default(); + let (client, _, token_address) = setup_test(&env); + + let token_admin_client = soroban_sdk::token::StellarAssetClient::new(&env, &token_address); + + let creator = Address::generate(&env); + let campaign_id = create_test_campaign_id(&env, 203); + client.create_campaign( + &campaign_id, + &String::from_str(&env, "Single Donor Test"), + &creator, + &10000i128, + &(env.ledger().timestamp() + 1000), + &token_address, + ); + + // Setup single donor + let donor = Address::generate(&env); + token_admin_client.mint(&donor, &10000i128); + + // Multiple donations from same donor + client.donate(&campaign_id, &donor, &token_address, &100i128); + assert_eq!(client.get_top_contributor_for_campaign(&campaign_id), donor); + + client.donate(&campaign_id, &donor, &token_address, &200i128); + assert_eq!(client.get_top_contributor_for_campaign(&campaign_id), donor); + + client.donate(&campaign_id, &donor, &token_address, &50i128); + assert_eq!(client.get_top_contributor_for_campaign(&campaign_id), donor); +} From 3e2ff829b876f7213252a072225eaea7b60d3471 Mon Sep 17 00:00:00 2001 From: Henry Peters Date: Fri, 20 Feb 2026 17:20:54 +0100 Subject: [PATCH 2/2] feat: fix cargo fmt --- contract/contract/test/crowdfunding_test.rs | 25 ++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/contract/contract/test/crowdfunding_test.rs b/contract/contract/test/crowdfunding_test.rs index 83d81bb..464b949 100644 --- a/contract/contract/test/crowdfunding_test.rs +++ b/contract/contract/test/crowdfunding_test.rs @@ -2785,27 +2785,42 @@ fn test_get_top_contributor_for_campaign() { // First donation: donor1 gives 100 XLM client.donate(&campaign_id, &donor1, &token_address, &100i128); - assert_eq!(client.get_top_contributor_for_campaign(&campaign_id), donor1); + assert_eq!( + client.get_top_contributor_for_campaign(&campaign_id), + donor1 + ); // Second donation: donor2 gives 500 XLM (new top contributor) client.donate(&campaign_id, &donor2, &token_address, &500i128); - assert_eq!(client.get_top_contributor_for_campaign(&campaign_id), donor2); + assert_eq!( + client.get_top_contributor_for_campaign(&campaign_id), + donor2 + ); // Third donation: donor3 gives 250 XLM (donor2 still top) client.donate(&campaign_id, &donor3, &token_address, &250i128); - assert_eq!(client.get_top_contributor_for_campaign(&campaign_id), donor2); + assert_eq!( + client.get_top_contributor_for_campaign(&campaign_id), + donor2 + ); // Fourth donation: donor1 gives 400 more (single donation is 400, less than current max 500) // donor2 remains top since 400 < 500 client.donate(&campaign_id, &donor1, &token_address, &400i128); - assert_eq!(client.get_top_contributor_for_campaign(&campaign_id), donor2); + assert_eq!( + client.get_top_contributor_for_campaign(&campaign_id), + donor2 + ); // Fifth donation: donor1 gives 600 (single donation exceeds current max) // donor1 becomes new top let donor1_new = Address::generate(&env); token_admin_client.mint(&donor1_new, &10000i128); client.donate(&campaign_id, &donor1_new, &token_address, &600i128); - assert_eq!(client.get_top_contributor_for_campaign(&campaign_id), donor1_new); + assert_eq!( + client.get_top_contributor_for_campaign(&campaign_id), + donor1_new + ); } #[test]