Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
8 changes: 8 additions & 0 deletions contracts/boundless/src/datatypes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,11 @@ pub struct CampaignStatusUpdated {
pub status: Status,
pub admin: Address,
}

#[contractevent]
pub struct FundsReleased {
pub campaign_id: u64,
pub milestone_id: u64,
pub amount: i128,
pub releaser: Address,
}
79 changes: 73 additions & 6 deletions contracts/boundless/src/logic/campaign.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::datatypes::{
Backer, BoundlessError, Campaign, CampaignCancelled, CampaignFunded, CampaignStatusUpdated,
Milestone, Status,
FundsReleased, Milestone, MilestoneStatus, Status,
};
use crate::interface::{CampaignManagement, ContractManagement};
use crate::{BoundlessContract, BoundlessContractArgs, BoundlessContractClient};
Expand Down Expand Up @@ -64,11 +64,78 @@ impl CampaignManagement for BoundlessContract {
}

fn release_funds(env: Env, campaign_id: u64, milestone_id: u64) -> Result<(), BoundlessError> {
// TODO: campaign funds release logic
// - Get campaign and milestone
// - Validate milestone can be released
// - Update milestone status
// - Emit release event
// Get the campaign from storage
let campaign_key = crate::datatypes::DataKey::Campaign(campaign_id);
let mut campaign: Campaign = env
.storage()
.persistent()
.get(&campaign_key)
.ok_or(BoundlessError::CampaignNotFound)?;

// Validate campaign status allows fund release
match campaign.status {
Status::Failed => return Err(BoundlessError::InvalidOperation),
Status::Completed => return Err(BoundlessError::InvalidOperation),
_ => {} // Allow release for Pending and Active campaigns
}

// Find the milestone in the campaign's milestones list
let mut milestone_found = false;
let mut milestone_amount = 0i128;

for milestone in campaign.milestones.iter() {
if milestone.id == milestone_id {
milestone_found = true;
milestone_amount = milestone.amount;

// Validate milestone status allows release
match milestone.status {
MilestoneStatus::Approved => {
// Milestone can be released
}
MilestoneStatus::Released => {
return Err(BoundlessError::InvalidOperation); // Already released
}
MilestoneStatus::Rejected => {
return Err(BoundlessError::InvalidOperation); // Cannot release rejected milestone
}
MilestoneStatus::Pending => {
return Err(BoundlessError::InvalidOperation); // Must be approved first
}
}
break;
}
}

if !milestone_found {
return Err(BoundlessError::MilestoneNotFound);
}

// Update the milestone status to Released
let mut updated_milestones = Vec::new(&env);
for milestone in campaign.milestones.iter() {
if milestone.id == milestone_id {
let mut updated_milestone = milestone.clone();
updated_milestone.status = MilestoneStatus::Released;
updated_milestones.push_back(updated_milestone);
} else {
updated_milestones.push_back(milestone.clone());
}
}
campaign.milestones = updated_milestones;

// Store the updated campaign
env.storage().persistent().set(&campaign_key, &campaign);

// Emit the release event
FundsReleased {
campaign_id,
milestone_id,
amount: milestone_amount,
releaser: env.current_contract_address(),
}
.publish(&env);

Ok(())
}

Expand Down
6 changes: 4 additions & 2 deletions contracts/boundless/src/tests/admin.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
#![cfg(test)]

