Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 49 additions & 10 deletions contracts/sorosave/src/contribution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<i128>,
) -> Result<(), ContractError> {
let group = storage::get_group(env, group_id).ok_or(ContractError::GroupNotFound)?;

if group.status != GroupStatus::Active {
Expand All @@ -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() {
Expand All @@ -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(())
Expand All @@ -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<i128, ContractError> {
let round_info =
storage::get_round(env, group_id, round).ok_or(ContractError::RoundNotActive)?;
Ok(round_info.contribution_amounts.get(member).unwrap_or(0))
}
1 change: 1 addition & 0 deletions contracts/sorosave/src/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
20 changes: 20 additions & 0 deletions contracts/sorosave/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<i128, ContractError> {
contribution::get_member_contribution_progress(&env, member, group_id, round)
}

// ─── Payouts ────────────────────────────────────────────────────

/// Distribute the pot to the current round's recipient. Anyone can call this
Expand Down
1 change: 1 addition & 0 deletions contracts/sorosave/src/payout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
44 changes: 44 additions & 0 deletions contracts/sorosave/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions contracts/sorosave/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub struct RoundInfo {
pub round_number: u32,
pub recipient: Address,
pub contributions: Map<Address, bool>,
pub contribution_amounts: Map<Address, i128>,
pub total_contributed: i128,
pub is_complete: bool,
pub deadline: u64,
Expand Down