From 3e2af1c021197c90d6ce3867dd651669e5d55ace Mon Sep 17 00:00:00 2001 From: nafsonig Date: Thu, 28 May 2026 23:03:57 +0000 Subject: [PATCH 1/3] implemented the input --- campaign/src/types.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/campaign/src/types.rs b/campaign/src/types.rs index 6e4f0de..5cb3c94 100644 --- a/campaign/src/types.rs +++ b/campaign/src/types.rs @@ -1,5 +1,18 @@ use soroban_sdk::{contracttype, Address, BytesN, Vec}; +// ── Error enum ────────────────────────────────────────────────────────────── + +/// Initialization validation errors +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Error { + InvalidGoalAmount, // goal_amount must be > 0 + InvalidEndTime, // end_time must be > current ledger timestamp + InvalidAssets, // accepted_assets must be non-empty + InvalidMilestones, // milestones must be sorted ascending and last must equal goal + MilestoneMismatch, // last milestone.target_amount != goal_amount +} + // ── Supporting enums ───────────────────────────────────────────────────────── /// Issue #167 – campaign lifecycle status From 63bbf454202d507ca4fc54d5a8fcb687cdb20455 Mon Sep 17 00:00:00 2001 From: nafsonig Date: Thu, 28 May 2026 23:04:36 +0000 Subject: [PATCH 2/3] implemented the input --- campaign/src/lib.rs | 112 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/campaign/src/lib.rs b/campaign/src/lib.rs index ea92d8d..9a9df45 100644 --- a/campaign/src/lib.rs +++ b/campaign/src/lib.rs @@ -3,14 +3,124 @@ pub mod storage; pub mod types; -use soroban_sdk::{contract, contractimpl, Env}; +use soroban_sdk::{contract, contractimpl, Env, Vec}; +use types::{AssetInfo, CampaignData, CampaignStatus, Error, MilestoneData}; +use storage::{get_campaign, set_campaign, set_milestone}; #[contract] pub struct CampaignContract; #[contractimpl] impl CampaignContract { + /// Initialize a new campaign with strict validation on all inputs. + /// + /// # Panics + /// - `Error::InvalidGoalAmount` if goal_amount <= 0 + /// - `Error::InvalidEndTime` if end_time <= current ledger timestamp + /// - `Error::InvalidAssets` if accepted_assets is empty + /// - `Error::InvalidMilestones` if milestones are not sorted ascending by target_amount + /// - `Error::MilestoneMismatch` if last milestone.target_amount != goal_amount + pub fn initialize( + env: Env, + creator: soroban_sdk::Address, + goal_amount: i128, + end_time: u64, + accepted_assets: Vec, + milestones: Vec, + ) -> Result<(), Error> { + // Validation 1: goal_amount > 0 + if goal_amount <= 0 { + panic_with_error(&env, Error::InvalidGoalAmount); + } + + // Validation 2: end_time > current ledger timestamp + let current_timestamp = env.ledger().timestamp(); + if end_time <= current_timestamp { + panic_with_error(&env, Error::InvalidEndTime); + } + + // Validation 3: accepted_assets non-empty + if accepted_assets.is_empty() { + panic_with_error(&env, Error::InvalidAssets); + } + + // Validation 4 & 5: milestones sorted ascending and last == goal_amount + if !milestones.is_empty() { + validate_milestones(&env, &milestones, goal_amount)?; + } + + // All validations passed, store campaign data + let campaign = CampaignData { + creator, + goal_amount, + raised_amount: 0, + end_time, + status: CampaignStatus::Active, + accepted_assets, + milestone_count: milestones.len() as u32, + }; + + set_campaign(&env, &campaign); + + // Store each milestone + for (index, milestone) in milestones.iter().enumerate() { + set_milestone(&env, index as u32, milestone); + } + + Ok(()) + } + pub fn hello(env: Env) -> soroban_sdk::Symbol { soroban_sdk::Symbol::new(&env, "campaign") } } + +/// Helper function to validate milestone conditions +fn validate_milestones( + env: &Env, + milestones: &Vec, + goal_amount: i128, +) -> Result<(), Error> { + // Check if milestones are sorted ascending by target_amount + for i in 1..milestones.len() { + let prev = &milestones.get(i - 1).unwrap(); + let current = &milestones.get(i).unwrap(); + + if prev.target_amount >= current.target_amount { + panic_with_error(env, Error::InvalidMilestones); + } + } + + // Check if last milestone.target_amount == goal_amount + if let Some(last_milestone) = milestones.last() { + if last_milestone.target_amount != goal_amount { + panic_with_error(env, Error::MilestoneMismatch); + } + } else { + panic_with_error(env, Error::InvalidMilestones); + } + + Ok(()) +} + +/// Helper function to panic with a descriptive error message +fn panic_with_error(env: &Env, error: Error) -> ! { + match error { + Error::InvalidGoalAmount => { + env.panic_with_error(soroban_sdk::Symbol::new(env, "InvalidGoalAmount")) + } + Error::InvalidEndTime => { + env.panic_with_error(soroban_sdk::Symbol::new(env, "InvalidEndTime")) + } + Error::InvalidAssets => { + env.panic_with_error(soroban_sdk::Symbol::new(env, "InvalidAssets")) + } + Error::InvalidMilestones => { + env.panic_with_error(soroban_sdk::Symbol::new(env, "InvalidMilestones")) + } + Error::MilestoneMismatch => { + env.panic_with_error(soroban_sdk::Symbol::new(env, "MilestoneMismatch")) + } + } +} + From b1be0a35b8e71031f10e4036ebed4428cbfe4f77 Mon Sep 17 00:00:00 2001 From: nafsonig Date: Thu, 28 May 2026 23:12:12 +0000 Subject: [PATCH 3/3] implemented the stayus --- campaign/src/lib.rs | 93 +++++++++++++++++++++++++++++++++++++------ campaign/src/types.rs | 43 ++++++++++++++------ 2 files changed, 111 insertions(+), 25 deletions(-) diff --git a/campaign/src/lib.rs b/campaign/src/lib.rs index 9a9df45..efb3ed7 100644 --- a/campaign/src/lib.rs +++ b/campaign/src/lib.rs @@ -4,7 +4,7 @@ pub mod storage; pub mod types; use soroban_sdk::{contract, contractimpl, Env, Vec}; -use types::{AssetInfo, CampaignData, CampaignStatus, Error, MilestoneData}; +use types::{AssetInfo, CampaignData, CampaignStatus, Error, MilestoneData, MilestoneStatus}; use storage::{get_campaign, set_campaign, set_milestone}; #[contract] @@ -105,21 +105,90 @@ fn validate_milestones( /// Helper function to panic with a descriptive error message fn panic_with_error(env: &Env, error: Error) -> ! { - match error { - Error::InvalidGoalAmount => { - env.panic_with_error(soroban_sdk::Symbol::new(env, "InvalidGoalAmount")) + let error_name = match error { + Error::InvalidGoalAmount => "InvalidGoalAmount", + Error::InvalidEndTime => "InvalidEndTime", + Error::InvalidAssets => "InvalidAssets", + Error::InvalidMilestones => "InvalidMilestones", + Error::MilestoneMismatch => "MilestoneMismatch", + Error::InvalidCampaignTransition => "InvalidCampaignTransition", + Error::InvalidMilestoneTransition => "InvalidMilestoneTransition", + Error::CampaignNotActive => "CampaignNotActive", + Error::CampaignEnded => "CampaignEnded", + Error::GoalNotReached => "GoalNotReached", + }; + env.panic_with_error(soroban_sdk::Symbol::new(env, error_name)) +} + +/// Validates campaign status transitions and panics if invalid +/// +/// Valid transitions: +/// Active -> GoalReached (when goal reached) +/// Active -> Ended (when deadline passes) +/// GoalReached -> Ended (when deadline passes) +/// Active/GoalReached/Ended -> Cancelled (by creator) +pub fn validate_campaign_transition( + env: &Env, + current_status: &CampaignStatus, + next_status: &CampaignStatus, +) -> Result<(), Error> { + match (current_status, next_status) { + // Active can transition to GoalReached, Ended, or Cancelled + (CampaignStatus::Active, CampaignStatus::GoalReached) => Ok(()), + (CampaignStatus::Active, CampaignStatus::Ended) => Ok(()), + (CampaignStatus::Active, CampaignStatus::Cancelled) => Ok(()), + + // GoalReached can transition to Ended or Cancelled + (CampaignStatus::GoalReached, CampaignStatus::Ended) => Ok(()), + (CampaignStatus::GoalReached, CampaignStatus::Cancelled) => Ok(()), + + // Ended can only transition to Cancelled + (CampaignStatus::Ended, CampaignStatus::Cancelled) => Ok(()), + + // Cancelled is terminal + (CampaignStatus::Cancelled, _) => { + panic_with_error(env, Error::InvalidCampaignTransition); } - Error::InvalidEndTime => { - env.panic_with_error(soroban_sdk::Symbol::new(env, "InvalidEndTime")) + + // All other transitions are invalid + _ => { + panic_with_error(env, Error::InvalidCampaignTransition); } - Error::InvalidAssets => { - env.panic_with_error(soroban_sdk::Symbol::new(env, "InvalidAssets")) + } +} + +/// Validates milestone status transitions and panics if invalid +/// +/// Valid transitions: +/// Locked -> Unlocked (when target_amount reached) +/// Unlocked -> Released (when explicitly released) +/// Locked -> Released (direct transition allowed) +pub fn validate_milestone_transition( + env: &Env, + current_status: &MilestoneStatus, + next_status: &MilestoneStatus, +) -> Result<(), Error> { + match (current_status, next_status) { + // Locked can transition to Unlocked or Released + (MilestoneStatus::Locked, MilestoneStatus::Unlocked) => Ok(()), + (MilestoneStatus::Locked, MilestoneStatus::Released) => Ok(()), + + // Unlocked can transition to Released + (MilestoneStatus::Unlocked, MilestoneStatus::Released) => Ok(()), + + // Released is terminal + (MilestoneStatus::Released, _) => { + panic_with_error(env, Error::InvalidMilestoneTransition); } - Error::InvalidMilestones => { - env.panic_with_error(soroban_sdk::Symbol::new(env, "InvalidMilestones")) + + // Prevent Unlocked -> Locked (going backwards) + (MilestoneStatus::Unlocked, MilestoneStatus::Locked) => { + panic_with_error(env, Error::InvalidMilestoneTransition); } - Error::MilestoneMismatch => { - env.panic_with_error(soroban_sdk::Symbol::new(env, "MilestoneMismatch")) + + // All other transitions are invalid + _ => { + panic_with_error(env, Error::InvalidMilestoneTransition); } } } diff --git a/campaign/src/types.rs b/campaign/src/types.rs index 5cb3c94..f1d263a 100644 --- a/campaign/src/types.rs +++ b/campaign/src/types.rs @@ -2,36 +2,53 @@ use soroban_sdk::{contracttype, Address, BytesN, Vec}; // ── Error enum ────────────────────────────────────────────────────────────── -/// Initialization validation errors +/// All error types for validation and state transitions #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub enum Error { - InvalidGoalAmount, // goal_amount must be > 0 - InvalidEndTime, // end_time must be > current ledger timestamp - InvalidAssets, // accepted_assets must be non-empty - InvalidMilestones, // milestones must be sorted ascending and last must equal goal - MilestoneMismatch, // last milestone.target_amount != goal_amount + // ── Initialization validation errors ── + InvalidGoalAmount, // goal_amount must be > 0 + InvalidEndTime, // end_time must be > current ledger timestamp + InvalidAssets, // accepted_assets must be non-empty + InvalidMilestones, // milestones must be sorted ascending and last must equal goal + MilestoneMismatch, // last milestone.target_amount != goal_amount + + // ── State transition errors ── + InvalidCampaignTransition, // campaign status transition not allowed + InvalidMilestoneTransition,// milestone status transition not allowed + CampaignNotActive, // campaign must be Active to accept donations + CampaignEnded, // campaign end_time has passed + GoalNotReached, // cannot transition to GoalReached before reaching goal } // ── Supporting enums ───────────────────────────────────────────────────────── /// Issue #167 – campaign lifecycle status +/// State transitions: +/// Active -> GoalReached (goal reached) +/// Active -> Ended (deadline passed) +/// GoalReached -> Ended (deadline passed) +/// Active/GoalReached/Ended -> Cancelled (by creator) #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub enum CampaignStatus { - Active, - Successful, - Failed, - Cancelled, + Active, // Campaign accepting donations + GoalReached, // Goal amount reached, still accepting donations until deadline + Ended, // Deadline passed or campaign concluded + Cancelled, // Campaign cancelled by creator } /// Issue #168 – milestone release status +/// State transitions: +/// Locked -> Unlocked (when target_amount reached) +/// Unlocked -> Released (when explicitly released by admin) +/// Locked/Unlocked -> Released (milestone marked as released) #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub enum MilestoneStatus { - Pending, - Released, - Cancelled, + Locked, // Milestone condition not yet met + Unlocked, // Target amount reached, awaiting release + Released, // Funds released to beneficiary } /// Accepted asset descriptor (native XLM or a Stellar asset)