From 74c45257a7c67b400fc8ee354cdf2dcc081437c9 Mon Sep 17 00:00:00 2001 From: er1c-cartman Date: Fri, 22 May 2026 14:33:50 +0900 Subject: [PATCH] Add completed group cloning --- contracts/sorosave/src/errors.rs | 1 + contracts/sorosave/src/group.rs | 48 ++++++++++++++++++++++++ contracts/sorosave/src/lib.rs | 9 +++++ contracts/sorosave/src/test.rs | 63 ++++++++++++++++++++++++++++++++ contracts/sorosave/src/types.rs | 1 + 5 files changed, 122 insertions(+) diff --git a/contracts/sorosave/src/errors.rs b/contracts/sorosave/src/errors.rs index a2b9d9d..014d247 100644 --- a/contracts/sorosave/src/errors.rs +++ b/contracts/sorosave/src/errors.rs @@ -22,4 +22,5 @@ pub enum ContractError { InsufficientMembers = 16, RoundNotComplete = 17, GroupCompleted = 18, + GroupNotCompleted = 19, } diff --git a/contracts/sorosave/src/group.rs b/contracts/sorosave/src/group.rs index 5033347..f45995e 100644 --- a/contracts/sorosave/src/group.rs +++ b/contracts/sorosave/src/group.rs @@ -30,6 +30,7 @@ pub fn create_group( let group = SavingsGroup { id: group_id, + source_group_id: 0, name, admin: admin.clone(), token, @@ -53,6 +54,53 @@ pub fn create_group( Ok(group_id) } +pub fn clone_group(env: &Env, admin: Address, source_group_id: u64) -> Result { + admin.require_auth(); + + let source_group = + storage::get_group(env, source_group_id).ok_or(ContractError::GroupNotFound)?; + + if admin != source_group.admin { + return Err(ContractError::Unauthorized); + } + + if source_group.status != GroupStatus::Completed { + return Err(ContractError::GroupNotCompleted); + } + + let group_id = storage::get_group_counter(env) + 1; + storage::set_group_counter(env, group_id); + + let group = SavingsGroup { + id: group_id, + source_group_id, + name: source_group.name, + admin: admin.clone(), + token: source_group.token, + contribution_amount: source_group.contribution_amount, + cycle_length: source_group.cycle_length, + max_members: source_group.max_members, + members: source_group.members.clone(), + payout_order: Vec::new(env), + current_round: 0, + total_rounds: 0, + status: GroupStatus::Forming, + created_at: env.ledger().timestamp(), + }; + + storage::set_group(env, &group); + for member in group.members.iter() { + storage::add_member_group(env, &member, group_id); + } + + env.events().publish( + (crate::symbol_short!("grp_clone"),), + (source_group_id, group_id), + ); + + Ok(group_id) +} + pub fn join_group(env: &Env, member: Address, group_id: u64) -> Result<(), ContractError> { member.require_auth(); diff --git a/contracts/sorosave/src/lib.rs b/contracts/sorosave/src/lib.rs index 454a6ca..27c1966 100644 --- a/contracts/sorosave/src/lib.rs +++ b/contracts/sorosave/src/lib.rs @@ -59,6 +59,15 @@ impl SoroSaveContract { group::leave_group(&env, member, group_id) } + /// Clone a completed group's settings and members into a new forming group. + pub fn clone_group( + env: Env, + admin: Address, + source_group_id: u64, + ) -> Result { + group::clone_group(&env, admin, source_group_id) + } + /// Start the group rounds. Only the group admin can call this. pub fn start_group(env: Env, admin: Address, group_id: u64) -> Result<(), ContractError> { group::start_group(&env, admin, group_id) diff --git a/contracts/sorosave/src/test.rs b/contracts/sorosave/src/test.rs index f1ac1ef..8f606a3 100644 --- a/contracts/sorosave/src/test.rs +++ b/contracts/sorosave/src/test.rs @@ -151,6 +151,69 @@ fn test_full_cycle() { assert_eq!(group.status, GroupStatus::Completed); } +#[test] +fn test_clone_completed_group_reuses_settings_and_members() { + 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 source_group_id = client.create_group( + &admin, + &String::from_str(&env, "Recurring Group"), + &token_id.address(), + &750_000, + &43200, + &4, + ); + client.join_group(&member1, &source_group_id); + client.start_group(&admin, &source_group_id); + + client.contribute(&admin, &source_group_id); + client.contribute(&member1, &source_group_id); + client.distribute_payout(&source_group_id); + + client.contribute(&admin, &source_group_id); + client.contribute(&member1, &source_group_id); + client.distribute_payout(&source_group_id); + assert_eq!( + client.get_group(&source_group_id).status, + GroupStatus::Completed + ); + + let cloned_group_id = client.clone_group(&admin, &source_group_id); + let cloned_group = client.get_group(&cloned_group_id); + + assert_eq!(cloned_group.source_group_id, source_group_id); + assert_eq!(cloned_group.token, token_id.address()); + assert_eq!(cloned_group.contribution_amount, 750_000); + assert_eq!(cloned_group.cycle_length, 43200); + assert_eq!(cloned_group.max_members, 4); + assert_eq!(cloned_group.status, GroupStatus::Forming); + assert_eq!(cloned_group.current_round, 0); + assert_eq!(cloned_group.members.len(), 2); + assert_eq!(cloned_group.members.get(0).unwrap(), admin); + assert_eq!(cloned_group.members.get(1).unwrap(), member1); + + assert_eq!( + client.get_member_groups(&admin).get(1).unwrap(), + cloned_group_id + ); + assert_eq!( + client.get_member_groups(&member1).get(1).unwrap(), + cloned_group_id + ); + + client.start_group(&admin, &cloned_group_id); + let started_clone = client.get_group(&cloned_group_id); + assert_eq!(started_clone.status, GroupStatus::Active); + assert_eq!(started_clone.total_rounds, 2); +} + #[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..a07241e 100644 --- a/contracts/sorosave/src/types.rs +++ b/contracts/sorosave/src/types.rs @@ -16,6 +16,7 @@ pub enum GroupStatus { #[derive(Clone, Debug)] pub struct SavingsGroup { pub id: u64, + pub source_group_id: u64, pub name: String, pub admin: Address, pub token: Address,