diff --git a/contracts/crowdfunding/src/lib.rs b/contracts/crowdfunding/src/lib.rs index 67e225997..0e68a7d18 100644 --- a/contracts/crowdfunding/src/lib.rs +++ b/contracts/crowdfunding/src/lib.rs @@ -45,13 +45,8 @@ mod propchain_crowdfunding { } #[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - scale::Encode, - scale::Decode, + Debug, Clone, Copy, PartialEq, Eq, + scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, )] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] @@ -64,13 +59,8 @@ mod propchain_crowdfunding { } #[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - scale::Encode, - scale::Decode, + Debug, Clone, Copy, PartialEq, Eq, + scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, )] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] @@ -81,13 +71,8 @@ mod propchain_crowdfunding { } #[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - scale::Encode, - scale::Decode, + Debug, Clone, Copy, PartialEq, Eq, + scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, )] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] @@ -98,13 +83,8 @@ mod propchain_crowdfunding { } #[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - scale::Encode, - scale::Decode, + Debug, Clone, Copy, PartialEq, Eq, + scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, )] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] @@ -115,13 +95,8 @@ mod propchain_crowdfunding { } #[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - scale::Encode, - scale::Decode, + Debug, Clone, Copy, PartialEq, Eq, + scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, )] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] @@ -133,7 +108,9 @@ mod propchain_crowdfunding { } #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, + Debug, Clone, PartialEq, + scale::Encode, scale::Decode, + ink::storage::traits::StorageLayout, )] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] pub struct Campaign { @@ -147,7 +124,9 @@ mod propchain_crowdfunding { } #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, + Debug, Clone, PartialEq, + scale::Encode, scale::Decode, + ink::storage::traits::StorageLayout, )] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] pub struct InvestorProfile { @@ -158,7 +137,9 @@ mod propchain_crowdfunding { } #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, + Debug, Clone, PartialEq, + scale::Encode, scale::Decode, + ink::storage::traits::StorageLayout, )] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] pub struct Milestone { @@ -172,7 +153,9 @@ mod propchain_crowdfunding { } #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, + Debug, Clone, PartialEq, + scale::Encode, scale::Decode, + ink::storage::traits::StorageLayout, )] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] pub struct Proposal { @@ -185,7 +168,9 @@ mod propchain_crowdfunding { } #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, + Debug, Clone, PartialEq, + scale::Encode, scale::Decode, + ink::storage::traits::StorageLayout, )] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] pub struct ShareListing { @@ -197,7 +182,9 @@ mod propchain_crowdfunding { } #[derive( - Debug, Clone, PartialEq, scale::Encode, scale::Decode, ink::storage::traits::StorageLayout, + Debug, Clone, PartialEq, + scale::Encode, scale::Decode, + ink::storage::traits::StorageLayout, )] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] pub struct RiskProfile { @@ -228,6 +215,7 @@ mod propchain_crowdfunding { admin: AccountId, campaigns: Mapping, campaign_count: u64, + campaign_ids: Vec, // index for iteration investor_profiles: Mapping, investments: Mapping<(u64, AccountId), u128>, milestones: Mapping, @@ -251,6 +239,8 @@ mod propchain_crowdfunding { refunds_issued: Mapping<(u64, AccountId), bool>, } + // ── Events ─────────────────────────────────────────────── + #[ink(event)] pub struct CampaignCreated { #[ink(topic)] @@ -325,6 +315,7 @@ mod propchain_crowdfunding { admin, campaigns: Mapping::default(), campaign_count: 0, + campaign_ids: Vec::new(), investor_profiles: Mapping::default(), investments: Mapping::default(), milestones: Mapping::default(), @@ -347,6 +338,8 @@ mod propchain_crowdfunding { } } + // ── Core Campaign Messages ─────────────────────────── + #[ink(message)] pub fn create_campaign( &mut self, @@ -364,6 +357,7 @@ mod propchain_crowdfunding { investor_count: 0, }; self.campaigns.insert(self.campaign_count, &campaign); + self.campaign_ids.push(self.campaign_count); self.env().emit_event(CampaignCreated { campaign_id: self.campaign_count, creator: self.env().caller(), @@ -461,8 +455,7 @@ mod propchain_crowdfunding { if current == 0 { campaign.investor_count += 1; } - self.investments - .insert((campaign_id, caller), &(current + amount)); + self.investments.insert((campaign_id, caller), &(current + amount)); campaign.raised_amount += amount; if campaign.raised_amount >= campaign.target_amount { campaign.status = CampaignStatus::Funded; @@ -470,8 +463,7 @@ mod propchain_crowdfunding { self.campaigns.insert(campaign_id, &campaign); let shares = (amount / 1000) as u64; let current_shares = self.share_holdings.get((campaign_id, caller)).unwrap_or(0); - self.share_holdings - .insert((campaign_id, caller), &(current_shares + shares)); + self.share_holdings.insert((campaign_id, caller), &(current_shares + shares)); self.env().emit_event(InvestmentMade { campaign_id, investor: caller, @@ -800,10 +792,8 @@ mod propchain_crowdfunding { .share_holdings .get((listing.campaign_id, buyer)) .unwrap_or(0); - self.share_holdings.insert( - (listing.campaign_id, buyer), - &(buyer_shares + listing.shares), - ); + self.share_holdings + .insert((listing.campaign_id, buyer), &(buyer_shares + listing.shares)); self.listings.remove(listing_id); Ok(total_cost) } @@ -837,6 +827,8 @@ mod propchain_crowdfunding { Ok(()) } + // ── Basic Getters ──────────────────────────────────── + #[ink(message)] pub fn get_campaign(&self, campaign_id: u64) -> Option { self.campaigns.get(campaign_id) @@ -898,15 +890,183 @@ mod propchain_crowdfunding { #[ink(message)] pub fn get_shares(&self, campaign_id: u64, investor: AccountId) -> u64 { - self.share_holdings - .get((campaign_id, investor)) - .unwrap_or(0) + self.share_holdings.get((campaign_id, investor)).unwrap_or(0) } #[ink(message)] pub fn get_admin(&self) -> AccountId { self.admin } + + // ── Search & Discovery ─────────────────────────────── + + fn campaign_to_summary(&self, campaign: &Campaign) -> CampaignSummary { + let funded_pct = if campaign.target_amount == 0 { + 0u32 + } else { + ((campaign.raised_amount * 100) / campaign.target_amount) as u32 + }; + let risk_rating = self + .risk_profiles + .get(campaign.campaign_id) + .map(|r| r.rating) + .unwrap_or(RiskRating::Unrated); + CampaignSummary { + campaign_id: campaign.campaign_id, + creator: campaign.creator, + title: campaign.title.clone(), + target_amount: campaign.target_amount, + raised_amount: campaign.raised_amount, + funded_pct, + status: campaign.status, + investor_count: campaign.investor_count, + risk_rating, + } + } + + fn matches_filter(summary: &CampaignSummary, filter: &CampaignFilter) -> bool { + if let Some(ref status) = filter.status { + if &summary.status != status { + return false; + } + } + if let Some(ref keyword) = filter.title_keyword { + if !summary.title.to_lowercase().contains(&keyword.to_lowercase()) { + return false; + } + } + if let Some(min) = filter.min_target { + if summary.target_amount < min { + return false; + } + } + if let Some(max) = filter.max_target { + if summary.target_amount > max { + return false; + } + } + if let Some(min_pct) = filter.min_funded_pct { + if summary.funded_pct < min_pct { + return false; + } + } + if filter.funded_only && summary.status != CampaignStatus::Funded { + return false; + } + true + } + + /// Browse all campaigns page by page. `page` is 0-indexed, max page_size is 50. + #[ink(message)] + pub fn get_campaigns_paginated(&self, page: u64, page_size: u64) -> Vec { + let page_size = page_size.min(50); + let start = (page * page_size) as usize; + self.campaign_ids + .iter() + .skip(start) + .take(page_size as usize) + .filter_map(|id| self.campaigns.get(*id)) + .map(|c| self.campaign_to_summary(&c)) + .collect() + } + + /// Filter campaigns by status, title keyword, amount range, or funded %. + /// Returns up to `limit` results (max 50). + #[ink(message)] + pub fn search_campaigns(&self, filter: CampaignFilter, limit: u64) -> Vec { + let limit = limit.min(50) as usize; + self.campaign_ids + .iter() + .filter_map(|id| self.campaigns.get(*id)) + .map(|c| self.campaign_to_summary(&c)) + .filter(|s| Self::matches_filter(s, &filter)) + .take(limit) + .collect() + } + + /// All campaigns created by a specific account. + #[ink(message)] + pub fn get_campaigns_by_creator(&self, creator: AccountId) -> Vec { + self.campaign_ids + .iter() + .filter_map(|id| self.campaigns.get(*id)) + .filter(|c| c.creator == creator) + .map(|c| self.campaign_to_summary(&c)) + .collect() + } + + /// Top N campaigns sorted by raised_amount descending (trending / most funded). + #[ink(message)] + pub fn get_top_campaigns(&self, n: u64) -> Vec { + let n = n.min(50) as usize; + let mut summaries: Vec = self.campaign_ids + .iter() + .filter_map(|id| self.campaigns.get(*id)) + .map(|c| self.campaign_to_summary(&c)) + .collect(); + summaries.sort_by(|a, b| b.raised_amount.cmp(&a.raised_amount)); + summaries.into_iter().take(n).collect() + } + + /// All campaigns matching a specific risk rating. + #[ink(message)] + pub fn get_campaigns_by_risk(&self, rating: RiskRating) -> Vec { + self.campaign_ids + .iter() + .filter_map(|id| self.campaigns.get(*id)) + .map(|c| self.campaign_to_summary(&c)) + .filter(|s| s.risk_rating == rating) + .collect() + } + + /// Active campaigns at or above `threshold_pct`% funded. Good for "closing soon". + #[ink(message)] + pub fn get_near_funded_campaigns(&self, threshold_pct: u32) -> Vec { + self.campaign_ids + .iter() + .filter_map(|id| self.campaigns.get(*id)) + .map(|c| self.campaign_to_summary(&c)) + .filter(|s| s.status == CampaignStatus::Active && s.funded_pct >= threshold_pct) + .collect() + } + + /// Campaign counts by status: (draft, active, funded, closed, cancelled). + #[ink(message)] + pub fn get_campaign_stats(&self) -> (u64, u64, u64, u64, u64) { + let (mut draft, mut active, mut funded, mut closed, mut cancelled) = + (0u64, 0u64, 0u64, 0u64, 0u64); + for id in self.campaign_ids.iter() { + if let Some(c) = self.campaigns.get(*id) { + match c.status { + CampaignStatus::Draft => draft += 1, + CampaignStatus::Active => active += 1, + CampaignStatus::Funded => funded += 1, + CampaignStatus::Closed => closed += 1, + CampaignStatus::Cancelled => cancelled += 1, + } + } + } + (draft, active, funded, closed, cancelled) + } + + /// All campaigns an investor has contributed to. + #[ink(message)] + pub fn get_investor_campaigns(&self, investor: AccountId) -> Vec { + self.campaign_ids + .iter() + .filter_map(|id| { + let invested = self.investments.get((*id, investor)).unwrap_or(0); + if invested > 0 { self.campaigns.get(*id) } else { None } + }) + .map(|c| self.campaign_to_summary(&c)) + .collect() + } + + /// Total number of campaigns ever created. + #[ink(message)] + pub fn get_campaign_count(&self) -> u64 { + self.campaign_count + } } impl Default for RealEstateCrowdfunding { @@ -922,7 +1082,9 @@ pub use crate::propchain_crowdfunding::{CrowdfundingError, RealEstateCrowdfundin mod tests { use super::*; use ink::env::{test, DefaultEnvironment}; - use propchain_crowdfunding::{CampaignStatus, CrowdfundingError, RealEstateCrowdfunding}; + use propchain_crowdfunding::{ + CampaignFilter, CampaignStatus, CrowdfundingError, RiskRating, RealEstateCrowdfunding, + }; fn setup() -> RealEstateCrowdfunding { let accounts = test::default_accounts::(); @@ -930,6 +1092,8 @@ mod tests { RealEstateCrowdfunding::new(accounts.alice) } + // ── Original tests ─────────────────────────────────────── + #[ink::test] fn test_create_campaign() { let mut contract = setup();