Skip to content

Commit 880200b

Browse files
authored
Merge pull request #190 from nafsonig/feat/asset
Feat/asset
2 parents 3fbbcb2 + 02538cf commit 880200b

2 files changed

Lines changed: 98 additions & 9 deletions

File tree

‎campaign/src/lib.rs‎

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ pub mod storage;
44
pub mod types;
55

66
use soroban_sdk::{contract, contractimpl, Env, Vec};
7-
use types::{AssetInfo, CampaignData, CampaignStatus, Error, MilestoneData, MilestoneStatus};
7+
use types::{CampaignData, CampaignStatus, Error, MilestoneData, MilestoneStatus, StellarAsset, CampaignEvent};
88
use storage::{get_campaign, set_campaign, set_milestone};
99

1010
pub const VERSION: u32 = 1;
@@ -16,20 +16,38 @@ pub struct CampaignContract;
1616
impl CampaignContract {
1717
/// Initialize a new campaign with strict validation on all inputs.
1818
///
19+
/// Requires: Creator authorization via `creator.require_auth()`
20+
/// Can only be called once per contract instance
21+
///
1922
/// # Panics
23+
/// - `Error::UnauthorizedCreator` if caller is not the creator or lacks authorization
24+
/// - `Error::AlreadyInitialized` if campaign already exists
2025
/// - `Error::InvalidGoalAmount` if goal_amount <= 0
2126
/// - `Error::InvalidEndTime` if end_time <= current ledger timestamp
2227
/// - `Error::InvalidAssets` if accepted_assets is empty
28+
/// - `Error::InvalidAssetCode` if any asset_code is empty or invalid
29+
/// - `Error::InvalidMilestoneCount` if milestone count is not 1-5
2330
/// - `Error::InvalidMilestones` if milestones are not sorted ascending by target_amount
2431
/// - `Error::MilestoneMismatch` if last milestone.target_amount != goal_amount
32+
///
33+
/// # Events
34+
/// Emits `campaign_initialized` event with campaign details
2535
pub fn initialize(
2636
env: Env,
2737
creator: soroban_sdk::Address,
2838
goal_amount: i128,
2939
end_time: u64,
30-
accepted_assets: Vec<AssetInfo>,
40+
accepted_assets: Vec<StellarAsset>,
3141
milestones: Vec<MilestoneData>,
3242
) -> Result<(), Error> {
43+
// Authorization check: creator must authorize this call
44+
creator.require_auth();
45+
46+
// Check if already initialized - can only initialize once
47+
if get_campaign(&env).is_some() {
48+
panic_with_error(&env, Error::AlreadyInitialized);
49+
}
50+
3351
// Validation 1: goal_amount > 0
3452
if goal_amount <= 0 {
3553
panic_with_error(&env, Error::InvalidGoalAmount);
@@ -46,20 +64,27 @@ impl CampaignContract {
4664
panic_with_error(&env, Error::InvalidAssets);
4765
}
4866

49-
// Validation 4 & 5: milestones sorted ascending and last == goal_amount
50-
if !milestones.is_empty() {
51-
validate_milestones(&env, &milestones, goal_amount)?;
67+
// Validation 3b: validate each asset code
68+
validate_assets(&env, &accepted_assets)?;
69+
70+
// Validation 4: milestone count must be 1-5
71+
let milestone_count = milestones.len() as u32;
72+
if milestone_count == 0 || milestone_count > types::MAX_MILESTONES {
73+
panic_with_error(&env, Error::InvalidMilestoneCount);
5274
}
5375

76+
// Validation 5 & 6: milestones sorted ascending and last == goal_amount
77+
validate_milestones(&env, &milestones, goal_amount)?;
78+
5479
// All validations passed, store campaign data
5580
let campaign = CampaignData {
56-
creator,
81+
creator: creator.clone(),
5782
goal_amount,
5883
raised_amount: 0,
5984
end_time,
6085
status: CampaignStatus::Active,
61-
accepted_assets,
62-
milestone_count: milestones.len() as u32,
86+
accepted_assets: accepted_assets.clone(),
87+
milestone_count,
6388
};
6489

6590
set_campaign(&env, &campaign);
@@ -69,6 +94,16 @@ impl CampaignContract {
6994
set_milestone(&env, index as u32, milestone);
7095
}
7196

97+
// Emit campaign_initialized event
98+
let event = CampaignEvent::Initialized {
99+
creator,
100+
goal_amount,
101+
end_time,
102+
asset_count: accepted_assets.len() as u32,
103+
milestone_count,
104+
};
105+
env.events().publish(("campaign", "initialized"), event);
106+
72107
Ok(())
73108
}
74109

@@ -81,6 +116,18 @@ impl CampaignContract {
81116
}
82117
}
83118

119+
/// Helper function to validate Stellar assets
120+
/// Ensures each asset has a non-empty asset_code
121+
fn validate_assets(env: &Env, assets: &Vec<StellarAsset>) -> Result<(), Error> {
122+
for asset in assets.iter() {
123+
// asset_code must be non-empty
124+
if asset.asset_code.len() == 0 {
125+
panic_with_error(env, Error::InvalidAssetCode);
126+
}
127+
}
128+
Ok(())
129+
}
130+
84131
/// Helper function to validate milestone conditions
85132
fn validate_milestones(
86133
env: &Env,
@@ -115,8 +162,12 @@ fn panic_with_error(env: &Env, error: Error) -> ! {
115162
Error::InvalidGoalAmount => "InvalidGoalAmount",
116163
Error::InvalidEndTime => "InvalidEndTime",
117164
Error::InvalidAssets => "InvalidAssets",
165+
Error::InvalidAssetCode => "InvalidAssetCode",
118166
Error::InvalidMilestones => "InvalidMilestones",
119167
Error::MilestoneMismatch => "MilestoneMismatch",
168+
Error::InvalidMilestoneCount => "InvalidMilestoneCount",
169+
Error::AlreadyInitialized => "AlreadyInitialized",
170+
Error::UnauthorizedCreator => "UnauthorizedCreator",
120171
Error::InvalidCampaignTransition => "InvalidCampaignTransition",
121172
Error::InvalidMilestoneTransition => "InvalidMilestoneTransition",
122173
Error::CampaignNotActive => "CampaignNotActive",

‎campaign/src/types.rs‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ pub enum Error {
1010
InvalidGoalAmount, // goal_amount must be > 0
1111
InvalidEndTime, // end_time must be > current ledger timestamp
1212
InvalidAssets, // accepted_assets must be non-empty
13+
InvalidAssetCode, // asset_code must be non-empty and valid
1314
InvalidMilestones, // milestones must be sorted ascending and last must equal goal
1415
MilestoneMismatch, // last milestone.target_amount != goal_amount
16+
InvalidMilestoneCount, // milestone count must be 1-5
17+
AlreadyInitialized, // campaign already initialized
18+
UnauthorizedCreator, // caller is not the creator or lacks authorization
1519

1620
// ── State transition errors ──
1721
InvalidCampaignTransition, // campaign status transition not allowed
@@ -51,7 +55,41 @@ pub enum MilestoneStatus {
5155
Released, // Funds released to beneficiary
5256
}
5357

58+
// ── Contract events ──────────────────────────────────────────────────────────
59+
60+
/// Campaign lifecycle events
61+
#[contracttype]
62+
#[derive(Clone, Debug, Eq, PartialEq)]
63+
pub enum CampaignEvent {
64+
Initialized {
65+
creator: Address,
66+
goal_amount: i128,
67+
end_time: u64,
68+
asset_count: u32,
69+
milestone_count: u32,
70+
},
71+
}
72+
73+
/// Reusable struct for Stellar asset representation
74+
/// Enables consistent multi-asset support across the contract
75+
#[contracttype]
76+
#[derive(Clone, Debug, Eq, PartialEq)]
77+
pub struct StellarAsset {
78+
/// Asset code (e.g., "XLM", "USDC", "EUR")
79+
pub asset_code: soroban_sdk::String,
80+
/// Issuer address; None for native XLM
81+
pub issuer: Option<Address>,
82+
}
83+
84+
impl StellarAsset {
85+
/// Helper function to check if this asset is native XLM
86+
pub fn is_xlm(&self) -> bool {
87+
self.issuer.is_none()
88+
}
89+
}
90+
5491
/// Accepted asset descriptor (native XLM or a Stellar asset)
92+
/// Deprecated: Use StellarAsset instead
5593
#[contracttype]
5694
#[derive(Clone, Debug, Eq, PartialEq)]
5795
pub enum AssetInfo {
@@ -83,7 +121,7 @@ pub struct CampaignData {
83121
pub raised_amount: i128,
84122
pub end_time: u64,
85123
pub status: CampaignStatus,
86-
pub accepted_assets: Vec<AssetInfo>,
124+
pub accepted_assets: Vec<StellarAsset>,
87125
pub milestone_count: u32,
88126
}
89127

0 commit comments

Comments
 (0)