Skip to content

Commit 6e8f59f

Browse files
committed
feat: Add campaign update functionality, direct submission on creation, and rejection reasons.
1 parent d63c56b commit 6e8f59f

12 files changed

+3152
-29
lines changed

contracts/crowdfund_registry/src/contract.rs

Lines changed: 105 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ impl CrowdfundRegistry {
129129
deadline: u64,
130130
milestone_descs: Vec<(String, u32)>,
131131
min_pledge: i128,
132+
submit: bool,
132133
) -> Result<u64, CrowdfundError> {
133134
owner.require_auth();
134135

@@ -140,18 +141,7 @@ impl CrowdfundRegistry {
140141
}
141142

142143
// Validate milestones: count 2-10, sum of pcts = 10000
143-
let ms_count = milestone_descs.len();
144-
if !(2..=10).contains(&ms_count) {
145-
return Err(CrowdfundError::InvalidMilestones);
146-
}
147-
let mut pct_sum: u32 = 0;
148-
for i in 0..ms_count {
149-
let (_, pct) = milestone_descs.get(i).unwrap();
150-
pct_sum += pct;
151-
}
152-
if pct_sum != 10000 {
153-
return Err(CrowdfundError::InvalidMilestones);
154-
}
144+
Self::validate_milestones(&milestone_descs)?;
155145

156146
let mut count: u64 = env
157147
.storage()
@@ -181,30 +171,24 @@ impl CrowdfundRegistry {
181171
env.invoke_contract(&escrow_addr, &sym(&env, "create_pool"), pool_args);
182172

183173
// Store milestones decomposed
184-
for i in 0..ms_count {
185-
let (desc, pct) = milestone_descs.get(i).unwrap();
186-
let milestone = Milestone {
187-
id: i,
188-
description: desc,
189-
pct,
190-
status: CrowdfundMilestoneStatus::Pending,
191-
};
192-
env.storage()
193-
.persistent()
194-
.set(&CrowdfundDataKey::CampaignMilestone(count, i), &milestone);
174+
Self::set_milestones(&env, count, &milestone_descs);
175+
176+
let mut status = CampaignStatus::Draft;
177+
if submit {
178+
status = CampaignStatus::Submitted;
195179
}
196180

197181
let campaign = Campaign {
198182
id: count,
199183
owner: owner.clone(),
200184
metadata_cid,
201-
status: CampaignStatus::Draft,
185+
status,
202186
funding_goal,
203187
current_funding: 0,
204188
asset,
205189
pool_id,
206190
deadline,
207-
milestone_count: ms_count,
191+
milestone_count: milestone_descs.len(),
208192
min_pledge,
209193
backer_count: 0,
210194
refund_progress: 0,
@@ -223,9 +207,65 @@ impl CrowdfundRegistry {
223207
}
224208
.publish(&env);
225209

210+
if submit {
211+
CampaignSubmittedForReview { id: count }.publish(&env);
212+
}
213+
226214
Ok(count)
227215
}
228216

217+
pub fn update_campaign(
218+
env: Env,
219+
campaign_id: u64,
220+
metadata_cid: String,
221+
funding_goal: i128,
222+
asset: Address,
223+
deadline: u64,
224+
milestone_descs: Vec<(String, u32)>,
225+
min_pledge: i128,
226+
) -> Result<(), CrowdfundError> {
227+
let key = CrowdfundDataKey::Campaign(campaign_id);
228+
let mut campaign: Campaign = env
229+
.storage()
230+
.persistent()
231+
.get(&key)
232+
.ok_or(CrowdfundError::CampaignNotFound)?;
233+
234+
campaign.owner.require_auth();
235+
236+
if campaign.status != CampaignStatus::Draft {
237+
return Err(CrowdfundError::NotDraft);
238+
}
239+
240+
if funding_goal <= 0 {
241+
return Err(CrowdfundError::AmountNotPositive);
242+
}
243+
if deadline <= env.ledger().timestamp() {
244+
return Err(CrowdfundError::DeadlinePassed);
245+
}
246+
247+
Self::validate_milestones(&milestone_descs)?;
248+
Self::set_milestones(&env, campaign_id, &milestone_descs);
249+
250+
campaign.metadata_cid = metadata_cid;
251+
campaign.funding_goal = funding_goal;
252+
campaign.asset = asset;
253+
campaign.deadline = deadline;
254+
campaign.milestone_count = milestone_descs.len();
255+
campaign.min_pledge = min_pledge;
256+
257+
env.storage().persistent().set(&key, &campaign);
258+
Self::extend_persistent_ttl(&env, &key);
259+
260+
crate::events::CampaignUpdated {
261+
id: campaign_id,
262+
funding_goal,
263+
}
264+
.publish(&env);
265+
266+
Ok(())
267+
}
268+
229269
// ========================================================================
230270
// GOVERNANCE: APPROVAL WORKFLOW
231271
// Draft → Submitted → Validated → Campaigning
@@ -309,7 +349,11 @@ impl CrowdfundRegistry {
309349
Ok(session_id)
310350
}
311351

312-
pub fn reject_campaign(env: Env, campaign_id: u64) -> Result<(), CrowdfundError> {
352+
pub fn reject_campaign(
353+
env: Env,
354+
campaign_id: u64,
355+
reason: String,
356+
) -> Result<(), CrowdfundError> {
313357
let admin = Self::require_admin(&env)?;
314358
admin.require_auth();
315359

@@ -328,7 +372,7 @@ impl CrowdfundRegistry {
328372
campaign.vote_session_id = None;
329373
env.storage().persistent().set(&key, &campaign);
330374

331-
CampaignRejected { id: campaign_id }.publish(&env);
375+
CampaignRejected { id: campaign_id, reason }.publish(&env);
332376
Ok(())
333377
}
334378

@@ -985,4 +1029,38 @@ impl CrowdfundRegistry {
9851029
.get(&CrowdfundDataKey::GovernanceVoting)
9861030
.expect("not initialized")
9871031
}
1032+
1033+
fn validate_milestones(milestone_descs: &Vec<(String, u32)>) -> Result<(), CrowdfundError> {
1034+
let ms_count = milestone_descs.len();
1035+
if !(2..=10).contains(&ms_count) {
1036+
return Err(CrowdfundError::InvalidMilestones);
1037+
}
1038+
let mut pct_sum: u32 = 0;
1039+
for i in 0..ms_count {
1040+
let (_, pct) = milestone_descs.get(i).unwrap();
1041+
pct_sum += pct;
1042+
}
1043+
if pct_sum != 10000 {
1044+
return Err(CrowdfundError::InvalidMilestones);
1045+
}
1046+
Ok(())
1047+
}
1048+
1049+
fn set_milestones(env: &Env, campaign_id: u64, milestone_descs: &Vec<(String, u32)>) {
1050+
for i in 0..milestone_descs.len() {
1051+
let (desc, pct) = milestone_descs.get(i).unwrap();
1052+
let milestone = Milestone {
1053+
id: i,
1054+
description: desc,
1055+
pct,
1056+
status: CrowdfundMilestoneStatus::Pending,
1057+
};
1058+
env.storage()
1059+
.persistent()
1060+
.set(
1061+
&CrowdfundDataKey::CampaignMilestone(campaign_id, i),
1062+
&milestone,
1063+
);
1064+
}
1065+
}
9881066
}

contracts/crowdfund_registry/src/events/mod.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use soroban_sdk::{contractevent, Address, BytesN};
1+
use soroban_sdk::{contractevent, Address, BytesN, String};
22

33
#[contractevent]
44
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -109,6 +109,15 @@ pub struct CampaignApproved {
109109
pub struct CampaignRejected {
110110
#[topic]
111111
pub id: u64,
112+
pub reason: String,
113+
}
114+
115+
#[contractevent]
116+
#[derive(Clone, Debug, Eq, PartialEq)]
117+
pub struct CampaignUpdated {
118+
#[topic]
119+
pub id: u64,
120+
pub funding_goal: i128,
112121
}
113122

114123
#[contractevent]

contracts/crowdfund_registry/src/tests/mod.rs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ fn test_create_campaign() {
110110
&(t.env.ledger().timestamp() + 86400),
111111
&make_milestones(&t.env),
112112
&100i128,
113+
&false,
113114
);
114115

115116
assert_eq!(cid, 1);
@@ -132,6 +133,7 @@ fn test_governance_flow() {
132133
&(t.env.ledger().timestamp() + 86400),
133134
&make_milestones(&t.env),
134135
&100i128,
136+
&false,
135137
);
136138

137139
assert_eq!(t.client.get_campaign(&cid).status, CampaignStatus::Draft);
@@ -176,13 +178,70 @@ fn test_reject_campaign() {
176178
&(t.env.ledger().timestamp() + 86400),
177179
&make_milestones(&t.env),
178180
&100i128,
181+
&false,
179182
);
180183

181184
t.client.submit_for_review(&cid);
182-
t.client.reject_campaign(&cid);
185+
t.client.reject_campaign(&cid, &String::from_str(&t.env, "Need more detail"));
183186
assert_eq!(t.client.get_campaign(&cid).status, CampaignStatus::Draft);
184187
}
185188

189+
#[test]
190+
fn test_create_and_submit_campaign() {
191+
let t = setup();
192+
let owner = t.admin.clone();
193+
194+
let cid = t.client.create_campaign(
195+
&owner,
196+
&String::from_str(&t.env, "Instant Submit"),
197+
&10000i128,
198+
&t.token_addr,
199+
&(t.env.ledger().timestamp() + 86400),
200+
&make_milestones(&t.env),
201+
&100i128,
202+
&true,
203+
);
204+
205+
assert_eq!(t.client.get_campaign(&cid).status, CampaignStatus::Submitted);
206+
}
207+
208+
#[test]
209+
fn test_update_campaign() {
210+
let t = setup();
211+
let owner = t.admin.clone();
212+
213+
let cid = t.client.create_campaign(
214+
&owner,
215+
&String::from_str(&t.env, "Draft"),
216+
&10000i128,
217+
&t.token_addr,
218+
&(t.env.ledger().timestamp() + 86400),
219+
&make_milestones(&t.env),
220+
&100i128,
221+
&false,
222+
);
223+
224+
let new_goal = 20000i128;
225+
let mut new_ms = Vec::new(&t.env);
226+
new_ms.push_back((String::from_str(&t.env, "Phase 1"), 5000u32));
227+
new_ms.push_back((String::from_str(&t.env, "Phase 2"), 5000u32));
228+
229+
t.client.update_campaign(
230+
&cid,
231+
&String::from_str(&t.env, "Updated"),
232+
&new_goal,
233+
&t.token_addr,
234+
&(t.env.ledger().timestamp() + 90000),
235+
&new_ms,
236+
&200i128,
237+
);
238+
239+
let campaign = t.client.get_campaign(&cid);
240+
assert_eq!(campaign.funding_goal, new_goal);
241+
assert_eq!(campaign.milestone_count, 2);
242+
assert_eq!(campaign.min_pledge, 200);
243+
}
244+
186245
#[test]
187246
fn test_full_lifecycle() {
188247
let t = setup();
@@ -203,6 +262,7 @@ fn test_full_lifecycle() {
203262
&(t.env.ledger().timestamp() + 86400),
204263
&make_milestones(&t.env),
205264
&100i128,
265+
&false,
206266
);
207267

208268
// Advance through governance flow
@@ -252,6 +312,7 @@ fn test_failed_campaign_refund() {
252312
&deadline,
253313
&make_milestones(&t.env),
254314
&100i128,
315+
&false,
255316
);
256317

257318
// Advance through governance flow
@@ -296,6 +357,7 @@ fn test_cancel_campaign() {
296357
&(t.env.ledger().timestamp() + 86400),
297358
&make_milestones(&t.env),
298359
&100i128,
360+
&false,
299361
);
300362

301363
// Advance through governance flow
@@ -331,6 +393,7 @@ fn test_reject_milestone() {
331393
&(t.env.ledger().timestamp() + 86400),
332394
&make_milestones(&t.env),
333395
&100i128,
396+
&false,
334397
);
335398

336399
// Advance through governance flow
@@ -376,6 +439,7 @@ fn test_invalid_milestones_rejected() {
376439
&(t.env.ledger().timestamp() + 86400),
377440
&bad_ms,
378441
&100i128,
442+
&false,
379443
);
380444
assert!(result.is_err());
381445
}

contracts/crowdfund_registry/test_snapshots/tests/test_cancel_campaign.1.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,9 @@
267267
},
268268
{
269269
"i128": "100"
270+
},
271+
{
272+
"bool": false
270273
}
271274
]
272275
}

0 commit comments

Comments
 (0)