use crate::{datatypes::BoundlessError, BoundlessContract, BoundlessContractClient};
use crate::{BoundlessContract, BoundlessContractClient};
use soroban_sdk::{
testutils::{Address as _, MockAuth, MockAuthInvoke},
Address, BytesN, Env,
};
extern crate std;
mod boundless {
soroban_sdk::contractimport!(file = "../../target/wasm32v1-none/release/boundless.wasm");
soroban_sdk::contractimport!(
file = "../../target/wasm32-unknown-unknown/release/boundless.wasm"
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert this to "../../target/wasm32v1-none/release/boundless.wasm"); For Rust v1.84.0 or higher, you need wasm32v1-none target.

}
#[test]
fn test_initialize() {
Expand Down
43 changes: 21 additions & 22 deletions contracts/boundless/src/tests/campaign.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
#![cfg(test)]

use crate::{
datatypes::{Campaign, Status},
datatypes::{Campaign, EntityType, Milestone, MilestoneStatus, Status},
BoundlessContract, BoundlessContractClient,
};
use soroban_sdk::{
testutils::Address as _,
Address, Env, Symbol, Vec,
};
use soroban_sdk::{testutils::Address as _, Address, Env, Symbol, Vec};

extern crate std;
mod boundless {
soroban_sdk::contractimport!(file = "../../target/wasm32v1-none/release/boundless.wasm");
soroban_sdk::contractimport!(
file = "../../target/wasm32-unknown-unknown/release/boundless.wasm"
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert this to "../../target/wasm32v1-none/release/boundless.wasm"); For Rust v1.84.0 or higher, you need wasm32v1-none target.

}

#[test]
Expand All @@ -22,9 +21,9 @@ fn test_cancel_campaign_success() {
let admin = Address::generate(&env);
let contract_id = env.register(BoundlessContract, ());
let contract = BoundlessContractClient::new(&env, &contract_id);

contract.initialize(&admin);

let campaign_id = 1u64;
let owner: Address = Address::generate(&env);
let title = Symbol::new(&env, "TestCampaign");
Expand All @@ -33,7 +32,7 @@ fn test_cancel_campaign_success() {
let escrow_contract_id = Address::generate(&env);
let milestones = Vec::new(&env);
let backers = Vec::new(&env);

let campaign = Campaign {
id: campaign_id,
owner: owner.clone(),
Expand All @@ -50,9 +49,9 @@ fn test_cancel_campaign_success() {
env.as_contract(&contract_id, || {
env.storage().persistent().set(&campaign_key, &campaign);
});

contract.cancel_campaign(&campaign_id, &admin);

let updated_campaign: Campaign = env.as_contract(&contract_id, || {
env.storage().persistent().get(&campaign_key).unwrap()
});
Expand All @@ -69,7 +68,7 @@ fn test_cancel_campaign_unauthorized() {
let unauthorized_user = Address::generate(&env);
let contract_id = env.register(BoundlessContract, ());
let contract = BoundlessContractClient::new(&env, &contract_id);

contract.initialize(&admin);

let campaign_id = 1u64;
Expand All @@ -80,7 +79,7 @@ fn test_cancel_campaign_unauthorized() {
let escrow_contract_id = Address::generate(&env);
let milestones = Vec::new(&env);
let backers = Vec::new(&env);

let campaign = Campaign {
id: campaign_id,
owner: owner.clone(),
Expand Down Expand Up @@ -108,7 +107,7 @@ fn test_cancel_campaign_not_found() {
let admin = Address::generate(&env);
let contract_id = env.register(BoundlessContract, ());
let contract = BoundlessContractClient::new(&env, &contract_id);

contract.initialize(&admin);
contract.cancel_campaign(&999u64, &admin);
}
Expand All @@ -122,7 +121,7 @@ fn test_cancel_campaign_already_completed() {
let admin = Address::generate(&env);
let contract_id = env.register(BoundlessContract, ());
let contract = BoundlessContractClient::new(&env, &contract_id);

contract.initialize(&admin);

let campaign_id = 1u64;
Expand All @@ -133,7 +132,7 @@ fn test_cancel_campaign_already_completed() {
let escrow_contract_id = Address::generate(&env);
let milestones = Vec::new(&env);
let backers = Vec::new(&env);

let campaign = Campaign {
id: campaign_id,
owner: owner.clone(),
Expand All @@ -160,7 +159,7 @@ fn test_cancel_campaign_already_failed() {
let admin = Address::generate(&env);
let contract_id = env.register(BoundlessContract, ());
let contract = BoundlessContractClient::new(&env, &contract_id);

contract.initialize(&admin);

let campaign_id = 1u64;
Expand All @@ -171,7 +170,7 @@ fn test_cancel_campaign_already_failed() {
let escrow_contract_id = Address::generate(&env);
let milestones = Vec::new(&env);
let backers = Vec::new(&env);

let campaign = Campaign {
id: campaign_id,
owner: owner.clone(),
Expand All @@ -197,7 +196,7 @@ fn test_cancel_campaign_pending_status() {
let admin = Address::generate(&env);
let contract_id = env.register(BoundlessContract, ());
let contract = BoundlessContractClient::new(&env, &contract_id);

contract.initialize(&admin);

let campaign_id = 1u64;
Expand All @@ -208,7 +207,7 @@ fn test_cancel_campaign_pending_status() {
let escrow_contract_id = Address::generate(&env);
let milestones = Vec::new(&env);
let backers = Vec::new(&env);

let campaign = Campaign {
id: campaign_id,
owner: owner.clone(),
Expand All @@ -225,9 +224,9 @@ fn test_cancel_campaign_pending_status() {
env.as_contract(&contract_id, || {
env.storage().persistent().set(&campaign_key, &campaign);
});

contract.cancel_campaign(&campaign_id, &admin);

let updated_campaign: Campaign = env.as_contract(&contract_id, || {
env.storage().persistent().get(&campaign_key).unwrap()
});
Expand Down
Loading
Loading