From 711241186cd401642d8dd38b4e92af2058971861 Mon Sep 17 00:00:00 2001 From: er1c-cartman Date: Fri, 22 May 2026 15:04:40 +0900 Subject: [PATCH] Add partial contribution support --- contracts/sorosave/src/contribution.rs | 59 +++++++++++++++++++++----- contracts/sorosave/src/group.rs | 1 + contracts/sorosave/src/lib.rs | 20 +++++++++ contracts/sorosave/src/payout.rs | 1 + contracts/sorosave/src/test.rs | 44 +++++++++++++++++++ contracts/sorosave/src/types.rs | 1 + 6 files changed, 116 insertions(+), 10 deletions(-) diff --git a/contracts/sorosave/src/contribution.rs b/contracts/sorosave/src/contribution.rs index 4285c9f..7f50d7c 100644 --- a/contracts/sorosave/src/contribution.rs +++ b/contracts/sorosave/src/contribution.rs @@ -6,7 +6,25 @@ use crate::types::{GroupStatus, RoundInfo}; pub fn contribute(env: &Env, member: Address, group_id: u64) -> Result<(), ContractError> { member.require_auth(); + contribute_amount(env, member, group_id, None) +} +pub fn contribute_partial( + env: &Env, + member: Address, + group_id: u64, + amount: i128, +) -> Result<(), ContractError> { + member.require_auth(); + contribute_amount(env, member, group_id, Some(amount)) +} + +fn contribute_amount( + env: &Env, + member: Address, + group_id: u64, + amount: Option, +) -> Result<(), ContractError> { let group = storage::get_group(env, group_id).ok_or(ContractError::GroupNotFound)?; if group.status != GroupStatus::Active { @@ -32,22 +50,32 @@ pub fn contribute(env: &Env, member: Address, group_id: u64) -> Result<(), Contr return Err(ContractError::RoundNotActive); } - // Check if already contributed this round - if round_info.contributions.contains_key(member.clone()) { + let already_paid = round_info + .contribution_amounts + .get(member.clone()) + .unwrap_or(0); + if already_paid >= group.contribution_amount { return Err(ContractError::AlreadyContributed); } + let remaining = group.contribution_amount - already_paid; + let amount = amount.unwrap_or(remaining); + if amount <= 0 || amount > remaining { + return Err(ContractError::InvalidAmount); + } // Transfer tokens from member to this contract let token_client = soroban_sdk::token::Client::new(env, &group.token); - token_client.transfer( - &member, - &env.current_contract_address(), - &group.contribution_amount, - ); + token_client.transfer(&member, &env.current_contract_address(), &amount); // Record contribution - round_info.contributions.set(member.clone(), true); - round_info.total_contributed += group.contribution_amount; + let new_paid = already_paid + amount; + round_info + .contribution_amounts + .set(member.clone(), new_paid); + if new_paid == group.contribution_amount { + round_info.contributions.set(member.clone(), true); + } + round_info.total_contributed += amount; // Check if all members have contributed if round_info.contributions.len() == group.members.len() { @@ -58,7 +86,7 @@ pub fn contribute(env: &Env, member: Address, group_id: u64) -> Result<(), Contr env.events().publish( (crate::symbol_short!("contrib"),), - (group_id, member, group.contribution_amount), + (group_id, member, amount), ); Ok(()) @@ -78,3 +106,14 @@ pub fn has_contributed( storage::get_round(env, group_id, round).ok_or(ContractError::RoundNotActive)?; Ok(round_info.contributions.contains_key(member)) } + +pub fn get_member_contribution_progress( + env: &Env, + member: Address, + group_id: u64, + round: u32, +) -> Result { + let round_info = + storage::get_round(env, group_id, round).ok_or(ContractError::RoundNotActive)?; + Ok(round_info.contribution_amounts.get(member).unwrap_or(0)) +} diff --git a/contracts/sorosave/src/group.rs b/contracts/sorosave/src/group.rs index 5033347..758710f 100644 --- a/contracts/sorosave/src/group.rs +++ b/contracts/sorosave/src/group.rs @@ -150,6 +150,7 @@ pub fn start_group(env: &Env, admin: Address, group_id: u64) -> Result<(), Contr round_number: 1, recipient: first_recipient, contributions: Map::new(env), + contribution_amounts: Map::new(env), total_contributed: 0, is_complete: false, deadline: env.ledger().timestamp() + group.cycle_length, diff --git a/contracts/sorosave/src/lib.rs b/contracts/sorosave/src/lib.rs index 454a6ca..e73a84e 100644 --- a/contracts/sorosave/src/lib.rs +++ b/contracts/sorosave/src/lib.rs @@ -81,6 +81,16 @@ impl SoroSaveContract { contribution::contribute(&env, member, group_id) } + /// Contribute part of the required amount to the current round of a group. + pub fn contribute_partial( + env: Env, + member: Address, + group_id: u64, + amount: i128, + ) -> Result<(), ContractError> { + contribution::contribute_partial(&env, member, group_id, amount) + } + /// Get the status of a specific round. pub fn get_round_status( env: Env, @@ -100,6 +110,16 @@ impl SoroSaveContract { contribution::has_contributed(&env, member, group_id, round) } + /// Get how much a member has contributed toward a round. + pub fn get_member_contribution_progress( + env: Env, + member: Address, + group_id: u64, + round: u32, + ) -> Result { + contribution::get_member_contribution_progress(&env, member, group_id, round) + } + // ─── Payouts ──────────────────────────────────────────────────── /// Distribute the pot to the current round's recipient. Anyone can call this diff --git a/contracts/sorosave/src/payout.rs b/contracts/sorosave/src/payout.rs index 76ed389..aa4cc73 100644 --- a/contracts/sorosave/src/payout.rs +++ b/contracts/sorosave/src/payout.rs @@ -50,6 +50,7 @@ pub fn distribute_payout(env: &Env, group_id: u64) -> Result<(), ContractError> round_number: group.current_round, recipient: next_recipient, contributions: Map::new(env), + contribution_amounts: Map::new(env), total_contributed: 0, is_complete: false, deadline: env.ledger().timestamp() + group.cycle_length, diff --git a/contracts/sorosave/src/test.rs b/contracts/sorosave/src/test.rs index f1ac1ef..d1af7a5 100644 --- a/contracts/sorosave/src/test.rs +++ b/contracts/sorosave/src/test.rs @@ -151,6 +151,50 @@ fn test_full_cycle() { assert_eq!(group.status, GroupStatus::Completed); } +#[test] +fn test_partial_contributions_accumulate_until_complete() { + let (env, admin, client, _token) = setup_env(); + + let member1 = Address::generate(&env); + let mint_admin = Address::generate(&env); + let token_id = env.register_stellar_asset_contract_v2(mint_admin); + let token_sac = StellarAssetClient::new(&env, &token_id.address()); + token_sac.mint(&admin, &10_000_000); + token_sac.mint(&member1, &10_000_000); + + let group_id = client.create_group( + &admin, + &String::from_str(&env, "Partial Contributions"), + &token_id.address(), + &1_000_000, + &86400, + &5, + ); + client.join_group(&member1, &group_id); + client.start_group(&admin, &group_id); + + client.contribute(&admin, &group_id); + client.contribute_partial(&member1, &group_id, &400_000); + + assert_eq!( + client.get_member_contribution_progress(&member1, &group_id, &1), + 400_000 + ); + assert!(!client.has_contributed(&member1, &group_id, &1)); + assert!(!client.get_round_status(&group_id, &1).is_complete); + + client.contribute_partial(&member1, &group_id, &600_000); + + let round = client.get_round_status(&group_id, &1); + assert_eq!( + client.get_member_contribution_progress(&member1, &group_id, &1), + 1_000_000 + ); + assert!(client.has_contributed(&member1, &group_id, &1)); + assert!(round.is_complete); + assert_eq!(round.total_contributed, 2_000_000); +} + #[test] fn test_member_groups() { let (env, admin, client, token) = setup_env(); diff --git a/contracts/sorosave/src/types.rs b/contracts/sorosave/src/types.rs index f741099..6ed8d73 100644 --- a/contracts/sorosave/src/types.rs +++ b/contracts/sorosave/src/types.rs @@ -37,6 +37,7 @@ pub struct RoundInfo { pub round_number: u32, pub recipient: Address, pub contributions: Map, + pub contribution_amounts: Map, pub total_contributed: i128, pub is_complete: bool, pub deadline: u64,