diff --git a/README.md b/README.md index b57a4a4f..67ddcad5 100644 --- a/README.md +++ b/README.md @@ -167,10 +167,14 @@ Found a bug? Have a feature request? Open an **issue** on GitHub: ## 10. Community & Support ### Join Our Channels: -- **Discord:** [Join Here](https://discord.gg/juUmBmwC3s) +* [Join Here](https://discord.gg/juUmBmwC3s) - **GitHub Discussions:** [Boundless Discussions](https://github.com/0xdevcollins/boundless/discussions) - **Twitter:** [Follow @boundless_fi](https://x.com/boundless_fi) +### Support +- **Benjamin:** [https://t.me/kitch_the_dev](https://t.me/kitch_the_dev) +- **Collins:** [https://t.me/devcollinss](https://t.me/devcollinss) + --- Thank you for contributing to Boundless! 🚀 diff --git a/contracts/project_contract/src/create_project/create_project.rs b/contracts/project_contract/src/create_project/create_project.rs index ba0687d2..bc97ab12 100644 --- a/contracts/project_contract/src/create_project/create_project.rs +++ b/contracts/project_contract/src/create_project/create_project.rs @@ -1,19 +1,98 @@ -use soroban_sdk::{contract, contractimpl, Address, Env, String}; -use crate::project::Project; - use super::{CreateProjectEvent, CreateProjectStorage}; - +use crate::project::Project; +use soroban_sdk::{contract, contractimpl, Address, Env, String}; #[contract] pub struct CreateProject; #[contractimpl] impl CreateProject { - pub fn create_project(env: Env, project_id: String, creator: Address, metadata_uri: String, funding_target: u64, milestone_count: u32) { - let project = Project::new(&env, project_id.clone(), creator.clone(), metadata_uri, funding_target, milestone_count); - + pub fn create_project( + env: Env, + project_id: String, + creator: Address, + metadata_uri: String, + funding_target: u64, + milestone_count: u32, + ) { + if CreateProjectStorage::project_exists(&env, &project_id) { + panic!("Project ID already exists"); + } + if funding_target == 0 { + panic!("Funding target must be greater than zero"); + } + let project = Project::new( + &env, + project_id.clone(), + creator.clone(), + metadata_uri, + funding_target, + milestone_count, + ); + CreateProjectStorage::save(&env, &project); CreateProjectEvent::emit(&env, project_id, creator); } + + pub fn get_project(env: Env, project_id: String) -> Option { + CreateProjectStorage::fetch_project(&env, &project_id) + } + pub fn update_project_metadata( + env: Env, + project_id: String, + creator: Address, + new_metadata_uri: String, + ) { + let mut project = + CreateProjectStorage::fetch_project(&env, &project_id).expect("Project does not exist"); + if project.creator != creator { + panic!("Only the project creator can modify metadata"); + } + + if project.is_closed { + panic!("Cannot modify a closed project"); + } + project.metadata_uri = new_metadata_uri; + + CreateProjectStorage::save(&env, &project); + } + + pub fn modify_milestone( + env: Env, + project_id: String, + caller: Address, + new_milestone_count: u32, + ) { + let mut project = + CreateProjectStorage::fetch_project(&env, &project_id).expect("Project does not exist"); + + if project.creator != caller { + panic!("Only the project creator can modify milestones"); + } + + if project.is_closed { + panic!("Cannot modify milestones for a closed project"); + } + + project.milestone_count = new_milestone_count; + + CreateProjectStorage::save(&env, &project); + } + + pub fn close_project(env: Env, project_id: String, creator: Address) { + let mut project = + CreateProjectStorage::fetch_project(&env, &project_id).expect("Project does not exist"); + + if project.creator != creator { + panic!("Only the project creator can close the project"); + } + + if project.is_closed { + panic!("Project is already closed"); + } + project.is_closed = true; + + CreateProjectStorage::save(&env, &project); + } } diff --git a/contracts/project_contract/src/create_project/create_project_event.rs b/contracts/project_contract/src/create_project/create_project_event.rs index 38f11f13..a06d496b 100644 --- a/contracts/project_contract/src/create_project/create_project_event.rs +++ b/contracts/project_contract/src/create_project/create_project_event.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{Address, Env, String, Symbol}; +use soroban_sdk::{Env, Address, String, Symbol}; pub struct CreateProjectEvent; diff --git a/contracts/project_contract/src/create_project/create_project_storage.rs b/contracts/project_contract/src/create_project/create_project_storage.rs index d3ac5d95..356c3f30 100644 --- a/contracts/project_contract/src/create_project/create_project_storage.rs +++ b/contracts/project_contract/src/create_project/create_project_storage.rs @@ -1,14 +1,40 @@ -use soroban_sdk::{Env, String}; use crate::project::Project; +use soroban_sdk::{symbol_short, Env, Map, String}; pub struct CreateProjectStorage; impl CreateProjectStorage { pub fn save(env: &Env, project: &Project) { - env.storage().instance().set(&project.project_id, project); + let mut projects: Map = env + .storage() + .persistent() + .get(&symbol_short!("projects")) + .unwrap_or(Map::new(&env)); + + projects.set(project.project_id.clone(), project.clone()); + + env.storage() + .persistent() + .set(&symbol_short!("projects"), &projects); } - - pub fn get(env: &Env, project_id: &String) -> Option { - env.storage().instance().get(project_id) + + pub fn fetch_project(env: &Env, project_id: &String) -> Option { + let projects: Map = env + .storage() + .persistent() + .get(&symbol_short!("projects")) + .unwrap_or(Map::new(&env)); + + projects.get(project_id.clone()) + } + + pub fn project_exists(env: &Env, project_id: &String) -> bool { + let projects: Map = env + .storage() + .persistent() + .get(&symbol_short!("projects")) + .unwrap_or(Map::new(&env)); + + projects.contains_key(project_id.clone()) } } diff --git a/contracts/project_contract/src/lib.rs b/contracts/project_contract/src/lib.rs index 0f29b803..33b12c55 100644 --- a/contracts/project_contract/src/lib.rs +++ b/contracts/project_contract/src/lib.rs @@ -2,7 +2,7 @@ mod project; mod create_project; -// mod vote_project; +// pub use crate::vote_project:: // mod fund_project; // mod release_milestone; // mod refund; diff --git a/contracts/project_contract/src/tests/create_project_test.rs b/contracts/project_contract/src/tests/create_project_test.rs index db57c58b..4a03354e 100644 --- a/contracts/project_contract/src/tests/create_project_test.rs +++ b/contracts/project_contract/src/tests/create_project_test.rs @@ -1,34 +1,182 @@ -#[cfg(test)] -mod tests { - use soroban_sdk::{Env, Address, String}; - use crate::create_project::{CreateProjectStorage, CreateProject, CreateProjectClient}; - - #[test] - fn test_create_project() { - let env = Env::default(); - let contract_id = env.register(CreateProject, ()); - let client = CreateProjectClient::new(&env, &contract_id); - - let creator_key = String::from_str(&env, "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"); - let creator = Address::from_string(&creator_key); - let project_id = String::from_str(&env, "project-1"); - let meta_uri = String::from_str(&env, "ipfs://QmExample"); - - client.create_project(&project_id, &creator, &meta_uri, &10000, &5); - - let project = env.as_contract(&contract_id, || { - CreateProjectStorage::get(&env, &project_id).expect("Project not found") - }); - - assert_eq!(project.project_id, project_id); - assert_eq!(project.creator, creator); - assert_eq!(project.metadata_uri, meta_uri); - assert_eq!(project.funding_target, 10000); - assert_eq!(project.milestone_count, 5); - assert_eq!(project.total_funded, 0); - assert_eq!(project.current_milestone, 0); - assert_eq!(project.validated, false); - assert_eq!(project.is_successful, false); - assert_eq!(project.is_closed, false); - } +#![cfg(test)] + +use crate::create_project::{CreateProject, CreateProjectClient, CreateProjectStorage}; +use soroban_sdk::{testutils::Address, Env, String}; + +#[test] +fn test_create_project_success() { + let env = Env::default(); + let contract_id = env.register(CreateProject, ()); + let client = CreateProjectClient::new(&env, &contract_id); + + // let creator_key = String::from_str( + // &env, + // "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", + // ); + let creator = ::generate(&env); + let project_id = String::from_str(&env, "test_project"); + let metadata_uri = String::from_str(&env, "ipfs://example-metadata"); + let funding_target = 1000; + let milestone_count = 5; + + client.create_project( + &project_id, + &creator, + &metadata_uri, + &funding_target, + &milestone_count, + ); + + let project = env.as_contract(&contract_id, || { + CreateProjectStorage::fetch_project(&env, &project_id).expect("Project not found") + }); + + assert_eq!(project.project_id, project_id); + assert_eq!(project.creator, creator); + assert_eq!(project.metadata_uri, metadata_uri); + assert_eq!(project.funding_target, funding_target); + assert_eq!(project.milestone_count, milestone_count); + assert!(!project.is_closed); +} + +#[test] +#[should_panic(expected = "Project ID already exists")] +fn test_create_project_duplicate_id_fails() { + let env = Env::default(); + let contract_id = env.register(CreateProject, ()); + let client = CreateProjectClient::new(&env, &contract_id); + + let creator = ::generate(&env); + let project_id = String::from_str(&env, "test_project"); + let metadata_uri = String::from_str(&env, "ipfs://example-metadata"); + + client.create_project(&project_id, &creator, &metadata_uri, &1000, &5); + client.create_project(&project_id, &creator, &metadata_uri, &2000, &3); +} + +#[test] +#[should_panic(expected = "Funding target must be greater than zero")] +fn test_create_project_zero_funding_target_fails() { + let env = Env::default(); + let contract_id = env.register(CreateProject, ()); + let client = CreateProjectClient::new(&env, &contract_id); + + let creator = ::generate(&env); + let project_id = String::from_str(&env, "test_project"); + let metadata_uri = String::from_str(&env, "ipfs://example-metadata"); + + client.create_project(&project_id, &creator, &metadata_uri, &0, &3); +} + +#[test] +fn test_update_project_metadata_success() { + let env = Env::default(); + let contract_id = env.register(CreateProject, ()); + let client = CreateProjectClient::new(&env, &contract_id); + + let creator = ::generate(&env); + let project_id = String::from_str(&env, "test_project"); + let metadata_uri = String::from_str(&env, "ipfs://example-metadata"); + let new_metadata_uri = String::from_str(&env, "ipfs://new-metadata"); + + client.create_project(&project_id, &creator, &metadata_uri, &1000, &5); + client.update_project_metadata(&project_id, &creator, &new_metadata_uri); + + let project = env.as_contract(&contract_id, || { + CreateProjectStorage::fetch_project(&env, &project_id).expect("Project not found") + }); + + assert_eq!(project.metadata_uri, new_metadata_uri); +} + +#[test] +#[should_panic(expected = "Only the project creator can modify metadata")] +fn test_update_project_metadata_wrong_creator_fails() { + let env = Env::default(); + let contract_id = env.register(CreateProject, ()); + let client = CreateProjectClient::new(&env, &contract_id); + + let creator = ::generate(&env); + let other_user = ::generate(&env); + let project_id = String::from_str(&env, "test_project"); + let metadata_uri = String::from_str(&env, "ipfs://example-metadata"); + + client.create_project(&project_id, &creator, &metadata_uri, &1000, &5); + client.update_project_metadata( + &project_id, + &other_user, + &String::from_str(&env, "ipfs://new-metadata"), + ); +} + +#[test] +fn test_modify_milestone_success() { + let env = Env::default(); + let contract_id = env.register(CreateProject, ()); + let client = CreateProjectClient::new(&env, &contract_id); + + let creator = ::generate(&env); + let project_id = String::from_str(&env, "test_project"); + let metadata_uri = String::from_str(&env, "ipfs://example-metadata"); + + client.create_project(&project_id, &creator, &metadata_uri, &1000, &5); + client.modify_milestone(&project_id, &creator, &10); + + let project = env.as_contract(&contract_id, || { + CreateProjectStorage::fetch_project(&env, &project_id).expect("Project not found") + }); + + assert_eq!(project.milestone_count, 10); +} + +#[test] +#[should_panic(expected = "Only the project creator can modify milestones")] +fn test_modify_milestone_wrong_caller_fails() { + let env = Env::default(); + let contract_id = env.register(CreateProject, ()); + let client = CreateProjectClient::new(&env, &contract_id); + + let creator = ::generate(&env); + let other_user = ::generate(&env); + let project_id = String::from_str(&env, "test_project"); + let metadata_uri = String::from_str(&env, "ipfs://example-metadata"); + + client.create_project(&project_id, &creator, &metadata_uri, &1000, &5); + client.modify_milestone(&project_id, &other_user, &10); +} + +#[test] +fn test_close_project_success() { + let env = Env::default(); + let contract_id = env.register(CreateProject, ()); + let client = CreateProjectClient::new(&env, &contract_id); + + let creator = ::generate(&env); + let project_id = String::from_str(&env, "test_project"); + let metadata_uri = String::from_str(&env, "ipfs://example-metadata"); + + client.create_project(&project_id, &creator, &metadata_uri, &1000, &5); + client.close_project(&project_id, &creator); + + let project = env.as_contract(&contract_id, || { + CreateProjectStorage::fetch_project(&env, &project_id).expect("Project not found") + }); + + assert!(project.is_closed); +} + +#[test] +#[should_panic(expected = "Only the project creator can close the project")] +fn test_close_project_wrong_caller_fails() { + let env = Env::default(); + let contract_id = env.register(CreateProject, ()); + let client = CreateProjectClient::new(&env, &contract_id); + + let creator = ::generate(&env); + let other_user = ::generate(&env); + let project_id = String::from_str(&env, "test_project"); + let metadata_uri = String::from_str(&env, "ipfs://example-metadata"); + + client.create_project(&project_id, &creator, &metadata_uri, &1000, &5); + client.close_project(&project_id, &other_user); } diff --git a/contracts/project_contract/src/vote_project/mod.rs b/contracts/project_contract/src/vote_project/mod.rs new file mode 100644 index 00000000..56c9e21d --- /dev/null +++ b/contracts/project_contract/src/vote_project/mod.rs @@ -0,0 +1,4 @@ +pub mod vote_project; +pub mod vote_project_storage; +pub mod vote_project_event; + diff --git a/contracts/project_contract/src/vote_project/vote_project.rs b/contracts/project_contract/src/vote_project/vote_project.rs new file mode 100644 index 00000000..9c6d0f15 --- /dev/null +++ b/contracts/project_contract/src/vote_project/vote_project.rs @@ -0,0 +1,28 @@ +use soroban_sdk::{contract, contractimpl, Address, Env, String}; + +use super::{vote_project_event, vote_project_storage}; + +// use super::vote_project_event; + +#[contract] +pub struct VotingContract; + +#[contractimpl] +impl VotingContract { + pub fn vote_project( + env: Env, + project_id: String, + voter: Address, + vote_value: i32, + weight: u64, + ) { + vote_project_storage::store_vote( + env.clone(), + project_id.clone(), + voter.clone(), + vote_value, + weight, + ); + vote_project_event::emit_vote_event(env, project_id, voter, vote_value, weight); + } +} diff --git a/contracts/project_contract/src/vote_project/vote_project_event.rs b/contracts/project_contract/src/vote_project/vote_project_event.rs new file mode 100644 index 00000000..dc9aa349 --- /dev/null +++ b/contracts/project_contract/src/vote_project/vote_project_event.rs @@ -0,0 +1,12 @@ +use soroban_sdk::{log, Address, Env, String}; + +pub fn emit_vote_event(env: Env, project_id: String, voter: Address, vote_value: i32, weight: u64) { + log!( + &env, + "Vote Cast: Project ID {}, Voter {}, Vote Value {}, Weight {}", + project_id, + voter, + vote_value, + weight + ); +} diff --git a/contracts/project_contract/src/vote_project/vote_project_storage.rs b/contracts/project_contract/src/vote_project/vote_project_storage.rs new file mode 100644 index 00000000..52c05076 --- /dev/null +++ b/contracts/project_contract/src/vote_project/vote_project_storage.rs @@ -0,0 +1,19 @@ +use soroban_sdk::{symbol_short, Address, Env, Map, String}; + +pub fn store_vote(env: Env, project_id: String, voter: Address, vote_value: i32, weight: u64) { + let key = (project_id.clone(), voter.clone()); + let mut votes: Map<(String, Address), (i32, u64)> = env + .storage() + .persistent() + .get(&symbol_short!("votes")) + .unwrap_or(Map::new(&env)); + + if votes.contains_key(key) { + panic!("User has already voted on this project"); + } + + let key = (project_id, voter); + votes.set(key, (vote_value, weight)); + env.storage().persistent().set(&symbol_short!("votes"), &votes); +} +