From c84245129688a7d768e0b8c412358f38cfc765d3 Mon Sep 17 00:00:00 2001 From: wheval Date: Sun, 31 Aug 2025 02:09:18 +0100 Subject: [PATCH 1/2] feat: implement emergency system, autoschedule system, penalty system --- src/base/errors.cairo | 129 ++ src/base/events.cairo | 83 + src/base/types.cairo | 89 + src/component/analytics.cairo | 367 +++ src/component/auto_schedule.cairo | 387 ++++ src/component/contribution/contribution.cairo | 320 ++- src/component/emergency.cairo | 204 ++ src/component/member_profile.cairo | 398 ++++ src/component/payment_flexibility.cairo | 619 +++++ src/component/penalty.cairo | 446 ++++ src/interfaces/IStarkRemit.cairo | 47 + src/lib.cairo | 6 + src/starkremit/StarkRemit.cairo | 2029 ++++++++++++++++- tests/test_analytics_component.cairo | 197 ++ tests/test_auto_schedule_component.cairo | 241 ++ tests/test_emergency_component.cairo | 277 +++ tests/test_integration.cairo | 226 ++ tests/test_penalty_component.cairo | 192 ++ 18 files changed, 6126 insertions(+), 131 deletions(-) create mode 100644 src/component/analytics.cairo create mode 100644 src/component/auto_schedule.cairo create mode 100644 src/component/emergency.cairo create mode 100644 src/component/member_profile.cairo create mode 100644 src/component/payment_flexibility.cairo create mode 100644 src/component/penalty.cairo create mode 100644 tests/test_analytics_component.cairo create mode 100644 tests/test_auto_schedule_component.cairo create mode 100644 tests/test_emergency_component.cairo create mode 100644 tests/test_integration.cairo create mode 100644 tests/test_penalty_component.cairo diff --git a/src/base/errors.cairo b/src/base/errors.cairo index b052204..0ab5801 100644 --- a/src/base/errors.cairo +++ b/src/base/errors.cairo @@ -316,3 +316,132 @@ pub mod GovernanceErrors { /// Error triggered when zero address is provided pub const ZERO_ADDRESS: felt252 = 'GOV: invalid contract'; } + +pub mod EmergencyComponentErrors { + /// Error triggered when caller is not an Admin or higher + pub const NOT_ADMIN: felt252 = 'Emergency: not admin'; + /// Error triggered when contract is paused + pub const CONTRACT_PAUSED: felt252 = 'Emergency: contract is paused'; + /// Error triggered when contract is not paused + pub const CONTRACT_NOT_PAUSED: felt252 = 'Emergency: contract not paused'; + /// Error triggered when member is already banned + pub const MEMBER_ALREADY_BANNED: felt252 = 'Emergency: member is banned'; + /// Error triggered when member is not banned + pub const MEMBER_NOT_BANNED: felt252 = 'Emergency: member is not banned'; + /// Error triggered when contract is already paused + pub const ALREADY_PAUSED: felt252 = 'Emergency: contract is paused'; +} + +pub mod EmergencyErrors { + /// Error triggered when no funds available for withdrawal + pub const NO_FUNDS_TO_WITHDRAW: felt252 = 'Emergency: no funds'; + /// Error triggered when member does not exist + pub const MEMBER_NOT_EXISTS: felt252 = 'Emergency: member not found'; + /// Error triggered when member has no contributions + pub const MEMBER_NO_CONTRIBUTIONS: felt252 = 'Emergency: no contributions'; + /// Error triggered when round is not active + pub const ROUND_NOT_ACTIVE: felt252 = 'Emergency: round inactive'; + /// Error triggered when recipient address is invalid + pub const INVALID_RECIPIENT: felt252 = 'Emergency: invalid recipient'; + /// Error triggered when new recipient is not a member + pub const RECIPIENT_NOT_MEMBER: felt252 = 'Emergency: not member'; + /// Error triggered when recipient is already set + pub const RECIPIENT_ALREADY_SET: felt252 = 'Emergency: recipient set'; + /// Error triggered when token address is invalid + pub const INVALID_TOKEN_ADDRESS: felt252 = 'Emergency: invalid token'; + /// Error triggered when amount is invalid + pub const INVALID_AMOUNT: felt252 = 'Emergency: invalid amount'; + /// Error triggered when balance is insufficient + pub const INSUFFICIENT_BALANCE: felt252 = 'Emergency: insufficient'; + /// Error triggered when contract address is invalid + pub const INVALID_CONTRACT_ADDRESS: felt252 = 'Emergency: invalid contract'; + /// Error triggered when trying to migrate to self + pub const CANNOT_MIGRATE_TO_SELF: felt252 = 'Emergency: migrate to self'; + /// Error triggered when migration target is invalid + pub const INVALID_MIGRATION_TARGET: felt252 = 'Emergency: invalid target'; + /// Error triggered when no funds to migrate + pub const NO_FUNDS_TO_MIGRATE: felt252 = 'Emergency: no funds'; +} + +pub mod PenaltyComponentErrors { + /// Error triggered when caller is not an Admin or higher + pub const NOT_ADMIN: felt252 = 'Penalty: not admin'; + /// Error triggered when contract is paused + pub const CONTRACT_PAUSED: felt252 = 'Penalty: contract is paused'; + /// Error triggered when contract is not paused + pub const CONTRACT_NOT_PAUSED: felt252 = 'Penalty: contract not paused'; + /// Error triggered when penalty pool is disabled + pub const PENALTY_POOL_DISABLED: felt252 = 'Penalty: penalty pool disabled'; + /// Error triggered when no contribution for round + pub const NO_CONTRIBUTION_FOR_ROUND: felt252 = 'Penalty: no contribution'; + /// Error triggered when not late + pub const NOT_LATE: felt252 = 'Penalty: not late'; +} + +pub mod AutoScheduleErrors { + /// Error triggered when caller is not an Admin or higher + pub const NOT_ADMIN: felt252 = 'AutoSchedule: not admin'; + /// Error triggered when contract is paused + pub const CONTRACT_PAUSED: felt252 = 'AutoSchedule: paused'; + /// Error triggered when contract is not paused + pub const CONTRACT_NOT_PAUSED: felt252 = 'AutoSchedule: not paused'; + /// Error triggered when new deadline is not in the future + pub const NEW_DEADLINE_NOT_IN_FUTURE: felt252 = 'AutoSchedule: not future'; + /// Error triggered when new deadline is not after start + pub const NEW_DEADLINE_NOT_AFTER_START: felt252 = 'AutoSchedule: not after start'; + +} + +pub mod ContributionErrors { + /// Error triggered when caller is not a member + pub const NOT_MEMBER: felt252 = 'Contribution: not member'; + /// Error triggered when member already contributed + pub const ALREADY_CONTRIBUTED: felt252 = 'Contribution: contributed'; + /// Error triggered when round is not active + pub const ROUND_NOT_ACTIVE: felt252 = 'Contribution: not active'; + /// Error triggered when contribution deadline passed + pub const CONTRIBUTION_DEADLINE_PASSED: felt252 = 'Contribution: deadline passed'; + /// Error triggered when contribution amount is insufficient + pub const INSUFFICIENT_AMOUNT: felt252 = 'Contribution: insufficient'; + /// Error triggered when contribution limit exceeded + pub const CONTRIBUTION_LIMIT_EXCEEDED: felt252 = 'Contribution: limit exceeded'; + /// Error triggered when recipient is not a member + pub const RECIPIENT_NOT_MEMBER: felt252 = 'Contribution: not member'; + /// Error triggered when deadline is not in future + pub const DEADLINE_NOT_IN_FUTURE: felt252 = 'Contribution: not future'; + /// Error triggered when round deadline not passed + pub const ROUND_DEADLINE_NOT_PASSED: felt252 = 'Contribution: not deadline '; + /// Error triggered when round not completed + pub const ROUND_NOT_COMPLETED: felt252 = 'Contribution: not completed'; + /// Error triggered when address is invalid + pub const INVALID_ADDRESS: felt252 = 'Contribution: invalid'; + /// Error triggered when already a member + pub const ALREADY_MEMBER: felt252 = 'Contribution: already member'; + /// Error triggered when caller is not owner + pub const NOT_OWNER: felt252 = 'Contribution: not owner'; +} + +pub mod PaymentFlexibilityErrors { + /// Error triggered when caller is not an Admin or higher + pub const NOT_ADMIN: felt252 = 'Payment: not admin'; + /// Error triggered when auto-payment is disabled + pub const AUTO_PAYMENT_DISABLED: felt252 = 'Payment: auto disabled'; + /// Error triggered when token is not supported + pub const INVALID_TOKEN: felt252 = 'Payment: invalid token'; + /// Error triggered when amount is invalid + pub const INVALID_AMOUNT: felt252 = 'Payment: invalid amount'; + /// Error triggered when payment not found + pub const PAYMENT_NOT_FOUND: felt252 = 'Payment: not found'; + /// Error triggered when grace period expired + pub const GRACE_PERIOD_EXPIRED: felt252 = 'Payment: grace expired'; + /// Error triggered when insufficient allowance + pub const INSUFFICIENT_ALLOWANCE: felt252 = 'Payment: no allowance'; + /// Error triggered when oracle error occurs + pub const ORACLE_ERROR: felt252 = 'Payment: oracle error'; + /// Error triggered when round not found + pub const ROUND_NOT_FOUND: felt252 = 'Payment: round not found'; + /// Error triggered when auto-payment already active + pub const AUTO_PAYMENT_ACTIVE: felt252 = 'Payment: already active'; + /// Error triggered when frequency is invalid + pub const INVALID_FREQUENCY: felt252 = 'Payment: invalid frequency'; +} \ No newline at end of file diff --git a/src/base/events.cairo b/src/base/events.cairo index 3f9e6da..cd3e899 100644 --- a/src/base/events.cairo +++ b/src/base/events.cairo @@ -408,3 +408,86 @@ pub struct UpdateCancelled { #[key] pub key: felt252, } + +#[derive(Drop, starknet::Event)] + pub struct EmergencyWithdrawalAll { + pub total_amount: u256, + pub member_count: u32, + pub executed_by: ContractAddress, + pub timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct EmergencyWithdrawalMember { + pub member: ContractAddress, + pub amount: u256, + pub executed_by: ContractAddress, + pub timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct RoundEmergencyCompleted { + pub round_id: u256, + pub recipient: ContractAddress, + pub amount: u256, + pub completed_by: ContractAddress, + pub timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct RoundEmergencyCancelled { + pub round_id: u256, + pub cancelled_by: ContractAddress, + pub reason: felt252, + pub timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct RecipientChanged { + pub round_id: u256, + pub old_recipient: ContractAddress, + pub new_recipient: ContractAddress, + pub changed_by: ContractAddress, + pub timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct TokensRecovered { + pub token: ContractAddress, + pub amount: u256, + pub recovered_by: ContractAddress, + pub recipient: ContractAddress, + pub timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct FundsMigrated { + pub new_contract: ContractAddress, + pub amount: u256, + pub migrated_by: ContractAddress, + pub timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct PenaltyPoolDistributed { + pub total_amount: u256, + pub recipient_count: u32, + pub distribution_type: felt252, + pub timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct MemberUnbanned { + pub member: ContractAddress, + pub unbanned_by: ContractAddress, + pub timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct MemberBanned { + pub member: ContractAddress, + pub reason: felt252, + pub strikes: u32, + pub banned_by: ContractAddress, + pub timestamp: u64, + } \ No newline at end of file diff --git a/src/base/types.cairo b/src/base/types.cairo index e220fab..39e1582 100644 --- a/src/base/types.cairo +++ b/src/base/types.cairo @@ -1,4 +1,6 @@ use starknet::ContractAddress; +use core::serde::Serde; +use core::array::{ArrayTrait, Array}; /// User profile structure containing user information @@ -255,6 +257,7 @@ pub struct SavingsGroup { // Enum for the status of a contribution round #[derive(Copy, Drop, Serde, PartialEq, starknet::Store, Debug)] pub enum RoundStatus { + Scheduled, #[default] Active, Completed, @@ -319,3 +322,89 @@ pub struct ParameterHistory { pub changed_by: ContractAddress, pub changed_at: u64, } + +// Penalty configuration structure +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct PenaltyConfig { + pub late_fee_percentage: u256, // Late fee as basis points (e.g., 250 = 2.5%) + pub grace_period_hours: u64, // Grace period before late fees apply + pub max_strikes: u32, // Maximum strikes before automatic ban + pub security_deposit_multiplier: u256, // Security deposit amount in tokens + pub penalty_pool_enabled: bool, // Whether penalty pool distribution is enabled +} + +// Member penalty record structure +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct MemberPenaltyRecord { + pub total_penalties_paid: u256, // Total penalties paid by member + pub strikes: u32, // Current strike count + pub is_banned: bool, // Whether member is currently banned + pub last_penalty_date: u64, // Timestamp of last penalty + pub last_strike_date: u64, // Timestamp of last strike + pub total_rounds_missed: u32, // Total rounds where contribution was missed +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct AutoScheduleConfig { + pub round_duration_days: u64, + pub start_date: u64, + pub auto_activation_enabled: bool, + pub auto_completion_enabled: bool, + pub rolling_schedule_count: u8 // Maintain 2-3 future rounds +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct ScheduledRound { + pub round_id: u256, + pub recipient: ContractAddress, + pub scheduled_start: u64, + pub scheduled_deadline: u64, + pub status: RoundStatus, + pub auto_generated: bool, +} + +// Penalty event structure for history tracking +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct PenaltyEventRecord { + pub member: ContractAddress, // Member who received penalty + pub round_id: u256, // Round where penalty occurred + pub event_type: PenaltyEventType, // Type of penalty event + pub amount: u256, // Penalty amount + pub timestamp: u64, // When penalty occurred + pub admin: ContractAddress, // Admin who applied penalty +} + +// Distribution data structure for penalty pool distribution +// Note: This struct is not stored, only used for calculations +#[derive(Clone, Drop, Serde)] +pub struct DistributionData { + pub total_amount: u256, // Total penalty pool amount + pub member_shares: Array, // Array of member shares + pub total_compliant_contributions: u256, // Total contributions from compliant members +} + +// Individual member share structure +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct MemberShare { + pub member: ContractAddress, // Member address + pub share: u256, // Share amount to receive + pub contribution: u256, // Member's total contribution +} + +// Penalty event types +#[allow(starknet::store_no_default_variant)] +#[derive(Copy, Drop, Serde, starknet::Store)] +pub enum PenaltyEventType { + LateFee, + Strike, + Ban, + Unban, + StrikeRemoved, +} + +#[derive(Copy, Drop, starknet::Store)] +pub struct RoundData { + pub deadline: u64, + pub status: RoundStatus, + pub total_contributions: u256, +} \ No newline at end of file diff --git a/src/component/analytics.cairo b/src/component/analytics.cairo new file mode 100644 index 0000000..4e50edd --- /dev/null +++ b/src/component/analytics.cairo @@ -0,0 +1,367 @@ +// use starknet::ContractAddress; + +// #[starknet::interface] +// pub trait IAnalytics { +// fn generate_contribution_report(self: @TContractState) -> ContributionAnalytics; +// fn get_member_performance(self: @TContractState, member: ContractAddress) -> MemberAnalytics; +// fn calculate_system_health(self: @TContractState) -> u8; +// } + +// // Data structures for analytics functionality +// #[derive(Copy, Drop, Serde, starknet::Store)] +// pub struct ContributionAnalytics { +// pub total_rounds: u256, +// pub successful_rounds: u256, +// pub failed_rounds: u256, +// pub average_completion_time: u64, +// pub total_penalties_collected: u256, +// pub member_reliability_distribution: Array, +// } + +// #[derive(Copy, Drop, Serde, starknet::Store)] +// pub struct MemberAnalytics { +// pub total_contributions: u256, +// pub on_time_payments: u256, +// pub late_payments: u256, +// pub missed_payments: u256, +// pub reliability_score: u8, +// pub last_updated: u64, +// } + +// #[derive(Copy, Drop, Serde, starknet::Store)] +// pub struct RoundPerformanceMetrics { +// pub round_id: u256, +// pub completion_rate: u8, +// pub average_delay: u64, +// pub total_fees_collected: u256, +// pub success_status: RoundSuccessStatus, +// } + +// #[derive(Copy, Drop, Serde, starknet::Store)] +// pub enum RoundSuccessStatus { +// Outstanding, +// Good, +// Average, +// Poor, +// Failed, +// } + +// #[derive(Copy, Drop, Serde, starknet::Store)] +// pub struct FinancialReport { +// pub period_start: u64, +// pub period_end: u64, +// pub total_contributions: u256, +// pub total_fees_collected: u256, +// pub total_penalties_collected: u256, +// pub active_members: u32, +// pub rounds_completed: u32, +// } + +// #[derive(Copy, Drop, Serde, starknet::Store)] +// pub struct SystemHealthMetrics { +// pub system_uptime_percentage: u8, +// pub active_rounds: u32, +// pub total_locked_value: u256, +// pub security_score: u8, +// } + +// #[generate_trait] +// pub impl IAnalyticsInternal of IAnalyticsInternalTrait { +// fn initializer(ref self: ComponentState); +// fn _update_member_analytics(ref self: ComponentState, member: ContractAddress, payment_made: bool); +// fn _calculate_system_health(self: @ComponentState) -> u8; +// } + +// #[starknet::component] +// pub mod analytics_component { +// use core::starknet::{ContractAddress, get_block_timestamp, get_caller_address}; +// use core::starknet::storage::{ +// Map, StoragePointerReadAccess, StoragePointerWriteAccess, +// }; +// use super::{ContributionAnalytics, MemberAnalytics, RoundPerformanceMetrics, FinancialReport, SystemHealthMetrics, RoundSuccessStatus}; + +// #[derive(Drop)] +// pub enum Errors { +// NOT_ADMIN: (), +// INVALID_PERIOD: (), +// MEMBER_NOT_FOUND: (), +// INSUFFICIENT_DATA: (), +// } + +// #[storage] +// pub struct Storage { +// member_analytics: Map, +// contribution_analytics: ContributionAnalytics, +// round_metrics: Map, +// financial_reports: Map, // timestamp -> report +// system_metrics: SystemHealthMetrics, +// admin: ContractAddress, +// last_update_timestamp: u64, +// total_system_value: u256, +// } + +// #[event] +// #[derive(Drop, starknet::Event)] +// pub enum Event { +// AnalyticsUpdated: AnalyticsUpdated, +// ReportGenerated: ReportGenerated, +// SystemHealthUpdated: SystemHealthUpdated, +// MemberPerformanceUpdated: MemberPerformanceUpdated, +// } + +// #[derive(Drop, starknet::Event)] +// pub struct AnalyticsUpdated { +// member: ContractAddress, +// new_score: u8, +// timestamp: u64, +// } + +// #[derive(Drop, starknet::Event)] +// pub struct ReportGenerated { +// report_type: felt252, +// timestamp: u64, +// } + +// #[derive(Drop, starknet::Event)] +// pub struct SystemHealthUpdated { +// health_score: u8, +// timestamp: u64, +// } + +// #[derive(Drop, starknet::Event)] +// pub struct MemberPerformanceUpdated { +// member: ContractAddress, +// total_contributions: u256, +// reliability_score: u8, +// timestamp: u64, +// } + +// impl AnalyticsImpl< +// TContractState, +HasComponent, +// > of super::IAnalytics> { +// fn generate_contribution_report(self: @ComponentState) -> ContributionAnalytics { +// let analytics = self.contribution_analytics.read(); +// self.emit(Event::ReportGenerated(ReportGenerated { +// report_type: 'CONTRIBUTION_REPORT', +// timestamp: get_block_timestamp(), +// })); +// analytics +// } + +// fn get_member_performance(self: @ComponentState, member: ContractAddress) -> MemberAnalytics { +// let analytics = self.member_analytics.read(member); +// if analytics.last_updated == 0 { +// // Return default analytics for new member +// MemberAnalytics { +// total_contributions: 0, +// on_time_payments: 0, +// late_payments: 0, +// missed_payments: 0, +// reliability_score: 50, +// last_updated: get_block_timestamp(), +// } +// } else { +// analytics +// } +// } + +// fn calculate_system_health(self: @ComponentState) -> u8 { +// let health_score = self._calculate_system_health(); +// self.emit(Event::SystemHealthUpdated(SystemHealthUpdated { +// health_score, +// timestamp: get_block_timestamp(), +// })); +// health_score +// } +// } + +// // Additional analytics functionality +// impl AdditionalAnalyticsImpl< +// TContractState, +HasComponent, +// > of AdditionalAnalyticsTrait { +// fn update_member_performance(ref self: ComponentState, member: ContractAddress, amount: u256, on_time: bool) { +// let mut analytics = self.member_analytics.read(member); + +// analytics.total_contributions += amount; +// if on_time { +// analytics.on_time_payments += 1; +// } else { +// analytics.late_payments += 1; +// } + +// // Recalculate reliability score +// let total_payments = analytics.on_time_payments + analytics.late_payments + analytics.missed_payments; +// if total_payments > 0 { +// analytics.reliability_score = ((analytics.on_time_payments * 100) / total_payments).try_into().unwrap(); +// } + +// analytics.last_updated = get_block_timestamp(); +// self.member_analytics.write(member, analytics); + +// self.emit(Event::MemberPerformanceUpdated(MemberPerformanceUpdated { +// member, +// total_contributions: analytics.total_contributions, +// reliability_score: analytics.reliability_score, +// timestamp: get_block_timestamp(), +// })); +// } + +// fn record_missed_payment(ref self: ComponentState, member: ContractAddress) { +// let mut analytics = self.member_analytics.read(member); +// analytics.missed_payments += 1; + +// // Update reliability score +// let total_payments = analytics.on_time_payments + analytics.late_payments + analytics.missed_payments; +// if total_payments > 0 { +// analytics.reliability_score = ((analytics.on_time_payments * 100) / total_payments).try_into().unwrap(); +// } + +// analytics.last_updated = get_block_timestamp(); +// self.member_analytics.write(member, analytics); +// } + +// fn update_round_metrics(ref self: ComponentState, round_id: u256, completion_rate: u8, fees_collected: u256) { +// let success_status = if completion_rate >= 95 { +// RoundSuccessStatus::Outstanding +// } else if completion_rate >= 85 { +// RoundSuccessStatus::Good +// } else if completion_rate >= 70 { +// RoundSuccessStatus::Average +// } else if completion_rate >= 50 { +// RoundSuccessStatus::Poor +// } else { +// RoundSuccessStatus::Failed +// }; + +// let metrics = RoundPerformanceMetrics { +// round_id, +// completion_rate, +// average_delay: 0, // Would be calculated based on payment timestamps +// total_fees_collected: fees_collected, +// success_status, +// }; + +// self.round_metrics.write(round_id, metrics); +// } + +// fn generate_financial_report(ref self: ComponentState, period_start: u64, period_end: u64) -> FinancialReport { +// assert(period_start < period_end, Errors::INVALID_PERIOD); + +// let report = FinancialReport { +// period_start, +// period_end, +// total_contributions: self.total_system_value.read(), +// total_fees_collected: self.total_system_value.read() / 100, // 1% fee assumption +// total_penalties_collected: self.total_system_value.read() / 200, // 0.5% penalty assumption +// active_members: 50, // Placeholder +// rounds_completed: 20, // Placeholder +// }; + +// self.financial_reports.write(period_start, report); +// report +// } + +// fn update_system_metrics(ref self: ComponentState, total_value: u256, active_rounds: u32) { +// self.total_system_value.write(total_value); + +// let mut metrics = self.system_metrics.read(); +// metrics.total_locked_value = total_value; +// metrics.active_rounds = active_rounds; +// metrics.system_uptime_percentage = 99; // High uptime assumption +// metrics.security_score = 95; // High security score + +// self.system_metrics.write(metrics); +// self.last_update_timestamp.write(get_block_timestamp()); +// } + +// fn get_system_metrics(self: @ComponentState) -> SystemHealthMetrics { +// self.system_metrics.read() +// } + +// fn get_round_performance(self: @ComponentState, round_id: u256) -> RoundPerformanceMetrics { +// self.round_metrics.read(round_id) +// } + +// fn get_financial_report(self: @ComponentState, timestamp: u64) -> FinancialReport { +// self.financial_reports.read(timestamp) +// } +// } + +// #[generate_trait] +// pub trait AdditionalAnalyticsTrait { +// fn update_member_performance(ref self: ComponentState, member: ContractAddress, amount: u256, on_time: bool); +// fn record_missed_payment(ref self: ComponentState, member: ContractAddress); +// fn update_round_metrics(ref self: ComponentState, round_id: u256, completion_rate: u8, fees_collected: u256); +// fn generate_financial_report(ref self: ComponentState, period_start: u64, period_end: u64) -> FinancialReport; +// fn update_system_metrics(ref self: ComponentState, total_value: u256, active_rounds: u32); +// fn get_system_metrics(self: @ComponentState) -> SystemHealthMetrics; +// fn get_round_performance(self: @ComponentState, round_id: u256) -> RoundPerformanceMetrics; +// fn get_financial_report(self: @ComponentState, timestamp: u64) -> FinancialReport; +// } + +// #[generate_trait] +// pub impl InternalImpl< +// TContractState, +HasComponent, +// > of super::IAnalyticsInternal { +// fn initializer(ref self: ComponentState) { +// self.admin.write(get_caller_address()); +// self.total_system_value.write(0); +// self.last_update_timestamp.write(get_block_timestamp()); + +// // Initialize default analytics +// let default_analytics = ContributionAnalytics { +// total_rounds: 0, +// successful_rounds: 0, +// failed_rounds: 0, +// average_completion_time: 0, +// total_penalties_collected: 0, +// member_reliability_distribution: array![], +// }; +// self.contribution_analytics.write(default_analytics); +// } + +// fn _update_member_analytics(ref self: ComponentState, member: ContractAddress, payment_made: bool) { +// let mut analytics = self.member_analytics.read(member); + +// if payment_made { +// analytics.on_time_payments += 1; +// } else { +// analytics.missed_payments += 1; +// } + +// // Recalculate reliability score +// let total_attempts = analytics.on_time_payments + analytics.late_payments + analytics.missed_payments; +// if total_attempts > 0 { +// let successful = analytics.on_time_payments + analytics.late_payments; +// analytics.reliability_score = ((successful * 100) / total_attempts).try_into().unwrap(); +// } + +// analytics.last_updated = get_block_timestamp(); +// self.member_analytics.write(member, analytics); + +// self.emit(Event::AnalyticsUpdated(AnalyticsUpdated { +// member, +// new_score: analytics.reliability_score, +// timestamp: get_block_timestamp(), +// })); +// } + +// fn _calculate_system_health(self: @ComponentState) -> u8 { +// let metrics = self.system_metrics.read(); +// let analytics = self.contribution_analytics.read(); + +// // Simple health calculation based on multiple factors +// let uptime_score = metrics.system_uptime_percentage; +// let security_score = metrics.security_score; +// let success_rate = if analytics.total_rounds > 0 { +// ((analytics.successful_rounds * 100) / analytics.total_rounds).try_into().unwrap() +// } else { +// 100_u8 +// }; + +// // Weighted average +// let health = (uptime_score + security_score + success_rate) / 3; +// health +// } +// } +// } diff --git a/src/component/auto_schedule.cairo b/src/component/auto_schedule.cairo new file mode 100644 index 0000000..38323d9 --- /dev/null +++ b/src/component/auto_schedule.cairo @@ -0,0 +1,387 @@ +use starknet::ContractAddress; +use starkremit_contract::base::types::{AutoScheduleConfig, RoundStatus, ScheduledRound}; + + +// Trait that the main contract must implement to provide data access +pub trait IMainContractData { + fn get_member_count(self: @TContractState) -> u32; + fn get_member_by_index(self: @TContractState, index: u32) -> ContractAddress; + fn get_current_round_id(self: @TContractState) -> u256; + fn create_round(ref self: TContractState, recipient: ContractAddress, deadline: u64) -> u256; +} + +#[starknet::interface] +pub trait IAutoSchedule { + // Configuration and query functions (simple operations) + fn get_config(self: @TContractState) -> AutoScheduleConfig; + fn get_scheduled_round(self: @TContractState, round_id: u256) -> ScheduledRound; + fn get_next_scheduled_rounds(self: @TContractState, count: u8) -> Array; + fn get_current_rotation_index(self: @TContractState) -> u32; + + fn is_auto_schedule_enabled(self: @TContractState) -> bool; + fn get_rotation_length(self: @TContractState) -> u32; +} + +#[starknet::component] +pub mod auto_schedule_component { + use starknet::{ContractAddress, get_block_timestamp, get_caller_address}; + use starknet::storage::{ + Map, StoragePointerReadAccess, StoragePointerWriteAccess, StorageMapReadAccess, StorageMapWriteAccess, + }; + + use core::array::ArrayTrait; + use starkremit_contract::base::types::RoundStatus; + use super::{AutoScheduleConfig, ScheduledRound, IMainContractData}; + use starkremit_contract::base::errors::AutoScheduleErrors; + + const SECONDS_PER_DAY: u64 = 86400; + + #[storage] + pub struct Storage { + config: AutoScheduleConfig, + scheduled_rounds: Map, + member_rotation: Map, // Index -> Address + rotation_length: u32, + current_rotation_index: u32, + next_round_id: u256, + last_processed_timestamp: u64, + admin: ContractAddress, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + AutoScheduleSetup: AutoScheduleSetup, + RoundAutoActivated: RoundAutoActivated, + RoundAutoCompleted: RoundAutoCompleted, + ScheduleMaintained: ScheduleMaintained, + RoundScheduleModified: RoundScheduleModified, + ConfigUpdated: ConfigUpdated, + ScheduleProcessed: ScheduleProcessed, + } + + #[derive(Drop, starknet::Event)] + pub struct AutoScheduleSetup { + admin: ContractAddress, + start_date: u64, + round_duration_days: u64, + rolling_schedule_count: u8, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct RoundAutoActivated { + round_id: u256, + recipient: ContractAddress, + scheduled_start: u64, + scheduled_deadline: u64, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct RoundAutoCompleted { + round_id: u256, + completed_at: u64, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct ScheduleMaintained { + rounds_created: u32, + last_maintenance_timestamp: u64, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct RoundScheduleModified { + round_id: u256, + old_deadline: u64, + new_deadline: u64, + modified_by: ContractAddress, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct ConfigUpdated { + old_config: AutoScheduleConfig, + new_config: AutoScheduleConfig, + updated_by: ContractAddress, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct ScheduleProcessed { + rounds_processed: u32, + timestamp: u64, + } + + #[embeddable_as(AutoSchedule)] + impl AutoScheduleImpl< + TContractState, +HasComponent, +IMainContractData, + > of super::IAutoSchedule> { + + fn get_config(self: @ComponentState) -> AutoScheduleConfig { + self.config.read() + } + + fn get_scheduled_round(self: @ComponentState, round_id: u256) -> ScheduledRound { + self.scheduled_rounds.read(round_id) + } + + fn get_next_scheduled_rounds(self: @ComponentState, count: u8) -> Array { + let mut rounds = ArrayTrait::new(); + let current_index = self.current_rotation_index.read(); + let mut rounds_added = 0_u8; + + // Get next scheduled rounds + let mut i = 1_u256; + while i <= self.next_round_id.read() && rounds_added < count.into() { + let scheduled_round = self.scheduled_rounds.read(i); + if scheduled_round.status == RoundStatus::Scheduled { + rounds.append(scheduled_round); + rounds_added += 1_u8; + } + i += 1_u256; + } + + rounds + } + + fn get_current_rotation_index(self: @ComponentState) -> u32 { + self.current_rotation_index.read() + } + + fn is_auto_schedule_enabled(self: @ComponentState) -> bool { + let config = self.config.read(); + config.auto_activation_enabled + } + + fn get_rotation_length(self: @ComponentState) -> u32 { + self.rotation_length.read() + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, +HasComponent, +IMainContractData, + > of InternalTrait { + + fn initializer(ref self: ComponentState, admin: ContractAddress) { + self.admin.write(admin); + + // Set default auto-schedule configuration + let default_config = AutoScheduleConfig { + round_duration_days: 30, + start_date: get_block_timestamp(), + auto_activation_enabled: true, + auto_completion_enabled: true, + rolling_schedule_count: 3, + }; + self.config.write(default_config); + + // Initialize rotation system + self.rotation_length.write(0); + self.current_rotation_index.write(0); + self.next_round_id.write(1); + self.last_processed_timestamp.write(get_block_timestamp()); + + self.emit(Event::AutoScheduleSetup(AutoScheduleSetup { + admin, + start_date: default_config.start_date, + round_duration_days: default_config.round_duration_days, + rolling_schedule_count: default_config.rolling_schedule_count, + timestamp: get_block_timestamp(), + })); + } + + // Internal function to assert that the caller is the admin + fn _assert_admin(self: @ComponentState) { + let admin: ContractAddress = self.admin.read(); + let caller: ContractAddress = get_caller_address(); + assert(caller == admin, AutoScheduleErrors::NOT_ADMIN); + } + + // Complex operations that will be called by the main contract + fn _setup_auto_schedule(ref self: ComponentState, config: AutoScheduleConfig) { + self._assert_admin(); + + let old_config = self.config.read(); + self.config.write(config); + + // Initialize member rotation if not already set + if self.rotation_length.read() == 0 { + self._initialize_member_rotation(); + } + + self.emit(Event::ConfigUpdated(ConfigUpdated { + old_config, + new_config: config, + updated_by: get_caller_address(), + timestamp: get_block_timestamp(), + })); + } + + fn _maintain_rolling_schedule(ref self: ComponentState) { + let config = self.config.read(); + let current_time = get_block_timestamp(); + let last_maintenance = self.last_processed_timestamp.read(); + + // Check if maintenance is needed + if current_time - last_maintenance < config.round_duration_days * SECONDS_PER_DAY { + return; + } + + let mut rounds_created = 0_u32; + let mut current_index: u256 = self.next_round_id.read(); + + // Create new rounds to maintain rolling schedule + while rounds_created < config.rolling_schedule_count.into() { + current_index += 1_u256; + + // Calculate timing for the new round + let round_duration_seconds: u64 = config.round_duration_days * SECONDS_PER_DAY; + let round_start = config.start_date + ((current_index - 1_u256).try_into().unwrap() * round_duration_seconds); + let round_deadline = round_start + round_duration_seconds; + + // Determine recipient by rotating through the member list + let recipient = self._get_next_recipient(); + + let scheduled_round = ScheduledRound { + round_id: current_index, + recipient, + scheduled_start: round_start, + scheduled_deadline: round_deadline, + status: RoundStatus::Scheduled, + auto_generated: true, + }; + + self.scheduled_rounds.write(current_index, scheduled_round); + rounds_created += 1; + } + + // Update indices for the next maintenance cycle + self.next_round_id.write(current_index); + self.last_processed_timestamp.write(current_time); + + self.emit(Event::ScheduleMaintained(ScheduleMaintained { + rounds_created: rounds_created.try_into().unwrap(), + last_maintenance_timestamp: current_time, + timestamp: get_block_timestamp(), + })); + } + + fn _auto_activate_round(ref self: ComponentState, round_id: u256) { + let config = self.config.read(); + if !config.auto_activation_enabled { + return; + } + + let mut scheduled_round = self.scheduled_rounds.read(round_id); + if scheduled_round.status == RoundStatus::Scheduled { + scheduled_round.status = RoundStatus::Active; + self.scheduled_rounds.write(round_id, scheduled_round); + + self.emit(Event::RoundAutoActivated(RoundAutoActivated { + round_id, + recipient: scheduled_round.recipient, + scheduled_start: scheduled_round.scheduled_start, + scheduled_deadline: scheduled_round.scheduled_deadline, + timestamp: get_block_timestamp(), + })); + } + } + + fn _auto_complete_expired_rounds(ref self: ComponentState) { + let config = self.config.read(); + if !config.auto_completion_enabled { + return; + } + + let current_time = get_block_timestamp(); + let mut rounds_processed = 0; + + // Check all scheduled rounds for expiration + let mut i = 1_u256; + while i <= self.next_round_id.read() { + let mut scheduled_round = self.scheduled_rounds.read(i); + + if scheduled_round.status == RoundStatus::Active + && scheduled_round.scheduled_deadline <= current_time { + scheduled_round.status = RoundStatus::Completed; + self.scheduled_rounds.write(i, scheduled_round); + rounds_processed += 1; + + self.emit(Event::RoundAutoCompleted(RoundAutoCompleted { + round_id: i, + completed_at: current_time, + timestamp: get_block_timestamp(), + })); + } + i += 1; + } + + if rounds_processed > 0 { + self.emit(Event::ScheduleProcessed(ScheduleProcessed { + rounds_processed, + timestamp: get_block_timestamp(), + })); + } + } + + fn _modify_schedule(ref self: ComponentState, round_id: u256, new_deadline: u64) { + self._assert_admin(); + + let mut scheduled_round = self.scheduled_rounds.read(round_id); + let old_deadline = scheduled_round.scheduled_deadline; + + // Validate new deadline + assert(new_deadline > get_block_timestamp(), AutoScheduleErrors::NEW_DEADLINE_NOT_IN_FUTURE); + assert(new_deadline > scheduled_round.scheduled_start, AutoScheduleErrors::NEW_DEADLINE_NOT_AFTER_START); + + scheduled_round.scheduled_deadline = new_deadline; + self.scheduled_rounds.write(round_id, scheduled_round); + + self.emit(Event::RoundScheduleModified(RoundScheduleModified { + round_id, + old_deadline, + new_deadline, + modified_by: get_caller_address(), + timestamp: get_block_timestamp(), + })); + } + + // Helper functions + fn _initialize_member_rotation(ref self: ComponentState) { + let contract_state = self.get_contract(); + let member_count = contract_state.get_member_count(); + + if member_count > 0 { + self.rotation_length.write(member_count); + + // Populate rotation array + let mut i = 0; + while i < member_count { + let member = contract_state.get_member_by_index(i); + self.member_rotation.write(i, member); + i += 1; + } + } + } + + fn _get_next_recipient(ref self: ComponentState) -> ContractAddress { + let rotation_length = self.rotation_length.read(); + if rotation_length == 0 { + return 0.try_into().unwrap(); + } + + let current_index = self.current_rotation_index.read(); + let next_index = (current_index + 1_u32) % rotation_length; + + // Update rotation index + self.current_rotation_index.write(next_index); + + self.member_rotation.read(next_index) + } + } + +} diff --git a/src/component/contribution/contribution.cairo b/src/component/contribution/contribution.cairo index a427a95..9efe1d8 100644 --- a/src/component/contribution/contribution.cairo +++ b/src/component/contribution/contribution.cairo @@ -1,5 +1,6 @@ use starknet::ContractAddress; -use starkremit_contract::base::types::{ContributionRound, MemberContribution}; +use starkremit_contract::base::types::{ContributionRound, MemberContribution, RoundStatus}; +use starkremit_contract::base::errors::ContributionErrors; #[starknet::interface] pub trait IContribution { @@ -20,6 +21,13 @@ pub trait IContribution { fn get_current_round_id(self: @TContractState) -> u256; fn set_required_contribution(ref self: TContractState, amount: u256); fn get_required_contribution(self: @TContractState) -> u256; + + + fn get_member_contribution_history(self: @TContractState, member: ContractAddress, limit: u32, offset: u32) -> Array; + fn get_round_statistics(self: @TContractState, round_id: u256) -> (u256, u32, u32); // total_amount, contributor_count, member_count + fn validate_contribution_eligibility(self: @TContractState, member: ContractAddress, round_id: u256) -> bool; + fn get_next_recipient(self: @TContractState) -> ContractAddress; + fn advance_round_rotation(ref self: TContractState); } #[starknet::component] @@ -34,6 +42,7 @@ pub mod contribution_component { }; use starknet::{ContractAddress, get_block_timestamp, get_caller_address, get_contract_address}; use starkremit_contract::base::types::{ContributionRound, MemberContribution, RoundStatus}; + use starkremit_contract::base::errors::ContributionErrors; use super::*; #[storage] @@ -49,6 +58,15 @@ pub mod contribution_component { required_contribution: u256, member_index_map: Map, erc20_address: ContractAddress, + + // Enhanced storage + member_contribution_history: Map<(ContractAddress, u32), u256>, // member -> (index -> round_id) + member_contribution_count: Map, // member -> total contributions + current_rotation_index: u32, // Current position in member rotation + round_contributor_count: Map, // round_id -> number of contributors + member_last_contribution: Map, // member -> last contribution timestamp + contribution_limits: Map, // member -> max contribution per round + grace_period_hours: u64, // Grace period for late contributions } #[event] @@ -61,41 +79,89 @@ pub mod contribution_component { MemberAdded: MemberAdded, MemberRemoved: MemberRemoved, RequiredContributionUpdated: RequiredContributionUpdated, + ContributionLimitUpdated: ContributionLimitUpdated, + GracePeriodUpdated: GracePeriodUpdated, + RoundRotationAdvanced: RoundRotationAdvanced, } + #[derive(Drop, starknet::Event)] pub struct ContributionMade { round_id: u256, member: ContractAddress, amount: u256, + timestamp: u64, + is_on_time: bool, } + #[derive(Drop, starknet::Event)] pub struct RoundDisbursed { round_id: u256, recipient: ContractAddress, amount: u256, + contributor_count: u32, + timestamp: u64, } + #[derive(Drop, starknet::Event)] pub struct RoundCompleted { round_id: u256, + total_amount: u256, + contributor_count: u32, + timestamp: u64, } + #[derive(Drop, starknet::Event)] pub struct ContributionMissed { round_id: u256, member: ContractAddress, + timestamp: u64, } + #[derive(Drop, starknet::Event)] pub struct MemberAdded { member: ContractAddress, + added_by: ContractAddress, + timestamp: u64, } #[derive(Drop, starknet::Event)] pub struct MemberRemoved { member: ContractAddress, + removed_by: ContractAddress, + timestamp: u64, } + #[derive(Drop, starknet::Event)] pub struct RequiredContributionUpdated { old_amount: u256, new_amount: u256, + updated_by: ContractAddress, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct ContributionLimitUpdated { + member: ContractAddress, + old_limit: u256, + new_limit: u256, + updated_by: ContractAddress, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct GracePeriodUpdated { + old_hours: u64, + new_hours: u64, + updated_by: ContractAddress, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct RoundRotationAdvanced { + old_index: u32, + new_index: u32, + next_recipient: ContractAddress, + timestamp: u64, } #[embeddable_as(Contribution)] @@ -109,46 +175,90 @@ pub mod contribution_component { ref self: ComponentState, round_id: u256, amount: u256, ) { let caller = get_caller_address(); - assert(self.is_member(caller), 'Caller is not a member'); + let current_time = get_block_timestamp(); + + // Validate caller is a member + assert(self.is_member(caller), ContributionErrors::NOT_MEMBER); // Prevent double contributions let existing_contribution = self.member_contributions.read((round_id, caller)); - assert(existing_contribution.amount == 0, 'Already contributed'); + assert(existing_contribution.amount == 0, ContributionErrors::ALREADY_CONTRIBUTED); let mut round = self.rounds.read(round_id); - assert(round.status == RoundStatus::Active, 'Round is not active'); - assert(get_block_timestamp() <= round.deadline, 'Contribution deadline passed'); + assert(round.status == RoundStatus::Active, ContributionErrors::ROUND_NOT_ACTIVE); + + // Check if contribution is within grace period + let grace_period = self.grace_period_hours.read() * 3600; // Convert to seconds + let is_on_time = current_time <= round.deadline + grace_period; + assert(is_on_time, ContributionErrors::CONTRIBUTION_DEADLINE_PASSED); // Validate contribution amount let required_amount = self.required_contribution.read(); - assert(amount >= required_amount, 'amount less than required'); + assert(amount >= required_amount, ContributionErrors::INSUFFICIENT_AMOUNT); + + // Check contribution limits + let member_limit = self.contribution_limits.read(caller); + if member_limit > 0 { + assert(amount <= member_limit, ContributionErrors::CONTRIBUTION_LIMIT_EXCEEDED); + } + // Create contribution record let contribution = MemberContribution { - member: caller, amount, contributed_at: get_block_timestamp(), + member: caller, + amount, + contributed_at: current_time, }; self.member_contributions.write((round_id, caller), contribution); + + // Update round statistics round.total_contributions += amount; self.rounds.write(round_id, round); + + // Update contributor count + let contributor_count = self.round_contributor_count.read(round_id); + self.round_contributor_count.write(round_id, contributor_count + 1); + + // Update member statistics + let member_contribution_count = self.member_contribution_count.read(caller); + self.member_contribution_count.write(caller, member_contribution_count + 1); + self.member_last_contribution.write(caller, current_time); + + // Add to member's contribution history + let history_count = self.member_contribution_count.read(caller); + self.member_contribution_history.write((caller, history_count - 1), round_id); // Token transfer let erc20_address = self.erc20_address.read(); IERC20Dispatcher { contract_address: erc20_address } .transfer_from(caller, get_contract_address(), amount); - self - .emit( - Event::ContributionMade(ContributionMade { round_id, member: caller, amount }), - ); + self.emit(Event::ContributionMade(ContributionMade { + round_id, + member: caller, + amount, + timestamp: current_time, + is_on_time: current_time <= round.deadline, + })); } fn complete_round(ref self: ComponentState, round_id: u256) { self.is_owner(); let mut round = self.rounds.read(round_id); - assert(round.status == RoundStatus::Active, 'Round is not active'); + assert(round.status == RoundStatus::Active, ContributionErrors::ROUND_NOT_ACTIVE); + + let current_time = get_block_timestamp(); round.status = RoundStatus::Completed; self.rounds.write(round_id, round); - self.emit(Event::RoundCompleted(RoundCompleted { round_id })); + + let contributor_count = self.round_contributor_count.read(round_id); + + self.emit(Event::RoundCompleted(RoundCompleted { + round_id, + total_amount: round.total_contributions, + contributor_count, + timestamp: current_time, + })); } fn add_round_to_schedule( @@ -157,18 +267,26 @@ pub mod contribution_component { self.is_owner(); // Validate recipient is a member - assert(self.is_member(recipient), 'Recipient must be a member'); + assert(self.is_member(recipient), ContributionErrors::RECIPIENT_NOT_MEMBER); // Validate deadline is in the future - assert(deadline > get_block_timestamp(), 'Deadline not in the future'); + assert(deadline > get_block_timestamp(), ContributionErrors::DEADLINE_NOT_IN_FUTURE); let round_id = self.round_ids.read() + 1; self.round_ids.write(round_id); self.rotation_schedule.write(round_id, recipient); + let round = ContributionRound { - round_id, recipient, deadline, total_contributions: 0, status: RoundStatus::Active, + round_id, + recipient, + deadline, + total_contributions: 0, + status: RoundStatus::Active, }; self.rounds.write(round_id, round); + + // Initialize contributor count + self.round_contributor_count.write(round_id, 0); } fn is_member(self: @ComponentState, address: ContractAddress) -> bool { @@ -177,8 +295,11 @@ pub mod contribution_component { fn check_missed_contributions(ref self: ComponentState, round_id: u256) { let round = self.rounds.read(round_id); - assert(round.status == RoundStatus::Active, 'Round is not active'); - assert(get_block_timestamp() > round.deadline, 'Round deadline not passed'); + assert(round.status == RoundStatus::Active, ContributionErrors::ROUND_NOT_ACTIVE); + + let current_time = get_block_timestamp(); + let grace_period = self.grace_period_hours.read() * 3600; + assert(current_time > round.deadline + grace_period, ContributionErrors::ROUND_DEADLINE_NOT_PASSED); // Check all members for missed contributions let all_members = self.get_all_members(); @@ -187,7 +308,11 @@ pub mod contribution_component { let member = *all_members[i]; let contribution = self.member_contributions.read((round_id, member)); if contribution.amount == 0 { - self.emit(ContributionMissed { round_id, member: member }); + self.emit(Event::ContributionMissed(ContributionMissed { + round_id, + member: member, + timestamp: current_time, + })); } i += 1; } @@ -214,9 +339,9 @@ pub mod contribution_component { self.is_owner(); // Validate address is not zero - assert(!address.is_zero(), 'Invalid address'); + assert(!address.is_zero(), ContributionErrors::INVALID_ADDRESS); - assert(!self.is_member(address), 'Already a member'); + assert(!self.is_member(address), ContributionErrors::ALREADY_MEMBER); self.members.write(address, true); let count = self.member_count.read(); self.member_by_index.write(count, address); @@ -225,34 +350,45 @@ pub mod contribution_component { self.member_index_map.write(address, count); self.member_count.write(count + 1); - self.emit(MemberAdded { member: address }); + + // Initialize member statistics + self.member_contribution_count.write(address, 0); + self.member_last_contribution.write(address, 0); + + self.emit(Event::MemberAdded(MemberAdded { + member: address, + added_by: get_caller_address(), + timestamp: get_block_timestamp(), + })); } fn disburse_round_contribution(ref self: ComponentState, round_id: u256) { self.is_owner(); let round = self.rounds.read(round_id); - assert(round.status == RoundStatus::Completed, 'Round not completed'); + assert(round.status == RoundStatus::Completed, ContributionErrors::ROUND_NOT_COMPLETED); + + let current_time = get_block_timestamp(); + let contributor_count = self.round_contributor_count.read(round_id); // Token transfer to recipient let erc20_address = self.erc20_address.read(); IERC20Dispatcher { contract_address: erc20_address } .transfer(round.recipient, round.total_contributions); - self - .emit( - Event::RoundDisbursed( - RoundDisbursed { - round_id, recipient: round.recipient, amount: round.total_contributions, - }, - ), - ); + self.emit(Event::RoundDisbursed(RoundDisbursed { + round_id, + recipient: round.recipient, + amount: round.total_contributions, + contributor_count, + timestamp: current_time, + })); } fn remove_member(ref self: ComponentState, address: ContractAddress) { self.is_owner(); - assert(self.is_member(address), 'Not a member'); + assert(self.is_member(address), ContributionErrors::NOT_MEMBER); // Remove from members mapping self.members.write(address, false); @@ -273,7 +409,11 @@ pub mod contribution_component { self.member_count.write(last_index); self.member_index_map.write(address, 0); - self.emit(MemberRemoved { member: address }); + self.emit(Event::MemberRemoved(MemberRemoved { + member: address, + removed_by: get_caller_address(), + timestamp: get_block_timestamp(), + })); } fn get_round_details( @@ -298,12 +438,121 @@ pub mod contribution_component { let old_amount = self.required_contribution.read(); self.required_contribution.write(amount); - self.emit(RequiredContributionUpdated { old_amount, new_amount: amount }); + self.emit(Event::RequiredContributionUpdated(RequiredContributionUpdated { + old_amount, + new_amount: amount, + updated_by: get_caller_address(), + timestamp: get_block_timestamp(), + })); } fn get_required_contribution(self: @ComponentState) -> u256 { self.required_contribution.read() } + + // Enhanced functions + fn get_member_contribution_history( + self: @ComponentState, + member: ContractAddress, + limit: u32, + offset: u32 + ) -> Array { + let mut contributions = ArrayTrait::new(); + let total_count = self.member_contribution_count.read(member); + + let mut i = offset; + let mut count = 0; + + while i < total_count && count < limit { + let round_id = self.member_contribution_history.read((member, i)); + if round_id > 0 { + let contribution = self.member_contributions.read((round_id, member)); + contributions.append(contribution); + count += 1; + } + i += 1; + } + + contributions + } + + fn get_round_statistics( + self: @ComponentState, + round_id: u256 + ) -> (u256, u32, u32) { + let round = self.rounds.read(round_id); + let contributor_count = self.round_contributor_count.read(round_id); + let member_count = self.member_count.read(); + + (round.total_contributions, contributor_count, member_count) + } + + fn validate_contribution_eligibility( + self: @ComponentState, + member: ContractAddress, + round_id: u256 + ) -> bool { + // Check if member exists and is active + if !self.is_member(member) { + return false; + } + + // Check if round is active + let round = self.rounds.read(round_id); + if round.status != RoundStatus::Active { + return false; + } + + // Check if member already contributed + let contribution = self.member_contributions.read((round_id, member)); + if contribution.amount > 0 { + return false; + } + + // Check if deadline hasn't passed (including grace period) + let current_time = get_block_timestamp(); + let grace_period = self.grace_period_hours.read() * 3600; + if current_time > round.deadline + grace_period { + return false; + } + + true + } + + fn get_next_recipient(self: @ComponentState) -> ContractAddress { + let member_count = self.member_count.read(); + if member_count == 0 { + return 0.try_into().unwrap(); + } + + let current_index = self.current_rotation_index.read(); + let next_index = (current_index + 1) % member_count; + + self.member_by_index.read(next_index) + } + + fn advance_round_rotation(ref self: ComponentState) { + self.is_owner(); + + let member_count = self.member_count.read(); + if member_count == 0 { + return; + } + + let current_index = self.current_rotation_index.read(); + let new_index = (current_index + 1) % member_count; + + self.current_rotation_index.write(new_index); + + let next_recipient = self.member_by_index.read(new_index); + + self.emit(Event::RoundRotationAdvanced(RoundRotationAdvanced { + old_index: current_index, + new_index, + next_recipient, + timestamp: get_block_timestamp(), + })); + } } #[generate_trait] @@ -315,12 +564,13 @@ pub mod contribution_component { > of InternalTrait { fn initializer(ref self: ComponentState, token_address: ContractAddress) { self.erc20_address.write(token_address); + self.grace_period_hours.write(24); // Default 24 hours grace period } fn is_owner(self: @ComponentState) { let owner_comp = get_dep_component!(self, Owner); let owner = owner_comp.owner(); - assert(owner == get_caller_address(), 'Caller is not the owner'); + assert(owner == get_caller_address(), ContributionErrors::NOT_OWNER); } } } diff --git a/src/component/emergency.cairo b/src/component/emergency.cairo new file mode 100644 index 0000000..0798739 --- /dev/null +++ b/src/component/emergency.cairo @@ -0,0 +1,204 @@ +use starknet::ContractAddress; + +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct EmergencyConfig { + pub emergency_cooldown: u64, + pub required_approvals: u8, +} + + + +#[starknet::interface] +pub trait IEmergency { + // Configuration and query functions (simple operations) + fn set_config(ref self: TContractState, cfg: EmergencyConfig); + fn get_config(self: @TContractState) -> EmergencyConfig; + fn get_pause_reason(self: @TContractState) -> felt252; + fn get_pause_timestamp(self: @TContractState) -> u64; + fn is_paused(self: @TContractState) -> bool; + fn is_banned(self: @TContractState, member: ContractAddress) -> bool; + + // Utility functions (simple operations) + fn assert_paused(self: @TContractState); + fn assert_not_paused(self: @TContractState); +} + + + +#[starknet::component] +pub mod emergency_component { + use starknet::{ContractAddress, get_block_timestamp, get_caller_address}; + use starknet::storage::{ + Map, StoragePointerReadAccess, StoragePointerWriteAccess, StorageMapReadAccess, StorageMapWriteAccess, + }; + use super::EmergencyConfig; + use starkremit_contract::base::errors::{EmergencyComponentErrors}; + + + #[storage] + pub struct Storage { + is_paused: bool, + pause_reason: felt252, + pause_timestamp: u64, + emergency_admin: ContractAddress, + banned_members: Map, + config: EmergencyConfig, + } + + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + PauseMetaSet: PauseMetaSet, + MemberBanned: MemberBanned, + MemberUnbanned: MemberUnbanned, + Paused: Paused, + Unpaused: Unpaused, + Initialized: Initialized, + } + + #[derive(Drop, starknet::Event)] + pub struct PauseMetaSet { + reason: felt252, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct MemberBanned { + member: ContractAddress, + } + + #[derive(Drop, starknet::Event)] + pub struct MemberUnbanned { + member: ContractAddress, + } + + #[derive(Drop, starknet::Event)] + pub struct Paused { + reason: felt252, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct Unpaused { + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct Initialized { + admin: ContractAddress, + } + + + #[embeddable_as(Emergency)] + impl EmergencyImpl< + TContractState, +HasComponent, + > of super::IEmergency> { + + + fn get_pause_reason(self: @ComponentState) -> felt252 { + self.pause_reason.read() + } + + fn get_pause_timestamp(self: @ComponentState) -> u64 { + self.pause_timestamp.read() + } + + + + fn is_banned(self: @ComponentState, member: ContractAddress) -> bool { + self.banned_members.read(member) + } + + fn set_config(ref self: ComponentState, cfg: EmergencyConfig) { + self._assert_admin(); + assert(!self.is_paused.read(), EmergencyComponentErrors::CONTRACT_PAUSED); + self.config.write(cfg); + } + + fn get_config(self: @ComponentState) -> EmergencyConfig { + self.config.read() + } + + fn is_paused(self: @ComponentState) -> bool { + self.is_paused.read() + } + + fn assert_paused(self: @ComponentState) { + assert(self.is_paused.read(), EmergencyComponentErrors::CONTRACT_NOT_PAUSED); + } + + fn assert_not_paused(self: @ComponentState) { + assert(!self.is_paused.read(), EmergencyComponentErrors::CONTRACT_PAUSED); + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, +HasComponent, + > of InternalTrait { + fn initializer(ref self: ComponentState, admin: ContractAddress) { + self.emergency_admin.write(admin); + self.is_paused.write(false); // Component starts unpaused by default + // Initialize config to default values + self.config.write( + EmergencyConfig { emergency_cooldown: 0, required_approvals: 0 } + ); + self.emit(Event::Initialized(Initialized { admin })); + } + + // Internal function to assert that the caller is the emergency admin. + fn _assert_admin(self: @ComponentState) { + let admin: ContractAddress = self.emergency_admin.read(); + let caller: ContractAddress = get_caller_address(); + assert(caller == admin, EmergencyComponentErrors::NOT_ADMIN); + } + + // Internal function to toggle the pause state. + fn _toggle_pause(ref self: ComponentState, paused: bool) { + self.is_paused.write(paused); + if paused { + self.pause_timestamp.write(get_block_timestamp()); + } else { + self.pause_reason.write(0); // Clear reason on unpause + self.pause_timestamp.write(0); // Clear timestamp on unpause + } + } + + // Complex operations that will be called by the main contract + fn _pause(ref self: ComponentState) { + assert(!self.is_paused.read(), EmergencyComponentErrors::ALREADY_PAUSED); + self._toggle_pause(true); + } + + fn _unpause(ref self: ComponentState) { + assert(self.is_paused.read(), EmergencyComponentErrors::CONTRACT_NOT_PAUSED); + self._toggle_pause(false); + } + + fn _pause_with_metadata(ref self: ComponentState, reason: felt252) { + assert(!self.is_paused.read(), EmergencyComponentErrors::ALREADY_PAUSED); + self.pause_reason.write(reason); + self.pause_timestamp.write(get_block_timestamp()); + self._toggle_pause(true); + } + + fn _unpause_with_metadata_clear(ref self: ComponentState) { + assert(self.is_paused.read(), EmergencyComponentErrors::CONTRACT_NOT_PAUSED); + self.pause_reason.write(0); + self.pause_timestamp.write(0); + self._toggle_pause(false); + } + + fn _set_pause_meta(ref self: ComponentState, reason: felt252) { + assert(self.is_paused.read(), EmergencyComponentErrors::CONTRACT_NOT_PAUSED); + self.pause_reason.write(reason); + self.pause_timestamp.write(get_block_timestamp()); + } + + fn _set_ban(ref self: ComponentState, member: ContractAddress, banned: bool) { + assert(!self.is_paused.read(), EmergencyComponentErrors::CONTRACT_PAUSED); + self.banned_members.write(member, banned); + } + } +} \ No newline at end of file diff --git a/src/component/member_profile.cairo b/src/component/member_profile.cairo new file mode 100644 index 0000000..aa19c4b --- /dev/null +++ b/src/component/member_profile.cairo @@ -0,0 +1,398 @@ +// // ENTIRE FILE COMMENTED OUT - EMERGENCY SYSTEM ONLY +// // This file is temporarily disabled to focus on emergency system implementation only + +// /* +// use starknet::ContractAddress; + +// #[starknet::interface] +// pub trait IMemberProfile { +// fn create_member_profile(ref self: TContractState, member: ContractAddress); +// fn update_reliability_rating(ref self: TContractState, member: ContractAddress, new_rating: u8); +// fn get_member_profile(self: @TContractState, member: ContractAddress) -> MemberProfile; +// } + +// // Data structures for member profile functionality +// #[derive(Copy, Drop, Serde, starknet::Store)] +// pub struct MemberProfile { +// pub join_date: u64, +// pub total_contributions: u256, +// pub missed_contributions: u8, +// pub credit_score: u8, +// pub last_recipient_round: u256, +// pub reliability_rating: u8, +// pub preferred_payment_method: felt252, +// pub communication_preferences: felt252, +// } + +// #[generate_trait] +// pub impl IMemberProfileInternal of IMemberProfileInternalTrait { +// fn initializer(ref self: ComponentState); +// fn _assert_admin(self: @ComponentState); +// fn _calculate_credit_score(self: @ComponentState, profile: @MemberProfile) -> u8; +// fn _update_contribution_stats(ref self: ComponentState, member: ContractAddress, amount: u256); +// } + +// #[starknet::component] +// pub mod member_profile_component { +// */ +// use core::starknet::{ContractAddress, get_block_timestamp, get_caller_address}; +// use core::starknet::storage::{ +// Map, StoragePointerReadAccess, StoragePointerWriteAccess, +// }; +// use super::MemberProfile; + +// #[derive(Drop)] +// pub enum Errors { +// NOT_ADMIN: (), +// PROFILE_NOT_FOUND: (), +// PROFILE_ALREADY_EXISTS: (), +// INVALID_RATING: (), +// INVALID_PREFERENCES: (), +// } + +// #[storage] +// pub struct Storage { +// member_profiles: Map, +// waitlist: Map, // Index -> Address +// waitlist_length: u32, +// total_members: u32, +// admin: ContractAddress, +// } + +// #[event] +// #[derive(Drop, starknet::Event)] +// pub enum Event { +// ProfileCreated: ProfileCreated, +// ProfileUpdated: ProfileUpdated, +// ReliabilityRatingUpdated: ReliabilityRatingUpdated, +// MemberAddedToWaitlist: MemberAddedToWaitlist, +// MemberRemovedFromWaitlist: MemberRemovedFromWaitlist, +// CommunicationSent: CommunicationSent, +// ContributionRecorded: ContributionRecorded, +// MissedContributionRecorded: MissedContributionRecorded, +// PaymentMethodUpdated: PaymentMethodUpdated, +// } + +// #[derive(Drop, starknet::Event)] +// pub struct ProfileCreated { +// member: ContractAddress, +// timestamp: u64, +// } + +// #[derive(Drop, starknet::Event)] +// pub struct ProfileUpdated { +// member: ContractAddress, +// updated_by: ContractAddress, +// timestamp: u64, +// } + +// #[derive(Drop, starknet::Event)] +// pub struct ReliabilityRatingUpdated { +// member: ContractAddress, +// old_rating: u8, +// new_rating: u8, +// timestamp: u64, +// } + +// #[derive(Drop, starknet::Event)] +// pub struct MemberAddedToWaitlist { +// member: ContractAddress, +// position: u32, +// timestamp: u64, +// } + +// #[derive(Drop, starknet::Event)] +// pub struct MemberRemovedFromWaitlist { +// member: ContractAddress, +// timestamp: u64, +// } + +// #[derive(Drop, starknet::Event)] +// pub struct CommunicationSent { +// message_hash: felt252, +// recipients_count: u32, +// timestamp: u64, +// } + +// #[derive(Drop, starknet::Event)] +// pub struct ContributionRecorded { +// member: ContractAddress, +// amount: u256, +// round_id: u256, +// timestamp: u64, +// } + +// #[derive(Drop, starknet::Event)] +// pub struct MissedContributionRecorded { +// member: ContractAddress, +// round_id: u256, +// timestamp: u64, +// } + +// #[derive(Drop, starknet::Event)] +// pub struct PaymentMethodUpdated { +// member: ContractAddress, +// old_method: felt252, +// new_method: felt252, +// timestamp: u64, +// } + +// impl MemberProfileImpl< +// TContractState, +HasComponent, +// > of super::IMemberProfile> { +// fn create_member_profile(ref self: ComponentState, member: ContractAddress) { +// // Check if profile already exists +// let existing_profile = self.member_profiles.read(member); +// assert(existing_profile.join_date == 0, Errors::PROFILE_ALREADY_EXISTS); + +// let current_time = get_block_timestamp(); +// let new_profile = MemberProfile { +// join_date: current_time, +// total_contributions: 0, +// missed_contributions: 0, +// credit_score: 50, // Start with neutral score +// last_recipient_round: 0, +// reliability_rating: 50, // Start with neutral rating +// preferred_payment_method: 'DEFAULT', +// communication_preferences: 'ALL', +// }; + +// self.member_profiles.write(member, new_profile); +// let total = self.total_members.read(); +// self.total_members.write(total + 1); + +// self.emit(Event::ProfileCreated(ProfileCreated { +// member, +// timestamp: current_time, +// })); +// } + +// fn update_reliability_rating(ref self: ComponentState, member: ContractAddress, new_rating: u8) { +// self._assert_admin(); +// assert(new_rating <= 100, Errors::INVALID_RATING); + +// let mut profile = self.member_profiles.read(member); +// assert(profile.join_date != 0, Errors::PROFILE_NOT_FOUND); + +// let old_rating = profile.reliability_rating; +// profile.reliability_rating = new_rating; +// self.member_profiles.write(member, profile); + +// self.emit(Event::ReliabilityRatingUpdated(ReliabilityRatingUpdated { +// member, +// old_rating, +// new_rating, +// timestamp: get_block_timestamp(), +// })); +// } + +// fn get_member_profile(self: @ComponentState, member: ContractAddress) -> MemberProfile { +// let profile = self.member_profiles.read(member); +// assert(profile.join_date != 0, Errors::PROFILE_NOT_FOUND); +// profile +// } +// } + +// // Additional public functions for enhanced member management +// impl AdditionalMemberProfileImpl< +// TContractState, +HasComponent, +// > of AdditionalMemberProfileTrait { +// fn add_to_waitlist(ref self: ComponentState, member: ContractAddress) { +// let current_length = self.waitlist_length.read(); +// self.waitlist.write(current_length, member); +// self.waitlist_length.write(current_length + 1); + +// self.emit(Event::MemberAddedToWaitlist(MemberAddedToWaitlist { +// member, +// position: current_length + 1, +// timestamp: get_block_timestamp(), +// })); +// } + +// fn remove_from_waitlist(ref self: ComponentState, member: ContractAddress) -> bool { +// let waitlist_length = self.waitlist_length.read(); +// let mut found = false; +// let mut i = 0; + +// // Find member in waitlist +// loop { +// if i >= waitlist_length { +// break; +// } + +// if self.waitlist.read(i) == member { +// found = true; +// // Shift remaining members +// let mut j = i; +// loop { +// if j + 1 >= waitlist_length { +// break; +// } +// let next_member = self.waitlist.read(j + 1); +// self.waitlist.write(j, next_member); +// j += 1; +// }; +// break; +// } +// i += 1; +// }; + +// if found { +// self.waitlist_length.write(waitlist_length - 1); +// self.emit(Event::MemberRemovedFromWaitlist(MemberRemovedFromWaitlist { +// member, +// timestamp: get_block_timestamp(), +// })); +// } + +// found +// } + +// fn record_contribution(ref self: ComponentState, member: ContractAddress, amount: u256, round_id: u256) { +// let mut profile = self.member_profiles.read(member); +// profile.total_contributions += amount; +// profile.credit_score = self._calculate_credit_score(@profile); +// self.member_profiles.write(member, profile); + +// self.emit(Event::ContributionRecorded(ContributionRecorded { +// member, +// amount, +// round_id, +// timestamp: get_block_timestamp(), +// })); +// } + +// fn record_missed_contribution(ref self: ComponentState, member: ContractAddress, round_id: u256) { +// let mut profile = self.member_profiles.read(member); +// profile.missed_contributions += 1; +// profile.credit_score = self._calculate_credit_score(@profile); +// // Decrease reliability rating +// if profile.reliability_rating > 5 { +// profile.reliability_rating -= 5; +// } else { +// profile.reliability_rating = 0; +// } +// self.member_profiles.write(member, profile); + +// self.emit(Event::MissedContributionRecorded(MissedContributionRecorded { +// member, +// round_id, +// timestamp: get_block_timestamp(), +// })); +// } + +// fn update_payment_method(ref self: ComponentState, member: ContractAddress, new_method: felt252) { +// let caller = get_caller_address(); +// assert(caller == member || caller == self.admin.read(), Errors::NOT_ADMIN); + +// let mut profile = self.member_profiles.read(member); +// assert(profile.join_date != 0, Errors::PROFILE_NOT_FOUND); + +// let old_method = profile.preferred_payment_method; +// profile.preferred_payment_method = new_method; +// self.member_profiles.write(member, profile); + +// self.emit(Event::PaymentMethodUpdated(PaymentMethodUpdated { +// member, +// old_method, +// new_method, +// timestamp: get_block_timestamp(), +// })); +// } + +// fn send_communication(ref self: ComponentState, message_hash: felt252, recipients_count: u32) { +// self._assert_admin(); + +// self.emit(Event::CommunicationSent(CommunicationSent { +// message_hash, +// recipients_count, +// timestamp: get_block_timestamp(), +// })); +// } + +// fn get_waitlist_position(self: @ComponentState, member: ContractAddress) -> u32 { +// let waitlist_length = self.waitlist_length.read(); +// let mut i = 0; + +// loop { +// if i >= waitlist_length { +// break 0; // Not found +// } + +// if self.waitlist.read(i) == member { +// break i + 1; // Position is 1-indexed +// } +// i += 1; +// } +// } + +// fn get_total_members(self: @ComponentState) -> u32 { +// self.total_members.read() +// } + +// fn get_waitlist_length(self: @ComponentState) -> u32 { +// self.waitlist_length.read() +// } +// } + +// #[generate_trait] +// pub trait AdditionalMemberProfileTrait { +// fn add_to_waitlist(ref self: ComponentState, member: ContractAddress); +// fn remove_from_waitlist(ref self: ComponentState, member: ContractAddress) -> bool; +// fn record_contribution(ref self: ComponentState, member: ContractAddress, amount: u256, round_id: u256); +// fn record_missed_contribution(ref self: ComponentState, member: ContractAddress, round_id: u256); +// fn update_payment_method(ref self: ComponentState, member: ContractAddress, new_method: felt252); +// fn send_communication(ref self: ComponentState, message_hash: felt252, recipients_count: u32); +// fn get_waitlist_position(self: @ComponentState, member: ContractAddress) -> u32; +// fn get_total_members(self: @ComponentState) -> u32; +// fn get_waitlist_length(self: @ComponentState) -> u32; +// } + +// #[generate_trait] +// pub impl InternalImpl< +// TContractState, +HasComponent, +// > of super::IMemberProfileInternal { +// fn initializer(ref self: ComponentState) { +// self.admin.write(get_caller_address()); +// self.total_members.write(0); +// self.waitlist_length.write(0); +// } + +// fn _assert_admin(self: @ComponentState) { +// let admin: ContractAddress = self.admin.read(); +// let caller: ContractAddress = get_caller_address(); +// assert(caller == admin, Errors::NOT_ADMIN); +// } + +// fn _calculate_credit_score(self: @ComponentState, profile: @MemberProfile) -> u8 { +// let total_contributions = *profile.total_contributions; +// let missed_contributions = *profile.missed_contributions; + +// if total_contributions == 0 && missed_contributions == 0 { +// return 50; // Neutral score for new members +// } + +// // Simple calculation: start with 50, add for successful contributions, subtract for missed +// let base_score = 50; +// let contribution_boost = if total_contributions > 0 { +// let contribution_count = total_contributions / 100; // Assuming each contribution is ~100 units +// if contribution_count > 50 { 50 } else { contribution_count.try_into().unwrap() } +// } else { 0 }; + +// let penalty = missed_contributions * 10; // 10 points per missed contribution + +// let calculated_score = base_score + contribution_boost - penalty.into(); + +// if calculated_score > 100 { 100 } +// else if calculated_score < 0 { 0 } +// else { calculated_score } +// } + +// fn _update_contribution_stats(ref self: ComponentState, member: ContractAddress, amount: u256) { +// let mut profile = self.member_profiles.read(member); +// profile.total_contributions += amount; +// profile.credit_score = self._calculate_credit_score(@profile); +// self.member_profiles.write(member, profile); +// } +// } +// } diff --git a/src/component/payment_flexibility.cairo b/src/component/payment_flexibility.cairo new file mode 100644 index 0000000..63a1ece --- /dev/null +++ b/src/component/payment_flexibility.cairo @@ -0,0 +1,619 @@ +use starknet::ContractAddress; +use starkremit_contract::base::types::{RoundStatus, RoundData}; +use starkremit_contract::base::errors::PaymentFlexibilityErrors; + +// Trait that the main contract must implement to provide data access +pub trait IMainContractData { + fn get_round_data(self: @TContractState, round_id: u256) -> RoundData; + fn get_member_status(self: @TContractState, member: ContractAddress) -> bool; + fn get_member_count(self: @TContractState) -> u32; + fn get_member_by_index(self: @TContractState, index: u32) -> ContractAddress; +} + +#[starknet::interface] +pub trait IPaymentFlexibility { + // Configuration and query functions (simple operations) + fn get_payment_config(self: @TContractState) -> PaymentConfig; + fn get_auto_payment_setup(self: @TContractState, member: ContractAddress) -> AutoPaymentSetup; + fn get_payment_status(self: @TContractState, member: ContractAddress, round_id: u256) -> PaymentStatus; + fn get_supported_tokens(self: @TContractState) -> Array; + fn is_token_supported(self: @TContractState, token: ContractAddress) -> bool; + + // Utility functions (simple operations) + fn get_grace_period_extension(self: @TContractState, member: ContractAddress) -> u64; + fn get_early_payment_discount(self: @TContractState, amount: u256) -> u256; +} + +// Data structures for payment flexibility functionality +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct PaymentConfig { + pub grace_period_hours: u64, + pub early_payment_discount_basis_points: u256, // E.g., 500 for 5% + pub auto_payment_enabled: bool, + pub usd_oracle_address: ContractAddress, + pub max_grace_period_extension: u64, // Maximum extension in hours + pub min_early_payment_days: u64, // Minimum days before deadline for early payment +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +pub enum PaymentFrequency { + Once, + Daily, + Weekly, + Monthly, +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct AutoPaymentSetup { + pub member: ContractAddress, + pub token: ContractAddress, + pub amount: u256, + pub frequency: PaymentFrequency, + pub next_payment_date: u64, + pub is_active: bool, + pub created_at: u64, + pub last_payment_date: u64, +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +pub enum PaymentStatus { + Pending, + Paid, + Late, + Missed, + Overpaid, + Early, +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct PaymentRecord { + pub member: ContractAddress, + pub round_id: u256, + pub amount: u256, + pub token: ContractAddress, + pub payment_date: u64, + pub status: PaymentStatus, + pub is_early_payment: bool, + pub discount_applied: u256, + pub grace_period_used: u64, +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct EarlyPaymentInfo { + pub member: ContractAddress, + pub round_id: u256, + pub original_amount: u256, + pub discount_amount: u256, + pub final_amount: u256, + pub payment_date: u64, +} + +#[starknet::component] +pub mod payment_flexibility_component { + use starknet::{ContractAddress, get_block_timestamp, get_caller_address}; + use starknet::storage::{ + Map, StoragePointerReadAccess, StoragePointerWriteAccess, StorageMapReadAccess, StorageMapWriteAccess, + }; + use core::array::ArrayTrait; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + use super::{PaymentConfig, PaymentFrequency, AutoPaymentSetup, PaymentStatus, PaymentRecord, EarlyPaymentInfo, IMainContractData}; + use starkremit_contract::base::errors::PaymentFlexibilityErrors; + use super::*; + + const SECONDS_PER_HOUR: u64 = 3600; + const SECONDS_PER_DAY: u64 = 86400; + const BASIS_POINTS: u256 = 10000; + + #[storage] + pub struct Storage { + payment_config: PaymentConfig, + auto_payment_setups: Map, + payment_records: Map<(ContractAddress, u256), PaymentRecord>, // (member, round_id) -> record + supported_tokens: Map, // Index -> Token + supported_tokens_count: u32, + early_payments: Map, // round_id -> early payment info + grace_period_extensions: Map, // member -> extension hours + admin: ContractAddress, + last_auto_payment_processing: u64, + auto_payment_interval: u64, // How often to process auto-payments + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + AutoPaymentSetup: AutoPaymentSetupEvent, + EarlyPaymentProcessed: EarlyPaymentProcessed, + GracePeriodExtended: GracePeriodExtended, + TokenValueConverted: TokenValueConverted, + PaymentStatusUpdated: PaymentStatusUpdated, + AutoPaymentExecuted: AutoPaymentExecuted, + SupportedTokenAdded: SupportedTokenAdded, + SupportedTokenRemoved: SupportedTokenRemoved, + PaymentConfigUpdated: PaymentConfigUpdated, + GracePeriodUsed: GracePeriodUsed, + } + + #[derive(Drop, starknet::Event)] + pub struct AutoPaymentSetupEvent { + member: ContractAddress, + token: ContractAddress, + amount: u256, + frequency: PaymentFrequency, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct EarlyPaymentProcessed { + member: ContractAddress, + round_id: u256, + original_amount: u256, + discount_amount: u256, + final_amount: u256, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct GracePeriodExtended { + member: ContractAddress, + extension_hours: u64, + new_deadline: u64, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct TokenValueConverted { + from_token: ContractAddress, + to_token: ContractAddress, + input_amount: u256, + output_amount: u256, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct PaymentStatusUpdated { + member: ContractAddress, + round_id: u256, + old_status: PaymentStatus, + new_status: PaymentStatus, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct AutoPaymentExecuted { + member: ContractAddress, + amount: u256, + token: ContractAddress, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct SupportedTokenAdded { + token: ContractAddress, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct SupportedTokenRemoved { + token: ContractAddress, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct PaymentConfigUpdated { + admin: ContractAddress, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct GracePeriodUsed { + member: ContractAddress, + round_id: u256, + extension_hours: u64, + timestamp: u64, + } + + #[embeddable_as(PaymentFlexibility)] + impl PaymentFlexibilityImpl< + TContractState, +HasComponent, +IMainContractData, + > of super::IPaymentFlexibility> { + + fn get_payment_config(self: @ComponentState) -> PaymentConfig { + self.payment_config.read() + } + + fn get_auto_payment_setup(self: @ComponentState, member: ContractAddress) -> AutoPaymentSetup { + self.auto_payment_setups.read(member) + } + + fn get_payment_status(self: @ComponentState, member: ContractAddress, round_id: u256) -> PaymentStatus { + self._calculate_payment_status(member, round_id) + } + + fn get_supported_tokens(self: @ComponentState) -> Array { + let mut tokens = ArrayTrait::new(); + let count = self.supported_tokens_count.read(); + let mut i = 0; + while i < count { + let token = self.supported_tokens.read(i); + tokens.append(token); + i += 1; + } + tokens + } + + fn is_token_supported(self: @ComponentState, token: ContractAddress) -> bool { + self._is_token_supported(token) + } + + fn get_grace_period_extension(self: @ComponentState, member: ContractAddress) -> u64 { + self.grace_period_extensions.read(member) + } + + fn get_early_payment_discount(self: @ComponentState, amount: u256) -> u256 { + let config = self.payment_config.read(); + (amount * config.early_payment_discount_basis_points) / BASIS_POINTS + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, +HasComponent, +IMainContractData, + > of InternalTrait { + + fn initializer(ref self: ComponentState, admin: ContractAddress) { + self.admin.write(admin); + + // Set default payment configuration + let default_config = PaymentConfig { + grace_period_hours: 48, // 48 hours default grace period + early_payment_discount_basis_points: 500, // 5% discount for early payments + auto_payment_enabled: true, + usd_oracle_address: 0.try_into().unwrap(), // No oracle by default + max_grace_period_extension: 168, // Maximum 1 week extension + min_early_payment_days: 7, // Minimum 7 days before deadline for early payment + }; + self.payment_config.write(default_config); + + // Initialize auto-payment processing + self.last_auto_payment_processing.write(get_block_timestamp()); + self.auto_payment_interval.write(3600); // Process every hour + + // Initialize supported tokens count + self.supported_tokens_count.write(0); + } + + // Internal function to assert that the caller is the admin + fn _assert_admin(self: @ComponentState) { + let admin: ContractAddress = self.admin.read(); + let caller: ContractAddress = get_caller_address(); + assert(caller == admin, PaymentFlexibilityErrors::NOT_ADMIN); + } + + // Complex operations that will be called by the main contract + fn _setup_auto_payment( + ref self: ComponentState, + member: ContractAddress, + token: ContractAddress, + amount: u256, + frequency: PaymentFrequency, + ) { + let config = self.payment_config.read(); + assert(config.auto_payment_enabled, PaymentFlexibilityErrors::AUTO_PAYMENT_DISABLED); + assert(amount > 0, PaymentFlexibilityErrors::INVALID_AMOUNT); + assert(self._is_token_supported(token), PaymentFlexibilityErrors::INVALID_TOKEN); + + // Check if member already has auto-payment setup + let existing_setup = self.auto_payment_setups.read(member); + assert(!existing_setup.is_active, PaymentFlexibilityErrors::AUTO_PAYMENT_ACTIVE); + + // Calculate next payment date based on frequency + let next_payment_date = self._calculate_next_payment_date(frequency); + + // Create auto-payment setup + let auto_payment = AutoPaymentSetup { + member, + token, + amount, + frequency, + next_payment_date, + is_active: true, + created_at: get_block_timestamp(), + last_payment_date: 0, + }; + + self.auto_payment_setups.write(member, auto_payment); + + self.emit(Event::AutoPaymentSetup(AutoPaymentSetupEvent { + member, + token, + amount, + frequency, + timestamp: get_block_timestamp(), + })); + } + + fn _process_early_payment( + ref self: ComponentState, + member: ContractAddress, + round_id: u256, + amount: u256, + ) -> (u256, u256) { + let config = self.payment_config.read(); + let contract_state = self.get_contract(); + let round = contract_state.get_round_data(round_id); + + // Validate round is active + assert(round.status == RoundStatus::Active, PaymentFlexibilityErrors::ROUND_NOT_FOUND); + + // Check if payment is early enough to qualify for discount + let current_time = get_block_timestamp(); + let days_until_deadline = (round.deadline - current_time) / SECONDS_PER_DAY; + assert(days_until_deadline >= config.min_early_payment_days, PaymentFlexibilityErrors::INVALID_AMOUNT); + + // Calculate early payment discount + let discount_amount = self._calculate_early_payment_discount(amount); + let final_amount = amount - discount_amount; + + // Store early payment info + let early_payment = EarlyPaymentInfo { + member, + round_id, + original_amount: amount, + discount_amount, + final_amount, + payment_date: current_time, + }; + self.early_payments.write(round_id, early_payment); + + // Update payment record + let payment_record = PaymentRecord { + member, + round_id, + amount: final_amount, + token: 0.try_into().unwrap(), // Default token + payment_date: current_time, + status: PaymentStatus::Early, + is_early_payment: true, + discount_applied: discount_amount, + grace_period_used: 0, + }; + self.payment_records.write((member, round_id), payment_record); + + self.emit(Event::EarlyPaymentProcessed(EarlyPaymentProcessed { + member, + round_id, + original_amount: amount, + discount_amount, + final_amount, + timestamp: current_time, + })); + + (final_amount, discount_amount) + } + + fn _extend_grace_period( + ref self: ComponentState, + member: ContractAddress, + extension_hours: u64, + ) { + self._assert_admin(); + + let config = self.payment_config.read(); + assert(extension_hours > 0, PaymentFlexibilityErrors::INVALID_AMOUNT); + assert(extension_hours <= config.max_grace_period_extension, PaymentFlexibilityErrors::INVALID_AMOUNT); + + // Get current grace period extension + let current_extension = self.grace_period_extensions.read(member); + let new_extension = current_extension + extension_hours; + + // Update grace period extension + self.grace_period_extensions.write(member, new_extension); + + self.emit(Event::GracePeriodExtended(GracePeriodExtended { + member, + extension_hours, + new_deadline: get_block_timestamp() + (new_extension * SECONDS_PER_HOUR), + timestamp: get_block_timestamp(), + })); + } + + fn _process_auto_payments(ref self: ComponentState) { + let config = self.payment_config.read(); + if !config.auto_payment_enabled { + return; + } + + let current_time = get_block_timestamp(); + let last_processing = self.last_auto_payment_processing.read(); + let interval = self.auto_payment_interval.read(); + + // Check if it's time to process auto-payments + if current_time < last_processing + interval { + return; + } + + let mut processed_count = 0; + let member_count = self._get_member_count(); + + // Process auto-payments for all members + let mut i = 0; + while i < member_count { + let member = self._get_member_by_index(i); + let auto_setup = self.auto_payment_setups.read(member); + + if auto_setup.is_active && current_time >= auto_setup.next_payment_date { + // Execute auto-payment + self._execute_auto_payment(member, auto_setup); + processed_count += 1; + } + i += 1; + } + + // Update last processing timestamp + self.last_auto_payment_processing.write(current_time); + + if processed_count > 0_u32 { + self.emit(Event::AutoPaymentExecuted(AutoPaymentExecuted { + member: 0.try_into().unwrap(), // Not applicable for bulk processing + amount: 0, // Not applicable for bulk processing + token: 0.try_into().unwrap(), // Not applicable for bulk processing + timestamp: current_time, + })); + } + } + + fn _add_supported_token(ref self: ComponentState, token: ContractAddress) { + self._assert_admin(); + + // Check if token is already supported + assert(!self._is_token_supported(token), PaymentFlexibilityErrors::INVALID_TOKEN); + + let count = self.supported_tokens_count.read(); + self.supported_tokens.write(count, token); + self.supported_tokens_count.write(count + 1); + + self.emit(Event::SupportedTokenAdded(SupportedTokenAdded { + token, + timestamp: get_block_timestamp(), + })); + } + + fn _remove_supported_token(ref self: ComponentState, token: ContractAddress) { + self._assert_admin(); + + // Check if token is supported + assert(self._is_token_supported(token), PaymentFlexibilityErrors::INVALID_TOKEN); + + // Find and remove token + let count = self.supported_tokens_count.read(); + let mut i = 0; + while i < count { + let stored_token = self.supported_tokens.read(i); + if stored_token == token { + // Remove by setting to zero address + self.supported_tokens.write(i, 0.try_into().unwrap()); + break; + } + i += 1; + } + + self.emit(Event::SupportedTokenRemoved(SupportedTokenRemoved { + token, + timestamp: get_block_timestamp(), + })); + } + + fn _update_payment_config(ref self: ComponentState, new_config: PaymentConfig) { + self._assert_admin(); + + let old_config = self.payment_config.read(); + self.payment_config.write(new_config); + + self.emit(Event::PaymentConfigUpdated(PaymentConfigUpdated { + admin: get_caller_address(), + timestamp: get_block_timestamp(), + })); + } + + // Helper functions + fn _calculate_payment_status( + self: @ComponentState, + member: ContractAddress, + round_id: u256, + ) -> PaymentStatus { + let contract_state = self.get_contract(); + let round = contract_state.get_round_data(round_id); + let payment_record = self.payment_records.read((member, round_id)); + let current_time = get_block_timestamp(); + let config = self.payment_config.read(); + + if payment_record.amount == 0 { + // No payment made + let grace_period_end = round.deadline + (config.grace_period_hours * SECONDS_PER_HOUR); + let member_extension = self.grace_period_extensions.read(member); + let extended_deadline = grace_period_end + (member_extension * SECONDS_PER_HOUR); + + if current_time > extended_deadline { + return PaymentStatus::Missed; + } else if current_time > round.deadline { + return PaymentStatus::Late; + } else { + return PaymentStatus::Pending; + } + } else { + // Payment made + if payment_record.is_early_payment { + return PaymentStatus::Early; + } else if current_time <= round.deadline { + return PaymentStatus::Paid; + } else if current_time <= round.deadline + (config.grace_period_hours * SECONDS_PER_HOUR) { + return PaymentStatus::Late; + } else { + return PaymentStatus::Overpaid; // Payment made after grace period + } + } + } + + fn _calculate_next_payment_date(self: @ComponentState, frequency: PaymentFrequency) -> u64 { + let current_time = get_block_timestamp(); + + match frequency { + PaymentFrequency::Once => current_time, + PaymentFrequency::Daily => current_time + SECONDS_PER_DAY, + PaymentFrequency::Weekly => current_time + (SECONDS_PER_DAY * 7), + PaymentFrequency::Monthly => current_time + (SECONDS_PER_DAY * 30), + } + } + + fn _calculate_early_payment_discount(self: @ComponentState, amount: u256) -> u256 { + let config = self.payment_config.read(); + (amount * config.early_payment_discount_basis_points) / BASIS_POINTS + } + + fn _is_token_supported(self: @ComponentState, token: ContractAddress) -> bool { + let count = self.supported_tokens_count.read(); + let mut i = 0; + while i < count { + let supported_token = self.supported_tokens.read(i); + if supported_token == token { + return true; + } + i += 1; + } + false + } + + fn _execute_auto_payment( + ref self: ComponentState, + member: ContractAddress, + mut auto_setup: AutoPaymentSetup, + ) { + // Update last payment date + auto_setup.last_payment_date = get_block_timestamp(); + + // Calculate next payment date + auto_setup.next_payment_date = self._calculate_next_payment_date(auto_setup.frequency); + + // Save updated auto-payment setup + self.auto_payment_setups.write(member, auto_setup); + + // Emit auto-payment executed event + self.emit(Event::AutoPaymentExecuted(AutoPaymentExecuted { + member, + amount: auto_setup.amount, + token: auto_setup.token, + timestamp: get_block_timestamp(), + })); + } + + fn _get_member_count(self: @ComponentState) -> u32 { + let contract_state = self.get_contract(); + contract_state.get_member_count() + } + + fn _get_member_by_index(self: @ComponentState, index: u32) -> ContractAddress { + let contract_state = self.get_contract(); + contract_state.get_member_by_index(index) + } + } +} diff --git a/src/component/penalty.cairo b/src/component/penalty.cairo new file mode 100644 index 0000000..2a1f3e2 --- /dev/null +++ b/src/component/penalty.cairo @@ -0,0 +1,446 @@ +use starknet::ContractAddress; +use starknet::get_block_timestamp; +use starknet::get_caller_address; +use core::array::{ArrayTrait, Array}; +use core::serde::Serde; +use starkremit_contract::base::types::{PenaltyConfig, MemberPenaltyRecord, MemberContribution, RoundStatus, PenaltyEventRecord, DistributionData, MemberShare, PenaltyEventType, RoundData}; + +// Trait that the main contract must implement to provide data access +pub trait IMainContractData { + fn get_member_contribution_data(self: @TContractState, round_id: u256, member: ContractAddress) -> MemberContribution; + fn get_round_data(self: @TContractState, round_id: u256) -> RoundData; + fn get_member_status(self: @TContractState, member: ContractAddress) -> bool; + fn get_member_count(self: @TContractState) -> u32; + fn get_round_ids(self: @TContractState) -> u256; + fn get_member_by_index(self: @TContractState, index: u32) -> ContractAddress; +} + + +#[starknet::interface] +pub trait IPenalty { + fn set_penalty_config(ref self: TContractState, config: PenaltyConfig); + fn get_penalty_config(self: @TContractState) -> PenaltyConfig; + fn get_member_penalty_record(self: @TContractState, member: ContractAddress) -> MemberPenaltyRecord; + fn get_penalty_pool(self: @TContractState) -> u256; + // Distribution calculation function + fn calculate_distribution_data(self: @TContractState) -> DistributionData; + // Reset penalty pool after distribution + fn reset_penalty_pool(ref self: TContractState); + // History functions + fn get_penalty_history(self: @TContractState, member: ContractAddress, limit: u32, offset: u32) -> Array; +} + + +// Component events +#[derive(Drop, starknet::Event)] +pub struct LateFeeApplied { + pub member: ContractAddress, + pub round_id: u256, + pub fee_amount: u256, + pub contribution_amount: u256, + pub timestamp: u64, +} + +#[derive(Drop, starknet::Event)] +pub struct StrikeAdded { + pub member: ContractAddress, + pub round_id: u256, + pub current_strikes: u32, + pub timestamp: u64, +} + +#[derive(Drop, starknet::Event)] +pub struct StrikeRemoved { + pub member: ContractAddress, + pub removed_by: ContractAddress, + pub new_strikes: u32, + pub timestamp: u64, +} + + +#[derive(Drop, starknet::Event)] +pub struct PenaltyConfigUpdated { + pub old_config: PenaltyConfig, + pub new_config: PenaltyConfig, + pub updated_by: ContractAddress, + pub timestamp: u64, +} + +#[derive(Drop, starknet::Event)] +pub struct PenaltyPoolDistributed { + pub total_amount: u256, + pub recipient_count: u32, + pub distribution_type: felt252, + pub timestamp: u64, +} + +#[derive(Drop, starknet::Event)] +pub struct GracePeriodExtended { + pub member: ContractAddress, + pub extension_hours: u64, + pub total_extension: u64, + pub extended_by: ContractAddress, + pub timestamp: u64, +} + +#[starknet::component] +pub mod penalty_component { + use super::*; + use starknet::storage::{ + Map, StoragePointerReadAccess, StoragePointerWriteAccess, StorageMapReadAccess, StorageMapWriteAccess, + }; + use core::array::ArrayTrait; + use starkremit_contract::base::errors::PenaltyComponentErrors; + + #[storage] + pub struct Storage { + penalty_config: PenaltyConfig, + member_penalties: Map, + penalty_pool: u256, + penalty_history: Map<(ContractAddress, u32), PenaltyEventRecord>, + penalty_history_count: Map, + grace_period_extensions: Map, + admin: ContractAddress, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + LateFeeApplied: LateFeeApplied, + StrikeAdded: StrikeAdded, + StrikeRemoved: StrikeRemoved, + PenaltyConfigUpdated: PenaltyConfigUpdated, + PenaltyPoolDistributed: PenaltyPoolDistributed, + GracePeriodExtended: GracePeriodExtended, + } + + #[embeddable_as(Penalty)] + impl PenaltyImpl< + TContractState, +HasComponent, +IMainContractData, + > of super::IPenalty> { + + fn set_penalty_config(ref self: ComponentState, config: PenaltyConfig) { + self._assert_admin(); + + let old_config = self.penalty_config.read(); + self.penalty_config.write(config); + + self.emit(Event::PenaltyConfigUpdated(PenaltyConfigUpdated { + old_config, + new_config: config, + updated_by: get_caller_address(), + timestamp: get_block_timestamp(), + })); + } + + fn get_penalty_config(self: @ComponentState) -> PenaltyConfig { + self.penalty_config.read() + } + + fn get_member_penalty_record(self: @ComponentState, member: ContractAddress) -> MemberPenaltyRecord { + self.member_penalties.read(member) + } + + fn get_penalty_pool(self: @ComponentState) -> u256 { + self.penalty_pool.read() + } + + fn reset_penalty_pool(ref self: ComponentState) { + self._reset_penalty_pool(); + } + + fn calculate_distribution_data(self: @ComponentState) -> DistributionData { + let contract_state = self.get_contract(); + let penalty_pool_amount = self.penalty_pool.read(); + + if penalty_pool_amount == 0 { + return DistributionData { + total_amount: 0, + member_shares: ArrayTrait::new(), + total_compliant_contributions: 0, + }; + } + + let mut total_compliant_contributions = 0; + let mut member_shares = ArrayTrait::new(); + + // Calculate total contributions from compliant members + let mut member_index = 0; + let total_members = contract_state.get_member_count(); + + while member_index < total_members { + let member_address = contract_state.get_member_by_index(member_index); + if contract_state.get_member_status(member_address) { + let penalty_record = self.member_penalties.read(member_address); + if !penalty_record.is_banned { + // Calculate member's total contribution across all rounds + let mut member_contribution = 0; + let mut round_id = 1; + while round_id <= contract_state.get_round_ids() { + let contribution = contract_state.get_member_contribution_data(round_id, member_address); + member_contribution += contribution.amount; + round_id += 1; + } + + total_compliant_contributions += member_contribution; + + if member_contribution > 0 { + let share = (member_contribution * penalty_pool_amount) / total_compliant_contributions; + if share > 0 { + member_shares.append(MemberShare { + member: member_address, + share, + contribution: member_contribution, + }); + } + } + } + } + member_index += 1; + } + + DistributionData { + total_amount: penalty_pool_amount, + member_shares, + total_compliant_contributions, + } + } + + fn get_penalty_history( + self: @ComponentState, + member: ContractAddress, + limit: u32, + offset: u32 + ) -> Array { + let mut history = ArrayTrait::new(); + let total_count = self.penalty_history_count.read(member); + + let mut i = offset; + let mut count = 0; + + while i < total_count && count < limit { + let event = self.penalty_history.read((member, i)); + history.append(event); + count += 1; + i += 1; + } + + history + } + } + + + + #[generate_trait] + pub impl InternalImpl< + TContractState, +HasComponent, +IMainContractData, + > of InternalTrait { + + fn initializer(ref self: ComponentState, admin: ContractAddress) { + self.admin.write(admin); + + // Set default penalty configuration + let default_config = PenaltyConfig { + late_fee_percentage: 250, // 2.5% in basis points + grace_period_hours: 48, // 48 hours + max_strikes: 2, // 2 strikes before ban + security_deposit_multiplier: 100000000000000000000, // 100 tokens + penalty_pool_enabled: true, + }; + self.penalty_config.write(default_config); + + // Initialize penalty pool + self.penalty_pool.write(0); + } + + fn _assert_admin(self: @ComponentState) { + let admin = self.admin.read(); + assert(get_caller_address() == admin, PenaltyComponentErrors::NOT_ADMIN); + } + + // Core penalty functions that need access to main contract data + fn apply_late_fee( + ref self: ComponentState, + member: ContractAddress, + round_id: u256 + ) { + // Get main contract state to access member contributions and rounds + let contract_state = self.get_contract(); + + // Get penalty configuration and round data + let penalty_config = self.penalty_config.read(); + let round = contract_state.get_round_data(round_id); + let member_ext: u64 = self.grace_period_extensions.read(member); + let total_grace_secs: u64 = penalty_config.grace_period_hours * 3600 + member_ext; + assert(get_block_timestamp() > round.deadline + total_grace_secs, PenaltyComponentErrors::NOT_LATE); + + // If pool is disabled, do not collect late fees into pool + assert(penalty_config.penalty_pool_enabled, PenaltyComponentErrors::PENALTY_POOL_DISABLED); + + // Get member contribution from main contract storage via trait + let contribution = contract_state.get_member_contribution_data(round_id, member); + assert(contribution.amount > 0, PenaltyComponentErrors::NO_CONTRIBUTION_FOR_ROUND); + + // Calculate late fee + let late_fee = (contribution.amount * penalty_config.late_fee_percentage) / 10000; + + // Update penalty pool + self._update_penalty_pool(late_fee); + + // Update member penalty record + let mut penalty_record = self.member_penalties.read(member); + penalty_record.total_penalties_paid += late_fee; + penalty_record.last_penalty_date = get_block_timestamp(); + self.member_penalties.write(member, penalty_record); + + // Record penalty event + self._record_penalty_event(member, round_id, PenaltyEventType::LateFee, late_fee); + + // Emit component event + self.emit(Event::LateFeeApplied(LateFeeApplied { + member, + round_id, + fee_amount: late_fee, + contribution_amount: contribution.amount, + timestamp: get_block_timestamp(), + })); + } + + fn add_strike( + ref self: ComponentState, + member: ContractAddress, + round_id: u256 + ) { + // Get penalty configuration from component storage + let penalty_config = self.penalty_config.read(); + + // Get current penalty record + let mut penalty_record = self.member_penalties.read(member); + penalty_record.strikes += 1; + penalty_record.last_penalty_date = get_block_timestamp(); + + // Check if member should be banned + if penalty_record.strikes >= penalty_config.max_strikes { + penalty_record.is_banned = true; + } + + // Save updated penalty record + self.member_penalties.write(member, penalty_record); + + // Record penalty event + self._record_penalty_event(member, round_id, PenaltyEventType::Strike, 0); + + // Emit strike event + self.emit(Event::StrikeAdded(StrikeAdded { + member, + round_id, + current_strikes: penalty_record.strikes, + timestamp: get_block_timestamp(), + })); + } + + fn remove_strike( + ref self: ComponentState, + member: ContractAddress + ) { + // Get current penalty record + let mut penalty_record = self.member_penalties.read(member); + + if penalty_record.strikes > 0 { + penalty_record.strikes -= 1; + + // Check if member should be unbanned + let penalty_config = self.penalty_config.read(); + if penalty_record.is_banned && penalty_record.strikes < penalty_config.max_strikes { + penalty_record.is_banned = false; + + // Note: Member re-addition is handled by the main contract + // The component only manages its own penalty state + } + + // Save updated penalty record + self.member_penalties.write(member, penalty_record); + + // Record penalty event + self._record_penalty_event(member, 0, PenaltyEventType::StrikeRemoved, 0); + + // Emit event + self.emit(Event::StrikeRemoved(StrikeRemoved { + member, + removed_by: get_caller_address(), + new_strikes: penalty_record.strikes, + timestamp: get_block_timestamp(), + })); + } + } + + fn ban_member( + ref self: ComponentState, + member: ContractAddress + ) { + // Get current penalty record + let mut penalty_record = self.member_penalties.read(member); + penalty_record.is_banned = true; + penalty_record.strikes = self.penalty_config.read().max_strikes; + + // Save updated penalty record + self.member_penalties.write(member, penalty_record); + + // Note: Member removal is handled by the main contract + // The component only manages its own penalty state + + // Record penalty event + self._record_penalty_event(member, 0, PenaltyEventType::Ban, 0); + } + + fn unban_member( + ref self: ComponentState, + member: ContractAddress + ) { + // Get current penalty record + let mut penalty_record = self.member_penalties.read(member); + penalty_record.is_banned = false; + penalty_record.strikes = 0; + + // Save updated penalty record + self.member_penalties.write(member, penalty_record); + + // Note: Member re-addition is handled by the main contract + // The component only manages its own penalty state + + // Record penalty event + self._record_penalty_event(member, 0, PenaltyEventType::Unban, 0); + } + + fn _reset_penalty_pool(ref self: ComponentState) { + self.penalty_pool.write(0); + } + + + fn _record_penalty_event( + ref self: ComponentState, + member: ContractAddress, + round_id: u256, + event_type: PenaltyEventType, + amount: u256, + ) { + let history_count = self.penalty_history_count.read(member); + let event = PenaltyEventRecord { + member, + round_id, + event_type, + amount, + timestamp: get_block_timestamp(), + admin: get_caller_address(), + }; + + self.penalty_history.write((member, history_count), event); + self.penalty_history_count.write(member, history_count + 1); + } + + fn _update_penalty_pool(ref self: ComponentState, amount: u256) { + let current_pool = self.penalty_pool.read(); + self.penalty_pool.write(current_pool + amount); + } + } +} diff --git a/src/interfaces/IStarkRemit.cairo b/src/interfaces/IStarkRemit.cairo index 64bc20d..0b2038c 100644 --- a/src/interfaces/IStarkRemit.cairo +++ b/src/interfaces/IStarkRemit.cairo @@ -271,4 +271,51 @@ pub trait IStarkRemit { // Utility Function fn get_timelock_duration(self: @TContractState) -> u64; + + // Emergency Pause Functions + fn emergency_pause_contract(ref self: TContractState, reason: felt252); + fn emergency_unpause_contract(ref self: TContractState); + fn emergency_pause_with_metadata(ref self: TContractState, reason: felt252); + fn emergency_unpause_with_metadata_clear(ref self: TContractState); + fn emergency_set_pause_meta(ref self: TContractState, reason: felt252); + fn emergency_set_ban(ref self: TContractState, member: ContractAddress, banned: bool); + + // Penalty Management Functions + fn apply_late_fee(ref self: TContractState, member: ContractAddress, round_id: u256); + fn add_strike(ref self: TContractState, member: ContractAddress, round_id: u256); + fn remove_strike(ref self: TContractState, member: ContractAddress); + fn ban_member(ref self: TContractState, member: ContractAddress); + fn unban_member(ref self: TContractState, member: ContractAddress); + fn distribute_penalty_pool(ref self: TContractState); + + // Auto-Schedule Management Functions + fn setup_auto_schedule(ref self: TContractState, config: AutoScheduleConfig); + fn maintain_rolling_schedule(ref self: TContractState); + fn auto_activate_round(ref self: TContractState, round_id: u256); + fn auto_complete_expired_rounds(ref self: TContractState); + fn modify_schedule(ref self: TContractState, round_id: u256, new_deadline: u64); + + // Payment Flexibility Functions + fn setup_auto_payment( + ref self: TContractState, + member: ContractAddress, + token: ContractAddress, + amount: u256, + frequency: PaymentFrequency, + ); + fn process_early_payment( + ref self: TContractState, + member: ContractAddress, + round_id: u256, + amount: u256, + ) -> (u256, u256); + fn extend_grace_period( + ref self: TContractState, + member: ContractAddress, + extension_hours: u64, + ); + fn add_supported_token(ref self: TContractState, token: ContractAddress); + fn remove_supported_token(ref self: TContractState, token: ContractAddress); + fn update_payment_config(ref self: TContractState, config: PaymentConfig); + fn process_auto_payments(ref self: TContractState); } diff --git a/src/lib.cairo b/src/lib.cairo index ae92e66..9a78fa2 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -28,10 +28,16 @@ pub mod component { pub mod mock; pub mod test; } + pub mod emergency; + pub mod penalty; pub mod kyc; pub mod loan; pub mod savings_group; pub mod token_management; pub mod transfer; pub mod user_management; + pub mod auto_schedule; + pub mod member_profile; + pub mod payment_flexibility; + pub mod analytics; } diff --git a/src/starkremit/StarkRemit.cairo b/src/starkremit/StarkRemit.cairo index 2cf127a..e3289ee 100644 --- a/src/starkremit/StarkRemit.cairo +++ b/src/starkremit/StarkRemit.cairo @@ -1,24 +1,35 @@ +use core::array::ArrayTrait; use core::num::traits::Zero; use openzeppelin::access::accesscontrol::AccessControlComponent; use openzeppelin::access::ownable::OwnableComponent; use openzeppelin::introspection::src5::SRC5Component; use openzeppelin::upgrades::UpgradeableComponent; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::storage::{ Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess, }; use starknet::{ContractAddress, get_block_timestamp, get_caller_address}; use starkremit_contract::base::errors::{ - GovernanceErrors, GroupErrors, KYCErrors, RegistrationErrors, TransferErrors, + GovernanceErrors, GroupErrors, KYCErrors, RegistrationErrors, TransferErrors, EmergencyErrors, }; use starkremit_contract::base::events::*; use starkremit_contract::base::types::{ Agent, AgentStatus, ContributionRound, GovRole, KYCLevel, KycLevel, KycStatus, LoanRequest, LoanStatus, MemberContribution, ParameterBounds, ParameterHistory, RegistrationRequest, RegistrationStatus, RoundStatus, SavingsGroup, TimelockChange, TransferData, TransferHistory, - TransferStatus, UserKycData, UserProfile, + TransferStatus, UserKycData, UserProfile, PenaltyConfig, MemberPenaltyRecord, PenaltyEventRecord, DistributionData, MemberShare, PenaltyEventType, + AutoScheduleConfig, ScheduledRound, RoundData, }; use starkremit_contract::interfaces::IStarkRemit; +use starkremit_contract::component::emergency::IEmergency; +use starkremit_contract::component::penalty::{IPenalty, IMainContractData as PenaltyMainContractData}; +use starkremit_contract::component::auto_schedule::{IAutoSchedule, IMainContractData as AutoScheduleMainContractData}; +use starkremit_contract::component::payment_flexibility::{PaymentConfig, PaymentFrequency, AutoPaymentSetup, PaymentStatus, PaymentRecord, IMainContractData as PaymentFlexibilityMainContractData}; +// use starkremit_contract::component::member_profile::MemberProfile; +// use starkremit_contract::component::analytics::{ +// ContributionAnalytics, MemberAnalytics, RoundPerformanceMetrics, FinancialReport, SystemHealthMetrics +// }; const INTEREST_RATE: u256 = 500; // 5% in basis points (0.05 * 10000) @@ -36,6 +47,13 @@ pub mod StarkRemit { use starkremit_contract::component::token_management::token_management_component; use starkremit_contract::component::transfer::transfer_component; use starkremit_contract::component::user_management::user_management_component; + use starkremit_contract::component::emergency::emergency_component; + use starkremit_contract::component::penalty::penalty_component; + use starkremit_contract::component::payment_flexibility::payment_flexibility_component; + use starkremit_contract::component::auto_schedule::auto_schedule_component; + // use starkremit_contract::component::member_profile::member_profile_component; + // use starkremit_contract::component::analytics::analytics_component; + use super::*; component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent); @@ -62,6 +80,12 @@ pub mod StarkRemit { event: TokenManagementEvent, ); component!(path: transfer_component, storage: transfer_component, event: TransferEvent); + component!(path: emergency_component, storage: emergency, event: EmergencyEvent); + component!(path: penalty_component, storage: penalty, event: PenaltyEvent); + component!(path: auto_schedule_component, storage: auto_schedule, event: AutoScheduleEvent); + component!(path: payment_flexibility_component, storage: payment_flexibility, event: PaymentFlexibilityEvent); + // component!(path: member_profile_component, storage: member_profile, event: MemberProfileEvent); + // component!(path: analytics_component, storage: analytics, event: AnalyticsEvent); #[abi(embed_v0)] impl AccessControlImpl = @@ -96,6 +120,30 @@ pub mod StarkRemit { // Transfer Component (internal use only - functions exposed via IStarkRemit) impl TransferImpl = transfer_component::Transfer; + // Emergency component internal methods + impl EmergencyInternalImpl = emergency_component::InternalImpl; + + // Penalty Component + impl PenaltyInternalImpl = penalty_component::InternalImpl; + + // Auto Schedule Component (internal use only - functions exposed via IStarkRemit) + impl AutoScheduleInternalImpl = auto_schedule_component::InternalImpl; + + // Payment Flexibility Component (internal use only - functions exposed via IStarkRemit) + impl PaymentFlexibilityInternalImpl = payment_flexibility_component::InternalImpl; + + // Member Profile Component (internal use only - functions exposed via IStarkRemit) + // impl MemberProfileImpl = member_profile_component::MemberProfileImpl; + // impl MemberProfileInternalImpl = member_profile_component::InternalImpl; + + // Payment Flexibility Component (internal use only - functions exposed via IStarkRemit) + // impl PaymentFlexibilityImpl = payment_flexibility_component::PaymentFlexibilityImpl; + // impl PaymentFlexibilityInternalImpl = payment_flexibility_component::InternalImpl; + + // Analytics Component (internal use only - functions exposed via IStarkRemit) + // impl AnalyticsImpl = analytics_component::AnalyticsImpl; + // impl AnalyticsInternalImpl = analytics_component::InternalImpl; + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; const PROTOCOL_OWNER_ROLE: felt252 = selector!("PROTOCOL_OWNER"); @@ -260,6 +308,10 @@ pub mod StarkRemit { SavingsGroupEvent: savings_group_component::Event, TokenManagementEvent: token_management_component::Event, TransferEvent: transfer_component::Event, + EmergencyEvent: emergency_component::Event, + PenaltyEvent: penalty_component::Event, + AutoScheduleEvent: auto_schedule_component::Event, + PaymentFlexibilityEvent: payment_flexibility_component::Event, // System Management Events AgentAuthorized: AgentAuthorized, AgentPermissionUpdated: AgentPermissionUpdated, @@ -297,7 +349,6 @@ pub mod StarkRemit { AgentStatusUpdated: AgentStatusUpdated, // Event for agent status updates TransferHistoryRecorded: TransferHistoryRecorded, // Event for history recording // contribution - ContributionMade: ContributionMade, RoundDisbursed: RoundDisbursed, RoundCompleted: RoundCompleted, ContributionMissed: ContributionMissed, @@ -324,9 +375,116 @@ pub mod StarkRemit { UpdateExecuted: UpdateExecuted, UpdateCancelled: UpdateCancelled, // loan_id -> timestamp_of_last_payment + EmergencyWithdrawalAll: EmergencyWithdrawalAll, + EmergencyWithdrawalMember: EmergencyWithdrawalMember, + RoundEmergencyCompleted: RoundEmergencyCompleted, + RoundEmergencyCancelled: RoundEmergencyCancelled, + RecipientChanged: RecipientChanged, + TokensRecovered: TokensRecovered, + FundsMigrated: FundsMigrated, + MemberBanned: MemberBanned, + MemberUnbanned: MemberUnbanned, + PenaltyPoolDistributed: PenaltyPoolDistributed, + // LateFeeApplied: LateFeeApplied, + // StrikeAdded: StrikeAdded, + // StrikeRemoved: StrikeRemoved, + // AutoPaymentSetup: AutoPaymentSetup, + // EarlyPaymentProcessed: EarlyPaymentProcessed, + // GracePeriodExtended: GracePeriodExtended, + // TokenValueConverted: TokenValueConverted, + // MemberProfileUpdated: MemberProfileUpdated, + // RollingScheduleMaintained: RollingScheduleMaintained, } + // #[derive(Drop, starknet::Event)] + // pub struct LateFeeApplied { + // pub member: ContractAddress, + // pub round_id: u256, + // pub fee_amount: u256, + // pub contribution_amount: u256, + // pub timestamp: u64, + // } + + + + // #[derive(Drop, starknet::Event)] + // pub struct StrikeAdded { + // pub member: ContractAddress, + // pub round_id: u256, + // pub current_strikes: u32, + // pub timestamp: u64, + // } + + // #[derive(Drop, starknet::Event)] + // pub struct StrikeRemoved { + // pub member: ContractAddress, + // pub removed_by: ContractAddress, + // pub new_strikes: u32, + // pub timestamp: u64, + // } + + + + // #[derive(Drop, starknet::Event)] + // pub struct AutoPaymentSetup { + // pub member: ContractAddress, + // pub token: ContractAddress, + // pub amount: u256, + // pub frequency: PaymentFrequency, + // pub next_payment_date: u64, + // pub timestamp: u64, + // } + + // #[derive(Drop, starknet::Event)] + // pub struct EarlyPaymentProcessed { + // pub member: ContractAddress, + // pub round_id: u256, + // pub original_amount: u256, + // pub discount_amount: u256, + // pub final_amount: u256, + // pub timestamp: u64, + // } + + // #[derive(Drop, starknet::Event)] + // pub struct GracePeriodExtended { + // pub member: ContractAddress, + // pub extension_hours: u64, + // pub total_extension: u64, + // pub extended_by: ContractAddress, + // pub timestamp: u64, + // } + + // #[derive(Drop, starknet::Event)] + // pub struct TokenValueConverted { + // pub member: ContractAddress, + // pub from_token: ContractAddress, + // pub to_token: ContractAddress, + // pub original_amount: u256, + // pub converted_amount: u256, + // pub from_price: u256, + // pub to_price: u256, + // pub timestamp: u64, + // } + + // #[derive(Drop, starknet::Event)] + // pub struct MemberProfileUpdated { + // pub member: ContractAddress, + // pub field: felt252, + // pub old_value: felt252, + // pub new_value: felt252, + // pub updated_by: ContractAddress, + // pub timestamp: u64, + // } + + + + // #[derive(Drop, starknet::Event)] + // pub struct RollingScheduleMaintained { + // pub rounds_created: u32, + // pub last_maintenance_timestamp: u64, + // } + // Contract storage definition #[storage] #[allow(starknet::colliding_storage_paths)] @@ -355,6 +513,40 @@ pub mod StarkRemit { token_management_component: token_management_component::Storage, #[substorage(v0)] transfer_component: transfer_component::Storage, + #[substorage(v0)] + emergency: emergency_component::Storage, + #[substorage(v0)] + penalty: penalty_component::Storage, + #[substorage(v0)] + auto_schedule: auto_schedule_component::Storage, + #[substorage(v0)] + payment_flexibility: payment_flexibility_component::Storage, + // Emergency and Penalty System Storage + emergency_approvals: Map>, + // penalty_config: PenaltyConfig, + // emergency_operations: Map, + // Penalty System Storage + // member_penalties: Map, + // penalty_pool: u256, + // penalty history stored as (member, index) -> event and per-member count + // penalty_history: Map<(ContractAddress, u32), PenaltyEvent>, + // penalty_history_count: Map, + // Auto-Schedule System Storage + // auto_schedule_config: AutoScheduleConfig, + // scheduled_rounds: Map, + // round_schedule_index: u256, + // last_schedule_maintenance: u64, + // schedule_maintenance_interval: u64, + // Member Profile Storage + // member_profiles: Map, + // member_profile_count: u32, + // Payment Flexibility Storage + // payment_config: PaymentConfig, + // auto_payment_setups: Map, + // Analytics Storage + // contribution_analytics: ContributionAnalytics, + // member_analytics: Map, + // last_analytics_update: u64, // System Management Storage agent_permissions: Map<(ContractAddress, felt252), bool>, // (agent, permission) -> granted paused_functions: Map, // function selector -> paused @@ -492,11 +684,148 @@ pub mod StarkRemit { // Initialize governance self.admin_roles.write(owner, GovRole::SuperAdmin); self.timelock_duration.write(86400); // 24 hours default timelock + // Initialize emergency component + self.emergency.initializer(owner); + // Initialize penalty component + self.penalty.initializer(owner); + // Initialize auto-schedule component + self.auto_schedule.initializer(owner); + // Initialize payment flexibility component + self.payment_flexibility.initializer(owner); } // Implementation of the StarkRemit interface with KYC functions #[abi(embed_v0)] impl IStarkRemitImpl of IStarkRemit::IStarkRemit { + // --- Penalty Functions --- + fn apply_late_fee(ref self: ContractState, member: ContractAddress, round_id: u256) { + self.ownable.assert_only_owner(); + self.penalty.apply_late_fee(member, round_id); + } + + fn add_strike(ref self: ContractState, member: ContractAddress, round_id: u256) { + self.ownable.assert_only_owner(); + + // Get current penalty record to check if member will be automatically banned + let current_record = self.penalty.get_member_penalty_record(member); + let penalty_config = self.penalty.get_penalty_config(); + let will_be_banned = current_record.strikes + 1 >= penalty_config.max_strikes; + + // Add strike in penalty component + self.penalty.add_strike(member, round_id); + + // If member was automatically banned, remove them from main contract's member list + if will_be_banned { + self._remove_member_from_list(member); + + // Emit main contract event for automatic ban + self.emit(Event::MemberBanned(MemberBanned { + member, + reason: 'max_strikes_reached', + strikes: current_record.strikes + 1, + banned_by: get_caller_address(), + timestamp: get_block_timestamp(), + })); + } + } + + fn remove_strike(ref self: ContractState, member: ContractAddress) { + self.ownable.assert_only_owner(); + + // Get current penalty record to check if member will be automatically unbanned + let current_record = self.penalty.get_member_penalty_record(member); + let penalty_config = self.penalty.get_penalty_config(); + let will_be_unbanned = current_record.is_banned && current_record.strikes - 1 < penalty_config.max_strikes; + + // Remove strike in penalty component + self.penalty.remove_strike(member); + + // If member was automatically unbanned, re-add them to main contract's member list + if will_be_unbanned { + self._add_member_to_list(member); + + // Emit main contract event for automatic unban + self.emit(Event::MemberUnbanned(MemberUnbanned { + member, + unbanned_by: get_caller_address(), + timestamp: get_block_timestamp(), + })); + } + } + + fn ban_member(ref self: ContractState, member: ContractAddress) { + self.ownable.assert_only_owner(); + + // First, update penalty state in component + self.penalty.ban_member(member); + + // Then, remove member from main contract's member list + self._remove_member_from_list(member); + + // Emit main contract event + self.emit(Event::MemberBanned(MemberBanned { + member, + reason: 'admin_ban', + strikes: self.penalty.get_member_penalty_record(member).strikes, + banned_by: get_caller_address(), + timestamp: get_block_timestamp(), + })); + } + + fn unban_member(ref self: ContractState, member: ContractAddress) { + self.ownable.assert_only_owner(); + + // First, update penalty state in component + self.penalty.unban_member(member); + + // Then, re-add member to main contract's member list + self._add_member_to_list(member); + + // Emit main contract event + self.emit(Event::MemberUnbanned(MemberUnbanned { + member, + unbanned_by: get_caller_address(), + timestamp: get_block_timestamp(), + })); + } + + fn distribute_penalty_pool(ref self: ContractState) { + self.ownable.assert_only_owner(); + + // Get distribution data from penalty component + let distribution_data = self.penalty.calculate_distribution_data(); + + if distribution_data.total_amount == 0 { + return; // No penalty pool to distribute + } + + // Execute transfers using main contract's transfer function + let mut distributed_count = 0; + let mut i = 0; + + while i < distribution_data.member_shares.len() { + let member_share = *distribution_data.member_shares.at(i); + if member_share.share > 0 { + // Transfer tokens to member + self.transfer_tokens_to_member(member_share.member, member_share.share); + distributed_count += 1; + } + i += 1; + } + + // Reset penalty pool in component after successful distribution + self.penalty.reset_penalty_pool(); + + // Emit main contract event + self.emit(Event::PenaltyPoolDistributed(PenaltyPoolDistributed { + total_amount: distribution_data.total_amount, + recipient_count: distributed_count, + distribution_type: 'proportional', + timestamp: get_block_timestamp(), + })); + } + + fn grant_admin_role(ref self: ContractState, admin: ContractAddress) { self.accesscontrol.assert_only_role(ADMIN_ROLE); self.accesscontrol._grant_role(ADMIN_ROLE, admin); @@ -2187,6 +2516,7 @@ pub mod StarkRemit { self.emit(ContractRegistered { name, addr: new_address }); true } + /// Schedule a parameter update (SuperAdmin only, timelock) fn schedule_parameter_update(ref self: ContractState, key: felt252, value: u256) -> bool { let caller = get_caller_address(); @@ -2329,8 +2659,174 @@ pub mod StarkRemit { fn get_timelock_duration(self: @ContractState) -> u64 { self.timelock_duration.read() } + + /// Emergency pause the entire contract with metadata + fn emergency_pause_contract(ref self: ContractState, reason: felt252) { + // Only pauser can pause + let caller = get_caller_address(); + assert(self.agent_permissions.read((caller, 'PAUSER')), 'Not authorized pauser'); + + self.emergency._pause_with_metadata(reason); + + self.emit(EmergencyPauseActivated { + function_selector: 0, // Global pause, not function-specific + caller, + expires_at: 0 // No expiry for global pause + }); + } + + /// Emergency unpause the entire contract + fn emergency_unpause_contract(ref self: ContractState) { + let caller = get_caller_address(); + assert(self.agent_permissions.read((caller, 'PAUSER')), 'Not authorized pauser'); + + self.emergency._unpause_with_metadata_clear(); + + self.emit(EmergencyPauseDeactivated { + function_selector: 0, // Global pause, not function-specific + caller + }); + } + + /// Emergency pause with metadata + fn emergency_pause_with_metadata(ref self: ContractState, reason: felt252) { + let caller = get_caller_address(); + assert(self.agent_permissions.read((caller, 'PAUSER')), 'Not authorized pauser'); + + self.emergency._pause_with_metadata(reason); + + self.emit(EmergencyPauseActivated { + function_selector: 0, // Global pause, not function-specific + caller, + expires_at: 0 // No expiry for global pause + }); + } + + /// Emergency unpause with metadata clear + fn emergency_unpause_with_metadata_clear(ref self: ContractState) { + let caller = get_caller_address(); + assert(self.agent_permissions.read((caller, 'PAUSER')), 'Not authorized pauser'); + + self.emergency._unpause_with_metadata_clear(); + + self.emit(EmergencyPauseDeactivated { + function_selector: 0, // Global pause, not function-specific + caller + }); + } + + /// Emergency set pause metadata + fn emergency_set_pause_meta(ref self: ContractState, reason: felt252) { + let caller = get_caller_address(); + assert(self.agent_permissions.read((caller, 'PAUSER')), 'Not authorized pauser'); + + self.emergency._set_pause_meta(reason); + } + + /// Emergency set ban status + fn emergency_set_ban(ref self: ContractState, member: ContractAddress, banned: bool) { + let caller = get_caller_address(); + assert(self.agent_permissions.read((caller, 'PAUSER')), 'Not authorized pauser'); + + self.emergency._set_ban(member, banned); + + if banned { + self.emit(Event::MemberBanned(MemberBanned { + member, + reason: 'emergency_ban', + strikes: 0, + banned_by: caller, + timestamp: get_block_timestamp(), + })); + } else { + self.emit(Event::MemberUnbanned(MemberUnbanned { + member, + unbanned_by: caller, + timestamp: get_block_timestamp(), + })); + } + } + + // --- Auto-Schedule Functions --- + fn setup_auto_schedule(ref self: ContractState, config: AutoScheduleConfig) { + self.ownable.assert_only_owner(); + self.auto_schedule._setup_auto_schedule(config); + } + + fn maintain_rolling_schedule(ref self: ContractState) { + self.ownable.assert_only_owner(); + self.auto_schedule._maintain_rolling_schedule(); + } + + fn auto_activate_round(ref self: ContractState, round_id: u256) { + self.ownable.assert_only_owner(); + self.auto_schedule._auto_activate_round(round_id); + } + + fn auto_complete_expired_rounds(ref self: ContractState) { + self.ownable.assert_only_owner(); + self.auto_schedule._auto_complete_expired_rounds(); + } + + fn modify_schedule(ref self: ContractState, round_id: u256, new_deadline: u64) { + self.ownable.assert_only_owner(); + self.auto_schedule._modify_schedule(round_id, new_deadline); + } + + // --- Payment Flexibility Functions --- + fn setup_auto_payment( + ref self: ContractState, + member: ContractAddress, + token: ContractAddress, + amount: u256, + frequency: PaymentFrequency, + ) { + self.ownable.assert_only_owner(); + self.payment_flexibility._setup_auto_payment(member, token, amount, frequency); + } + + fn process_early_payment( + ref self: ContractState, + member: ContractAddress, + round_id: u256, + amount: u256, + ) -> (u256, u256) { + self.ownable.assert_only_owner(); + self.payment_flexibility._process_early_payment(member, round_id, amount) + } + + fn extend_grace_period( + ref self: ContractState, + member: ContractAddress, + extension_hours: u64, + ) { + self.ownable.assert_only_owner(); + self.payment_flexibility._extend_grace_period(member, extension_hours); + } + + fn add_supported_token(ref self: ContractState, token: ContractAddress) { + self.ownable.assert_only_owner(); + self.payment_flexibility._add_supported_token(token); + } + + fn remove_supported_token(ref self: ContractState, token: ContractAddress) { + self.ownable.assert_only_owner(); + self.payment_flexibility._remove_supported_token(token); + } + + fn update_payment_config(ref self: ContractState, config: PaymentConfig) { + self.ownable.assert_only_owner(); + self.payment_flexibility._update_payment_config(config); + } + + fn process_auto_payments(ref self: ContractState) { + self.ownable.assert_only_owner(); + self.payment_flexibility._process_auto_payments(); + } } + + // --- System Management Functions --- #[generate_trait] impl SystemManagement of SystemManagementTrait { @@ -2511,133 +3007,1474 @@ pub mod StarkRemit { } } - // Internal helper functions + // --- Emergency Functions --- #[generate_trait] - impl InternalFunctions of InternalFunctionsTrait { - fn _validate_kyc_and_limits(self: @ContractState, user: ContractAddress, amount: u256) { - // Check KYC validity - assert(IStarkRemitImpl::is_kyc_valid(self, user), KYCErrors::INVALID_KYC_STATUS); + impl Emergency of EmergencyTrait { + + fn emergency_withdraw_all(ref self: ContractState) { + self.ownable.assert_only_owner(); + self.emergency.assert_paused(); // Only callable when paused + + // Business Logic: Calculate proportional withdrawal based on member contributions + let total_contract_balance = self.get_contract_token_balance(); + assert(total_contract_balance > 0, EmergencyErrors::NO_FUNDS_TO_WITHDRAW); + + // Get all active members and their contribution ratios + let mut total_contributions = 0; + let mut member_shares = ArrayTrait::new(); + + // Calculate total contributions across all rounds + let mut round_id = 1; + while round_id <= self.round_ids.read() { + let round = self.rounds.read(round_id); + if round.status == RoundStatus::Active { + total_contributions += round.total_contributions; + } + round_id += 1; + } - // Check transaction limits - let kyc_data = self.user_kyc_data.read(user); - let level_u8 = self._kyc_level_to_u8(kyc_data.level); + // Calculate each member's proportional share + let mut member_index = 0; + while member_index < self.member_count.read() { + let member_address = self.member_by_index.read(member_index); + if self.members.read(member_address) { + let member_total_contribution = self + .calculate_member_total_contribution(member_address); + if member_total_contribution > 0 { + let proportional_share = (member_total_contribution + * total_contract_balance) + / total_contributions; + member_shares.append((member_address, proportional_share)); + } + } + member_index += 1; + } - // Check single transaction limit - let single_limit = self.single_limits.read(level_u8); - assert(amount <= single_limit, KYCErrors::SINGLE_TX_LIMIT_EXCEEDED); + // Execute proportional withdrawals + let mut shares_index = 0; + while shares_index < member_shares.len() { + let (member, share) = member_shares[shares_index]; + if *share > 0 { + self.transfer_tokens_to_member(*member, *share); + } + shares_index += 1; + } - // Check daily limit - let daily_limit = self.daily_limits.read(level_u8); - let current_usage = self._get_daily_usage(user); - assert(current_usage + amount <= daily_limit, KYCErrors::DAILY_LIMIT_EXCEEDED); + // Emit event for audit trail + self + .emit( + Event::EmergencyWithdrawalAll( + EmergencyWithdrawalAll { + total_amount: total_contract_balance, + member_count: member_shares.len().try_into().unwrap(), + executed_by: get_caller_address(), + timestamp: get_block_timestamp(), + }, + ), + ); } - fn _get_daily_usage(self: @ContractState, user: ContractAddress) -> u256 { - let current_time = get_block_timestamp(); - let last_reset = self.last_reset.read(user); + fn emergency_withdraw_member(ref self: ContractState, member: ContractAddress) { + self.ownable.assert_only_owner(); + self.emergency.assert_paused(); - // Reset if it's a new day (86400 seconds = 24 hours) - if current_time > last_reset + 86400 { - return 0; - } + // Business Logic: Withdraw member's specific contributions + assert(self.members.read(member), EmergencyErrors::MEMBER_NOT_EXISTS); - self.daily_usage.read(user) + let member_contribution = self.calculate_member_total_contribution(member); + assert(member_contribution > 0, EmergencyErrors::MEMBER_NO_CONTRIBUTIONS); + + // Transfer member's contributions back + self.transfer_tokens_to_member(member, member_contribution); + + // Update member status - remove from active members + self.members.write(member, false); + self.member_count.write(self.member_count.read() - 1); + + self + .emit( + Event::EmergencyWithdrawalMember( + EmergencyWithdrawalMember { + member, + amount: member_contribution, + executed_by: get_caller_address(), + timestamp: get_block_timestamp(), + }, + ), + ); } - fn _record_daily_usage(ref self: ContractState, user: ContractAddress, amount: u256) { - let current_time = get_block_timestamp(); - let last_reset = self.last_reset.read(user); + fn emergency_complete_round(ref self: ContractState, round_id: u256) { + self.ownable.assert_only_owner(); + self.emergency.assert_paused(); + + // Business Logic: Force complete a stuck round + let mut round = self.rounds.read(round_id); + assert(round.status == RoundStatus::Active, EmergencyErrors::ROUND_NOT_ACTIVE); + + // Check if round has contributions + if round.total_contributions > 0 { + // Transfer funds to recipient + self.transfer_tokens_to_member(round.recipient, round.total_contributions); + + // Update round status + round.status = RoundStatus::Completed; + self.rounds.write(round_id, round); - if current_time > last_reset + 86400 { - // Reset for new day - self.daily_usage.write(user, amount); - self.last_reset.write(user, current_time); + // Update analytics + self.update_round_analytics(round_id, RoundStatus::Completed); + + self + .emit( + Event::RoundEmergencyCompleted( + RoundEmergencyCompleted { + round_id, + recipient: round.recipient, + amount: round.total_contributions, + completed_by: get_caller_address(), + timestamp: get_block_timestamp(), + }, + ), + ); } else { - // Add to current day usage - let current_usage = self.daily_usage.read(user); - self.daily_usage.write(user, current_usage + amount); + // No contributions - just cancel the round + round.status = RoundStatus::Cancelled; + self.rounds.write(round_id, round); + + self + .emit( + Event::RoundEmergencyCancelled( + RoundEmergencyCancelled { + round_id, + cancelled_by: get_caller_address(), + reason: 'no_contributions', + timestamp: get_block_timestamp(), + }, + ), + ); } } - fn _kyc_level_to_u8(self: @ContractState, level: KycLevel) -> u8 { - match level { - KycLevel::None => 0, - KycLevel::Basic => 1, - KycLevel::Enhanced => 2, - KycLevel::Premium => 3, - } + fn emergency_change_recipient( + ref self: ContractState, round_id: u256, new_recipient: ContractAddress, + ) { + self.ownable.assert_only_owner(); + self.emergency.assert_paused(); + + // Business Logic: Change recipient due to member issues + assert(!new_recipient.is_zero(), EmergencyErrors::INVALID_RECIPIENT); + assert(self.members.read(new_recipient), EmergencyErrors::RECIPIENT_NOT_MEMBER); + + let mut round = self.rounds.read(round_id); + assert(round.status == RoundStatus::Active, EmergencyErrors::ROUND_NOT_ACTIVE); + assert(round.recipient != new_recipient, EmergencyErrors::RECIPIENT_ALREADY_SET); + + let old_recipient = round.recipient; + round.recipient = new_recipient; + self.rounds.write(round_id, round); + + self + .emit( + Event::RecipientChanged( + RecipientChanged { + round_id, + old_recipient, + new_recipient, + changed_by: get_caller_address(), + timestamp: get_block_timestamp(), + }, + ), + ); } - fn _set_default_transaction_limits(ref self: ContractState) { - // None level - very restricted - self.daily_limits.write(0, 100_000_000_000_000_000); // 0.1 tokens - self.single_limits.write(0, 50_000_000_000_000_000); // 0.05 tokens + fn emergency_cancel_round(ref self: ContractState, round_id: u256) { + self.ownable.assert_only_owner(); + self.emergency.assert_paused(); - // Basic level - moderate limits - self.daily_limits.write(1, 1000_000_000_000_000_000_000); // 1,000 tokens - self.single_limits.write(1, 500_000_000_000_000_000_000); // 500 tokens + // Business Logic: Cancel round and refund all contributions + let round = self.rounds.read(round_id); + assert(round.status == RoundStatus::Active, EmergencyErrors::ROUND_NOT_ACTIVE); + + if round.total_contributions > 0 { + // Refund contributions proportionally to contributors + self.refund_round_contributions(round_id); + } - // Enhanced level - higher limits - self.daily_limits.write(2, 10000_000_000_000_000_000_000); // 10,000 tokens - self.single_limits.write(2, 5000_000_000_000_000_000_000); // 5,000 tokens + // Update round status + let mut updated_round = round; + updated_round.status = RoundStatus::Cancelled; + self.rounds.write(round_id, updated_round); - // Premium level - maximum limits - self.daily_limits.write(3, 100000_000_000_000_000_000_000); // 100,000 tokens - self.single_limits.write(3, 50000_000_000_000_000_000_000); // 50,000 tokens + self + .emit( + Event::RoundEmergencyCancelled( + RoundEmergencyCancelled { + round_id, + cancelled_by: get_caller_address(), + reason: 'emergency_cancellation', + timestamp: get_block_timestamp(), + }, + ), + ); } - fn _record_transfer_history( - ref self: ContractState, - transfer_id: u256, - action: felt252, - actor: ContractAddress, - previous_status: TransferStatus, - new_status: TransferStatus, - details: felt252, - ) { - let current_time = get_block_timestamp(); + fn emergency_recover_tokens(ref self: ContractState, token: ContractAddress, amount: u256) { + self.ownable.assert_only_owner(); + self.emergency.assert_paused(); - // Create history entry - let history = TransferHistory { - transfer_id, - action, - actor, - timestamp: current_time, - previous_status, - new_status, - details, - }; + // Business Logic: Recover accidentally sent tokens + assert(!token.is_zero(), EmergencyErrors::INVALID_TOKEN_ADDRESS); + assert(amount > 0, EmergencyErrors::INVALID_AMOUNT); - // Store in transfer history - let history_count = self.transfer_history_count.read(transfer_id); - self.transfer_history.write((transfer_id, history_count), history); - self.transfer_history_count.write(transfer_id, history_count + 1); + // Calculate unallocated balance: total_contract_balance - total_allocated_tokens + let total_contract_balance = self.get_contract_token_balance_specific(token); + let total_allocated_tokens = self._calculate_total_allocated_tokens(token); + let unallocated_balance = total_contract_balance - total_allocated_tokens; + + // Ensure we only recover unallocated tokens + assert(unallocated_balance >= amount, EmergencyErrors::INSUFFICIENT_BALANCE); - // Store in actor history - let actor_count = self.actor_history_count.read(actor); - self.actor_history.write((actor, actor_count), (transfer_id, history_count)); - self.actor_history_count.write(actor, actor_count + 1); + // Transfer tokens to owner + let owner = self.ownable.owner(); + self.transfer_specific_tokens_to_address(token, owner, amount); - // Store in action history - let action_count = self.action_history_count.read(action); - self.action_history.write((action, action_count), (transfer_id, history_count)); - self.action_history_count.write(action, action_count + 1); + self + .emit( + Event::TokensRecovered( + TokensRecovered { + token, + amount, + recovered_by: get_caller_address(), + recipient: owner, + timestamp: get_block_timestamp(), + }, + ), + ); + } + + fn emergency_migrate_funds(ref self: ContractState, new_contract: ContractAddress) { + self.ownable.assert_only_owner(); + self.emergency.assert_paused(); + + // Business Logic: Migrate all funds to new contract + assert(!new_contract.is_zero(), EmergencyErrors::INVALID_CONTRACT_ADDRESS); + assert(new_contract != starknet::get_contract_address(), EmergencyErrors::CANNOT_MIGRATE_TO_SELF); + + // Verify the target contract is registered in the contract registry + // or implements a specific interface + let registered_migration_target = self.contract_registry.read('migration_target'); + assert( + new_contract == registered_migration_target || registered_migration_target.is_zero(), + EmergencyErrors::INVALID_MIGRATION_TARGET + ); + + let total_balance = self.get_contract_token_balance(); + assert(total_balance > 0, EmergencyErrors::NO_FUNDS_TO_MIGRATE); + + // Transfer all funds to new contract + self.transfer_tokens_to_address(new_contract, total_balance); - // Emit event self .emit( - TransferHistoryRecorded { transfer_id, action, actor, timestamp: current_time }, + Event::FundsMigrated( + FundsMigrated { + new_contract, + amount: total_balance, + migrated_by: get_caller_address(), + timestamp: get_block_timestamp(), + }, + ), ); } - // Generates and stores a new unique group ID for a savings group - // Returns the newly generated group ID - fn _new_group_id(ref self: ContractState) -> u64 { - let group_id = self.group_count.read(); + } - self.group_count.write(group_id + 1); + // Implementation of IMainContractData trait for penalty component + impl MainContractDataImpl of PenaltyMainContractData { + fn get_member_contribution_data(self: @ContractState, round_id: u256, member: ContractAddress) -> MemberContribution { + let contribution = self.member_contributions.read((round_id, member)); + MemberContribution { + member, + amount: contribution.amount, + contributed_at: contribution.contributed_at, + } + } + + fn get_round_data(self: @ContractState, round_id: u256) -> RoundData { + let round = self.rounds.read(round_id); + RoundData { + deadline: round.deadline, + status: round.status, + total_contributions: round.total_contributions, + } + } + + fn get_member_status(self: @ContractState, member: ContractAddress) -> bool { + self.members.read(member) + } + + fn get_member_count(self: @ContractState) -> u32 { + self.member_count.read() + } + + fn get_round_ids(self: @ContractState) -> u256 { + self.round_ids.read() + } + + fn get_member_by_index(self: @ContractState, index: u32) -> ContractAddress { + self.member_by_index.read(index) + } + } - group_id + // Implementation of IMainContractData trait for auto-schedule component + impl AutoScheduleMainContractDataImpl of AutoScheduleMainContractData { + fn get_member_count(self: @ContractState) -> u32 { + self.member_count.read() + } + + fn get_member_by_index(self: @ContractState, index: u32) -> ContractAddress { + self.member_by_index.read(index) + } + + fn get_current_round_id(self: @ContractState) -> u256 { + self.round_ids.read() + } + + fn create_round(ref self: ContractState, recipient: ContractAddress, deadline: u64) -> u256 { + // Create a new round using existing round creation logic + let round_id = self.round_ids.read() + 1; + let round = ContributionRound { + round_id, + recipient, + deadline, + status: RoundStatus::Scheduled, + total_contributions: 0, + }; + self.rounds.write(round_id, round); + self.round_ids.write(round_id); + round_id } } + + + // Implementation of Member Profile Component Interface + // #[abi(embed_v0)] + // impl MemberProfileImpl of IMemberProfile { + // fn create_member_profile(ref self: ContractState, member: ContractAddress) { + // self.ownable.assert_only_owner(); + + // let profile = MemberProfile { + // join_date: get_block_timestamp(), + // total_contributions: 0, + // missed_contributions: 0, + // credit_score: 100, + // last_recipient_round: 0, + // reliability_rating: 100, + // preferred_payment_method: 'default', + // communication_preferences: 'email', + // }; + + // self.member_profiles.write(member, profile); + // self.member_profile_count.write(self.member_profile_count.read() + 1); + // } + + // fn update_reliability_rating( + // ref self: ContractState, member: ContractAddress, new_rating: u8, + // ) { + // self.ownable.assert_only_owner(); + + // assert(new_rating <= 100, 'Invalid rating: must be 0-100'); + + // let mut profile = self.member_profiles.read(member); + // profile.reliability_rating = new_rating; + // self.member_profiles.write(member, profile); + // } + + // fn get_member_profile(self: @ContractState, member: ContractAddress) -> MemberProfile { + // self.member_profiles.read(member) + // } + // } + + // // Implementation of Payment Flexibility Component Interface + // #[abi(embed_v0)] + // impl PaymentFlexibilityImpl of IPaymentFlexibility { + // fn setup_auto_payment( + // ref self: ContractState, + // token: ContractAddress, + // amount: u256, + // frequency: PaymentFrequency, + // ) { + // self.ownable.assert_only_owner(); + + // // Business Logic: Setup automatic recurring payments for a member + // let caller = get_caller_address(); + // let payment_config = self.payment_config.read(); + + // // Validate token is supported + // assert(self.is_token_supported(token), 'Token not supported'); + // assert(amount > 0, 'Invalid payment amount'); + + // // Check if member already has auto-payment setup + // let existing_setup = self.auto_payment_setups.read(caller); + // assert(!existing_setup.is_active, 'Auto-payment already active'); + + // // Calculate next payment date based on frequency + // let next_payment_date = self.calculate_next_payment_date(frequency); + + // // Create auto-payment setup + // let auto_payment = AutoPaymentSetup { + // member: caller, token, amount, frequency, next_payment_date, is_active: true, + // }; + + // self.auto_payment_setups.write(caller, auto_payment); + + // self + // .emit( + // Event::AutoPaymentSetup( + // AutoPaymentSetup { + // member: caller, + // token, + // amount, + // frequency, + // next_payment_date, + // timestamp: get_block_timestamp(), + // }, + // ), + // ); + // } + + // fn process_early_payment(ref self: ContractState, round_id: u256, amount: u256) { + // self.ownable.assert_only_owner(); + + // // Business Logic: Process early payment with discount + // let caller = get_caller_address(); + // let payment_config = self.payment_config.read(); + // let round = self.rounds.read(round_id); + + // // Validate round is active + // assert(round.status == RoundStatus::Active, 'Round not active'); + // assert(get_block_timestamp() < round.deadline, 'Round deadline passed'); + + // // Calculate early payment discount + // let discount_amount = (amount * payment_config.early_payment_discount_basis_points) + // / 10000; + // let final_amount = amount - discount_amount; + + // // Process the early payment + // self.process_contribution(round_id, caller, final_amount); + + // // Update member profile for early payment bonus + // let mut profile = self.member_profiles.read(caller); + // profile + // .reliability_rating = self + // .calculate_reliability_bonus(profile.reliability_rating, true); + // self.member_profiles.write(caller, profile); + + // self + // .emit( + // Event::EarlyPaymentProcessed( + // EarlyPaymentProcessed { + // member: caller, + // round_id, + // original_amount: amount, + // discount_amount, + // final_amount, + // timestamp: get_block_timestamp(), + // }, + // ), + // ); + // } + + // fn extend_grace_period( + // ref self: ContractState, member: ContractAddress, extension_hours: u64, + // ) { + // self.ownable.assert_only_owner(); + + // // Business Logic: Extend grace period for specific member + // assert(self.members.read(member), 'Member does not exist'); + // assert(extension_hours > 0, 'Invalid extension hours'); + // assert(extension_hours <= 168, 'Extension cannot exceed 1 week'); // Max 7 days + + // // Get current grace period extension + // let current_extension = self.grace_period_extensions.read(member); + // let new_extension = current_extension + extension_hours; + + // // Update grace period extension + // self.grace_period_extensions.write(member, new_extension); + + // self + // .emit( + // Event::GracePeriodExtended( + // GracePeriodExtended { + // member, + // extension_hours, + // total_extension: new_extension, + // extended_by: get_caller_address(), + // timestamp: get_block_timestamp(), + // }, + // ), + // ); + // } + + // fn convert_token_value( + // ref self: ContractState, + // from_token: ContractAddress, + // to_token: ContractAddress, + // amount: u256, + // ) -> u256 { + // // Business Logic: Convert token value using oracle + // let payment_config = self.payment_config.read(); + // let oracle_address = payment_config.usd_oracle_address; + + // assert(!oracle_address.is_zero(), 'Oracle address not set'); + // assert(from_token != to_token, 'Same token conversion not allowed'); + + // // Get token prices from oracle (simplified - would integrate with real oracle) + // let from_price = self.get_token_price_from_oracle(from_token); + // let to_price = self.get_token_price_from_oracle(to_token); + + // assert(from_price > 0 && to_price > 0, 'Invalid token prices'); + + // // Convert amount: (amount * from_price) / to_price + // let converted_amount = (amount * from_price) / to_price; + + // self + // .emit( + // Event::TokenValueConverted( + // TokenValueConverted { + // from_token, + // to_token, + // original_amount: amount, + // converted_amount, + // from_price, + // to_price, + // timestamp: get_block_timestamp(), + // }, + // ), + // ); + + // converted_amount + // } + + // fn get_payment_status( + // ref self: ContractState, member: ContractAddress, round_id: u256, + // ) -> PaymentStatus { + // // Business Logic: Determine payment status based on contribution timing + // let round = self.rounds.read(round_id); + // let member_contribution = self.member_contributions.read((round_id, member)); + // let current_time = get_block_timestamp(); + // let penalty_config = self.penalty_config.read(); + + // if member_contribution.amount == 0 { + // // No payment made + // if current_time > round.deadline + (penalty_config.grace_period_hours * 3600) { + // return PaymentStatus::Missed; + // } else if current_time > round.deadline { + // return PaymentStatus::Late; + // } else { + // return PaymentStatus::Pending; + // } + // } else { + // // Payment made + // if current_time <= round.deadline { + // return PaymentStatus::Paid; + // } else if current_time <= round.deadline + // + (penalty_config.grace_period_hours * 3600) { + // return PaymentStatus::Late; + // } else { + // return PaymentStatus::Overpaid; // Payment made after grace period + // } + // } + // } + // } + + // Implementation of Analytics Component Interface + // #[abi(embed_v0)] + // impl AnalyticsImpl of IAnalytics { + // fn generate_contribution_report(self: @ContractState) -> ContributionAnalytics { + // let mut analytics = self.contribution_analytics.read(); // Reads from cached analytics + + // // Calculate real-time statistics from actual data if cache is stale or not used + // // For a real-time report, you might recompute everything here, or update the cache + // // by calling an internal function. For this example, we assume + // // `cached_contribution_analytics` + // // is updated by other functions. + + // // Example of re-calculating if not using a cache: + // let mut total_rounds_count = 0; + // let mut successful_rounds_count = 0; + // let mut failed_rounds_count = 0; + + // let mut round_id = 1; + // let max_round_id = self.round_ids.read(); // Assuming this tracks total rounds created + // while round_id <= max_round_id { // Iterates through all scheduled rounds + // let round = self.rounds.read(round_id); // Reads round data + // total_rounds_count += 1; + + // match round.status { // Checks the status of the round + // RoundStatus::Completed => successful_rounds_count += 1, + // RoundStatus::Cancelled => failed_rounds_count += 1, + // _ => {} // Ignore Scheduled or Active rounds for "completed" or "failed" counts + // } + // round_id += 1; + // } + + // // Update analytics with real data + // analytics.total_rounds = total_rounds_count; + // analytics.successful_rounds = successful_rounds_count; + // analytics.failed_rounds = failed_rounds_count; + // analytics.total_penalties_collected = self.penalty_pool.read(); // Reads total penalties + // // member_reliability_distribution would need more complex aggregation logic + + // analytics + // } + + // fn get_member_performance( + // self: @ContractState, member: ContractAddress, + // ) -> MemberAnalytics { + // let mut analytics = MemberAnalytics { // Initializes a new MemberAnalytics struct + // total_contributions: 0, + // on_time_payments: 0, + // late_payments: 0, + // missed_payments: 0, + // reliability_score: 0, + // last_updated: 0, + // }; + + // // Calculate from actual contribution data + // let mut round_id = 1; + // let max_round_id = self.round_ids.read(); // Assuming this tracks total rounds created + // while round_id <= max_round_id { // Iterates through all scheduled rounds + // let round = self.rounds.read(round_id); // Reads round data + // // Assuming a mapping for member contributions per round: Map<(u256, + // // ContractAddress), ContributionDetail> + // let contribution = self.member_contributions.read((round_id, member)); + + // if contribution.amount > 0 { + // analytics.total_contributions += contribution.amount; + + // if contribution + // .contributed_at <= round + // .deadline { // Check against round deadline + // analytics.on_time_payments += 1; + // } else { + // analytics.late_payments += 1; + // } + // } else if round.status == RoundStatus::Completed + // || round.status == RoundStatus::Cancelled { + // // Only count as missed if the round has concluded and no contribution was made + // analytics.missed_payments += 1; + // } + + // round_id += 1; + // } + + // // Calculate reliability score + // let total_evaluated_rounds = analytics.on_time_payments + // + analytics.late_payments + // + analytics.missed_payments; + // if total_evaluated_rounds > 0 { + // analytics.reliability_score = (analytics.on_time_payments * 100) + // / total_evaluated_rounds; + // } + + // analytics.last_updated = get_block_timestamp(); + // analytics + // } + + // fn calculate_system_health(self: @ContractState) -> u8 { + // let analytics = self.contribution_analytics.read(); + + // if analytics.total_rounds == 0 { + // return 100; // Perfect health if no rounds yet + // } + + // // Calculate health based on success rate + // let success_rate = (analytics.successful_rounds * 100) / analytics.total_rounds; + // success_rate.try_into().unwrap() + // } + // } + + // Helper functions for enhanced business logic + // impl HelperFunctions of HelperFunctionsTrait { + // fn get_contract_token_balance(self: @ContractState) -> u256 { + // // Get contract's balance of the primary token + // let token_address = self.token_address.read(); + // let erc20_dispatcher = IERC20Dispatcher { contract_address: token_address }; + // erc20_dispatcher.balance_of(starknet::get_contract_address()) + // } + + // fn get_contract_token_balance_specific( + // self: @ContractState, token: ContractAddress, + // ) -> u256 { + // // Get contract's balance of a specific token + // let erc20_dispatcher = IERC20Dispatcher { contract_address: token }; + // erc20_dispatcher.balance_of(starknet::get_contract_address()) + // } + + // fn transfer_tokens_to_member( + // ref self: ContractState, member: ContractAddress, amount: u256, + // ) { + // // Transfer tokens to a member + // let token_address = self.token_address.read(); + // let erc20_dispatcher = IERC20Dispatcher { contract_address: token_address }; + // assert(erc20_dispatcher.transfer(member, amount), 'Transfer failed'); + // } + + // fn transfer_tokens_to_address( + // ref self: ContractState, recipient: ContractAddress, amount: u256, + // ) { + // // Transfer tokens to any address + // let token_address = self.token_address.read(); + // let erc20_dispatcher = IERC20Dispatcher { contract_address: token_address }; + // assert(erc20_dispatcher.transfer(recipient, amount), 'Transfer failed'); + // } + + // fn transfer_specific_tokens_to_address( + // ref self: ContractState, + // token: ContractAddress, + // recipient: ContractAddress, + // amount: u256, + // ) { + // // Transfer specific tokens to an address + // let erc20_dispatcher = IERC20Dispatcher { contract_address: token }; + // assert(erc20_dispatcher.transfer(recipient, amount), 'Transfer failed'); + // } + + // fn calculate_member_total_contribution( + // self: @ContractState, member: ContractAddress, + // ) -> u256 { + // // Calculate total contributions across all rounds for a member + // let mut total = 0; + // let mut round_id = 1; + // while round_id <= self.round_ids.read() { + // let contribution = self.member_contributions.read((round_id, member)); + // total += contribution.amount; + // round_id += 1; + // } + // total + // } + + // fn refund_round_contributions(ref self: ContractState, round_id: u256) { + // // Refund all contributions for a specific round + // let mut member_index = 0; + // while member_index < self.member_count.read() { + // let member = self.member_by_index.read(member_index); + // if self.members.read(member) { + // let contribution = self.member_contributions.read((round_id, member)); + // if contribution.amount > 0 { + // self.transfer_tokens_to_member(member, contribution.amount); + // } + // } + // member_index += 1; + // } + // } + + // fn update_round_analytics(ref self: ContractState, round_id: u256, status: RoundStatus) { + // // Update analytics when round status changes + // let mut analytics = self.contribution_analytics.read(); + + // match status { + // RoundStatus::Completed => { + // analytics.successful_rounds += 1; + // analytics.total_rounds += 1; + // }, + // RoundStatus::Cancelled => { + // analytics.failed_rounds += 1; + // analytics.total_rounds += 1; + // }, + // _ => {}, + // } + + // self.contribution_analytics.write(analytics); + // } + + // fn is_token_supported(self: @ContractState, token: ContractAddress) -> bool { + // // Check if token is supported for payments + // let payment_config = self.payment_config.read(); + // let supported_tokens = payment_config.supported_tokens; + + // let mut i = 0; + // while i < supported_tokens.len() { + // if supported_tokens[i] == token { + // return true; + // } + // i += 1; + // } + // false + // } + + // fn calculate_next_payment_date(self: @ContractState, frequency: PaymentFrequency) -> u64 { + // // Calculate next payment date based on frequency + // let current_time = get_block_timestamp(); + + // match frequency { + // PaymentFrequency::Once => current_time, + // PaymentFrequency::Daily => current_time + 86400, // 24 hours + // PaymentFrequency::Weekly => current_time + 604800, // 7 days + // PaymentFrequency::Monthly => current_time + 2592000 // 30 days + // } + // } + + // fn process_contribution( + // ref self: ContractState, round_id: u256, member: ContractAddress, amount: u256, + // ) { + // // Process a contribution for a round + // let mut round = self.rounds.read(round_id); + // round.total_contributions += amount; + // self.rounds.write(round_id, round); + + // // Update member contribution record + // let contribution = MemberContribution { + // member, amount, contributed_at: get_block_timestamp(), + // }; + // self.member_contributions.write((round_id, member), contribution); + // } + + // fn calculate_reliability_bonus( + // self: @ContractState, current_rating: u8, is_early: bool, + // ) -> u8 { + // // Calculate reliability rating bonus for early payments + // if is_early && current_rating < 100 { + // return current_rating + 5; // +5 points for early payment + // } + // current_rating + // } + + // fn get_token_price_from_oracle(self: @ContractState, token: ContractAddress) -> u256 { + // // Get token price from oracle (simplified implementation) + // // In real implementation, this would call an oracle contract + // // For now, return a default price + // 1000000000000000000 // 1.0 in wei format + // } + + // fn is_round_scheduled(self: @ContractState, round_id: u256) -> bool { + // let scheduled_round = self + // .scheduled_rounds + // .read(round_id); // Reads the scheduled round data + // // Checks if the round_id field of the struct is non-zero, indicating it has been set + // scheduled_round.round_id > 0 + // } + + // // Ensure all components update related state consistently + // fn complete_round(ref self: ContractState, round_id: u256) { + // self.ownable.assert_only_owner(); // Or triggered by an authorized keeper + // self.emergency.assert_not_paused(); // Round completion should happen when not paused + + // let mut round = self.rounds.read(round_id); // Reads the round data + // assert( + // round.status == RoundStatus::Active, 'Round not active', + // ); // Ensures round is in active state + // assert( + // get_block_timestamp() >= round.deadline, 'Round not expired', + // ); // Ensures deadline has passed + + // // Update round status to Completed + // round.status = RoundStatus::Completed; + // self.rounds.write(round_id, round); // Writes the updated round status + + // // Update scheduled round if it exists + // if self.is_round_scheduled(round_id) { + // let mut scheduled_round = self.scheduled_rounds.read(round_id); + // scheduled_round.status = RoundStatus::Completed; + // self.scheduled_rounds.write(round_id, scheduled_round); + // } + + // // Update analytics related to this round + // self + // .update_round_analytics( + // round_id, RoundStatus::Completed, + // ); // Calls internal analytics helper + + // // Transfer funds to the designated recipient + // let total_contributions_for_round = self + // .enhanced_contribution_internal + // ._get_total_contributions_for_round(round_id); // Get total contributions + // self + // .transfer_tokens_to_member( + // round.recipient, total_contributions_for_round, + // ); // Calls internal token transfer helper + + // // Apply penalties for missed contributions in this round + // self + // .penalty_internal + // ._apply_missed_contribution_penalties(round_id); // Calls internal penalty helper + + // self + // .emit( + // Event::RoundCompleted( + // RoundCompleted { + // round_id, + // recipient: round.recipient, + // total_amount: total_contributions_for_round, + // member_count: self.member_count.read(), + // completion_time: get_block_timestamp(), + // }, + // ), + // ); // Emits an event + // } + + // // Add penalty pool distribution logic + // fn distribute_penalty_pool(ref self: ContractState) { + // self.ownable.assert_only_owner(); // Only owner should trigger distribution + // self.emergency.assert_not_paused(); // Distribution should happen when not paused + + // let penalty_pool_amount = self.penalty_pool.read(); // Reads the total penalty pool + // if penalty_pool_amount == 0 { + // return; + // } + + // let mut total_compliant_contributions = 0; + // let mut compliant_members_list = array![]; // Use Array to collect compliant members + + // // Calculate total contributions from compliant members and collect their addresses + // let mut member_index = 0; + // let total_members = self + // .member_count + // .read(); // Assuming member_count tracks total registered members + // while member_index < total_members { + // let member_address = self + // .member_by_index + // .read(member_index); // Get member address by index + // let member_profile = self + // .member_profiles + // .read(member_address); // Read member profile + + // // A member is compliant if not banned and has a good credit score (example) + // if !member_profile.is_banned + // && member_profile.credit_score >= 80 { // Assuming is_banned in MemberProfile + // let member_contribution = self + // .calculate_member_total_contribution( + // member_address, + // ); // Calls internal helper + // total_compliant_contributions += member_contribution; + // compliant_members_list.append(member_address); + // } + // member_index += 1; + // } + + // if total_compliant_contributions > 0 { + // // Distribute penalty pool proportionally + // let mut distributed_count = 0; + // let mut i = 0; + // while i < compliant_members_list.len() { + // let member_address = *compliant_members_list.at(i); + // let member_contribution = self + // .calculate_member_total_contribution( + // member_address, + // ); // Calls internal helper + // let share = (member_contribution * penalty_pool_amount) + // / total_compliant_contributions; + + // if share > 0 { + // self + // .transfer_tokens_to_member( + // member_address, share, + // ); // Calls internal token transfer helper + // distributed_count += 1; + // } + // i += 1; + // } + + // // Reset penalty pool + // self.penalty_pool.write(0); // Resets the penalty pool + + // self + // .emit( + // Event::PenaltyPoolDistributed( + // PenaltyPoolDistributed { + // total_amount: penalty_pool_amount, + // recipient_count: distributed_count, + // distribution_type: 'proportional', + // timestamp: get_block_timestamp(), + // }, + // ), + // ); // Emits an event + // } + // } + // } + + // // Implementation of Internal Traits for Enhanced Components + // impl PenaltyInternalImpl of PenaltyInternalTrait { + // fn _apply_late_fee( + // ref self: ContractState, member: ContractAddress, round_id: u256, amount: u256, + // ) { + // // Call the existing penalty function + // self.penalty.apply_late_fee(member, round_id); + // } + + // fn _add_strike(ref self: ContractState, member: ContractAddress, round_id: u256) { + // // Call the existing penalty function + // self.penalty.add_strike(member, round_id); + // } + + // fn _remove_strike(ref self: ContractState, member: ContractAddress) { + // // Call the existing penalty function + // self.penalty.remove_strike(member); + // } + + // fn _ban_member(ref self: ContractState, member: ContractAddress) { + // // Call the existing penalty function + // self.penalty.ban_member(member); + // } + + // fn _unban_member(ref self: ContractState, member: ContractAddress) { + // // Call the existing penalty function + // self.penalty.unban_member(member); + // } + + // fn _apply_missed_contribution_penalties(ref self: ContractState, round_id: u256) { + // // Apply penalties for missed contributions in a round + // let round = self.rounds.read(round_id); + // let mut member_index = 0; + // let total_members = self.member_count.read(); + + // while member_index < total_members { + // let member_address = self.member_by_index.read(member_index); + // if self.members.read(member_address) { + // let contribution = self.member_contributions.read((round_id, member_address)); + // if contribution.amount == 0 { + // // Member missed contribution - apply penalty + // self.penalty.add_strike(member_address, round_id); + // } + // } + // member_index += 1; + // } + // } + // } + + // impl EnhancedContributionInternalImpl of EnhancedContributionInternalTrait { + // fn _process_contribution( + // ref self: ContractState, round_id: u256, member: ContractAddress, amount: u256, + // ) { + // // Process a contribution for a round + // let mut round = self.rounds.read(round_id); + // round.total_contributions += amount; + // self.rounds.write(round_id, round); + + // // Update member contribution record + // let contribution = MemberContribution { + // member, amount, contributed_at: get_block_timestamp(), + // }; + // self.member_contributions.write((round_id, member), contribution); + // } + + // fn _get_total_contributions_for_round(self: @ContractState, round_id: u256) -> u256 { + // let round = self.rounds.read(round_id); + // round.total_contributions + // } + + // fn _is_contribution_late( + // self: @ContractState, round_id: u256, member: ContractAddress, + // ) -> bool { + // let round = self.rounds.read(round_id); + // let contribution = self.member_contributions.read((round_id, member)); + // if contribution.amount == 0 { + // return false; // No contribution made + // } + + // let current_time = get_block_timestamp(); + // current_time > round.deadline + // } + // } + + // impl MemberProfileInternalImpl of MemberProfileInternalTrait { + // fn _update_profile_after_contribution( + // ref self: ContractState, member: ContractAddress, amount: u256, + // ) { + // let mut profile = self.member_profiles.read(member); + + // // Update contribution statistics + // profile.total_contributions += amount; + + // // Update credit score based on payment timing + // let current_time = get_block_timestamp(); + // let round = self.get_current_active_round(); + // if round > 0 { + // let round_data = self.rounds.read(round); + // if current_time <= round_data.deadline { + // // On-time payment - boost credit score + // if profile.credit_score < 100 { + // profile.credit_score += 2; + // } + // } + // } + + // self.member_profiles.write(member, profile); + // } + + // fn _calculate_member_total_contribution( + // self: @ContractState, member: ContractAddress, + // ) -> u256 { + // self.calculate_member_total_contribution(member) + // } + + // fn _update_reliability_rating( + // ref self: ContractState, member: ContractAddress, new_rating: u8, + // ) { + // let mut profile = self.member_profiles.read(member); + // profile.reliability_rating = new_rating; + // self.member_profiles.write(member, profile); + // } + // } + + // impl AnalyticsInternalImpl of AnalyticsInternalTrait { + // fn _update_round_analytics(ref self: ContractState, round_id: u256, status: RoundStatus) { + // self.update_round_analytics(round_id, status); + // } + + // fn _update_member_performance_for_round(ref self: ContractState, round_id: u256) { + // // Update member performance analytics for a specific round + // let round = self.rounds.read(round_id); + // let mut member_index = 0; + // let total_members = self.member_count.read(); + + // while member_index < total_members { + // let member_address = self.member_by_index.read(member_index); + // if self.members.read(member_address) { + // let contribution = self.member_contributions.read((round_id, member_address)); + // let mut member_analytics = self.member_analytics.read(member_address); + + // if contribution.amount > 0 { + // member_analytics.total_contributions += contribution.amount; + // if contribution.contributed_at <= round.deadline { + // member_analytics.on_time_payments += 1; + // } else { + // member_analytics.late_payments += 1; + // } + // } else if round.status == RoundStatus::Completed + // || round.status == RoundStatus::Cancelled { + // member_analytics.missed_payments += 1; + // } + + // member_analytics.last_updated = get_block_timestamp(); + // self.member_analytics.write(member_address, member_analytics); + // } + // member_index += 1; + // } + // } + + // fn _calculate_system_health(self: @ContractState) -> u8 { + // self.calculate_system_health() + // } + // } + + // impl TokenTransferInternalImpl of TokenTransferInternalTrait { + // fn _transfer_tokens_to_member( + // ref self: ContractState, member: ContractAddress, amount: u256, + // ) { + // self.transfer_tokens_to_member(member, amount); + // } + + // fn _transfer_tokens_to_address( + // ref self: ContractState, recipient: ContractAddress, amount: u256, + // ) { + // self.transfer_tokens_to_address(recipient, amount); + // } + + // fn _transfer_specific_tokens_to_address( + // ref self: ContractState, + // token: ContractAddress, + // recipient: ContractAddress, + // amount: u256, + // ) { + // self.transfer_specific_tokens_to_address(token, recipient, amount); + // } + // } + + // Internal helper functions + #[generate_trait] + impl InternalFunctions of InternalFunctionsTrait { + fn _validate_kyc_and_limits(self: @ContractState, user: ContractAddress, amount: u256) { + // Check KYC validity + assert(IStarkRemitImpl::is_kyc_valid(self, user), KYCErrors::INVALID_KYC_STATUS); + + // Check transaction limits + let kyc_data = self.user_kyc_data.read(user); + let level_u8 = self._kyc_level_to_u8(kyc_data.level); + + // Check single transaction limit + let single_limit = self.single_limits.read(level_u8); + assert(amount <= single_limit, KYCErrors::SINGLE_TX_LIMIT_EXCEEDED); + + // Check daily limit + let daily_limit = self.daily_limits.read(level_u8); + let current_usage = self._get_daily_usage(user); + assert(current_usage + amount <= daily_limit, KYCErrors::DAILY_LIMIT_EXCEEDED); + } + + fn _get_daily_usage(self: @ContractState, user: ContractAddress) -> u256 { + let current_time = get_block_timestamp(); + let last_reset = self.last_reset.read(user); + + // Reset if it's a new day (86400 seconds = 24 hours) + if current_time > last_reset + 86400 { + return 0; + } + + self.daily_usage.read(user) + } + + fn _record_daily_usage(ref self: ContractState, user: ContractAddress, amount: u256) { + let current_time = get_block_timestamp(); + let last_reset = self.last_reset.read(user); + + if current_time > last_reset + 86400 { + // Reset for new day + self.daily_usage.write(user, amount); + self.last_reset.write(user, current_time); + } else { + // Add to current day usage + let current_usage = self.daily_usage.read(user); + self.daily_usage.write(user, current_usage + amount); + } + } + + fn _kyc_level_to_u8(self: @ContractState, level: KycLevel) -> u8 { + match level { + KycLevel::None => 0, + KycLevel::Basic => 1, + KycLevel::Enhanced => 2, + KycLevel::Premium => 3, + } + } + + fn _set_default_transaction_limits(ref self: ContractState) { + // None level - very restricted + self.daily_limits.write(0, 100_000_000_000_000_000); // 0.1 tokens + self.single_limits.write(0, 50_000_000_000_000_000); // 0.05 tokens + + // Basic level - moderate limits + self.daily_limits.write(1, 1000_000_000_000_000_000_000); // 1,000 tokens + self.single_limits.write(1, 500_000_000_000_000_000_000); // 500 tokens + + // Enhanced level - higher limits + self.daily_limits.write(2, 10000_000_000_000_000_000_000); // 10,000 tokens + self.single_limits.write(2, 5000_000_000_000_000_000_000); // 5,000 tokens + + // Premium level - maximum limits + self.daily_limits.write(3, 100000_000_000_000_000_000_000); // 100,000 tokens + self.single_limits.write(3, 50000_000_000_000_000_000_000); // 50,000 tokens + } + + fn _calculate_total_allocated_tokens(self: @ContractState, token: ContractAddress) -> u256 { + // Calculate total allocated tokens = member balances + tokens locked in ongoing rounds + let mut total_allocated = 0_u256; + + // Get the primary token address for comparison + let primary_token = self.token_address.read(); + assert(token == primary_token, 'Only primary token supported'); + + // If this is the primary token, calculate allocated tokens + if token == primary_token { + // Add member balances for the primary token + let member_count = self.member_count.read(); + let mut i = 0_u32; + while i < member_count { + let member = self.member_by_index.read(i); + if self.members.read(member) { + let member_balance = self.balances.read(member); + total_allocated += member_balance; + } + i += 1; + } + + // Add tokens locked in ongoing rounds (active rounds with contributions) + let current_round_id = self.round_ids.read(); + if current_round_id > 0 { + let mut round_id = 1_u256; + while round_id <= current_round_id { + let round = self.rounds.read(round_id); + // Only count active rounds that have contributions + if round.status == RoundStatus::Active && round.total_contributions > 0 { + total_allocated += round.total_contributions; + } + round_id += 1; + } + } + } + total_allocated + } + + fn _record_transfer_history( + ref self: ContractState, + transfer_id: u256, + action: felt252, + actor: ContractAddress, + previous_status: TransferStatus, + new_status: TransferStatus, + details: felt252, + ) { + let current_time = get_block_timestamp(); + + // Create history entry + let history = TransferHistory { + transfer_id, + action, + actor, + timestamp: current_time, + previous_status, + new_status, + details, + }; + + // Store in transfer history + let history_count = self.transfer_history_count.read(transfer_id); + self.transfer_history.write((transfer_id, history_count), history); + self.transfer_history_count.write(transfer_id, history_count + 1); + + // Store in actor history + let actor_count = self.actor_history_count.read(actor); + self.actor_history.write((actor, actor_count), (transfer_id, history_count)); + self.actor_history_count.write(actor, actor_count + 1); + + // Store in action history + let action_count = self.action_history_count.read(action); + self.action_history.write((action, action_count), (transfer_id, history_count)); + self.action_history_count.write(action, action_count + 1); + + // Emit event + self + .emit( + TransferHistoryRecorded { transfer_id, action, actor, timestamp: current_time }, + ); + } + + // Generates and stores a new unique group ID for a savings group + // Returns the newly generated group ID + fn _new_group_id(ref self: ContractState) -> u64 { + let group_id = self.group_count.read(); + + self.group_count.write(group_id + 1); + + group_id + } + + // Helper functions for emergency operations + fn get_contract_token_balance(self: @ContractState) -> u256 { + // Get contract's balance of the primary token + let token_address = self.token_address.read(); + let erc20_dispatcher = IERC20Dispatcher { contract_address: token_address }; + erc20_dispatcher.balance_of(starknet::get_contract_address()) + } + + fn get_contract_token_balance_specific( + self: @ContractState, token: ContractAddress, + ) -> u256 { + // Get contract's balance of a specific token + let erc20_dispatcher = IERC20Dispatcher { contract_address: token }; + erc20_dispatcher.balance_of(starknet::get_contract_address()) + } + + fn transfer_tokens_to_member( + ref self: ContractState, member: ContractAddress, amount: u256, + ) { + // Transfer tokens to a member + let token_address = self.token_address.read(); + let erc20_dispatcher = IERC20Dispatcher { contract_address: token_address }; + assert(erc20_dispatcher.transfer(member, amount), 'Transfer failed'); + } + + fn transfer_tokens_to_address( + ref self: ContractState, recipient: ContractAddress, amount: u256, + ) { + // Transfer tokens to any address + let token_address = self.token_address.read(); + let erc20_dispatcher = IERC20Dispatcher { contract_address: token_address }; + assert(erc20_dispatcher.transfer(recipient, amount), 'Transfer failed'); + } + + fn transfer_specific_tokens_to_address( + ref self: ContractState, + token: ContractAddress, + recipient: ContractAddress, + amount: u256, + ) { + // Transfer specific tokens to an address + let erc20_dispatcher = IERC20Dispatcher { contract_address: token }; + assert(erc20_dispatcher.transfer(recipient, amount), 'Transfer failed'); + } + + fn calculate_member_total_contribution( + self: @ContractState, member: ContractAddress, + ) -> u256 { + // Calculate total contributions across all rounds for a member + let mut total = 0; + let mut round_id = 1; + while round_id <= self.round_ids.read() { + let contribution = self.member_contributions.read((round_id, member)); + total += contribution.amount; + round_id += 1; + } + total + } + + fn refund_round_contributions(ref self: ContractState, round_id: u256) { + // Refund all contributions for a specific round + let mut member_index = 0; + while member_index < self.member_count.read() { + let member = self.member_by_index.read(member_index); + if self.members.read(member) { + let contribution = self.member_contributions.read((round_id, member)); + if contribution.amount > 0 { + self.transfer_tokens_to_member(member, contribution.amount); + } + } + member_index += 1; + } + } + + fn update_round_analytics(ref self: ContractState, round_id: u256, status: RoundStatus) { + // Update analytics when round status changes + // This is a simplified implementation since analytics component is commented out + // In a full implementation, this would update the analytics storage + } + + // Private helper function to remove member from member list + fn _remove_member_from_list(ref self: ContractState, member: ContractAddress) { + // Check if member is currently active + if !self.members.read(member) { + return; // Already removed + } + + // Mark member as inactive + self.members.write(member, false); + + // Decrease member count + let current_count = self.member_count.read(); + self.member_count.write(current_count - 1); + + // Find and remove member from member_by_index + let mut i = 0; + let total_members = current_count; + + while i < total_members { + let member_at_index = self.member_by_index.read(i); + if member_at_index == member { + // Found the member, remove by setting to zero address + self.member_by_index.write(i, 0.try_into().unwrap()); + break; + } + i += 1; + } + } + + // Private helper function to add member back to member list + fn _add_member_to_list(ref self: ContractState, member: ContractAddress) { + // Check if member is already active + if self.members.read(member) { + return; // Already active + } + + // Mark member as active + self.members.write(member, true); + + // Increase member count + let current_count = self.member_count.read(); + self.member_count.write(current_count + 1); + + // Add member to member_by_index at the end + self.member_by_index.write(current_count, member); + } + } } diff --git a/tests/test_analytics_component.cairo b/tests/test_analytics_component.cairo new file mode 100644 index 0000000..64a5bda --- /dev/null +++ b/tests/test_analytics_component.cairo @@ -0,0 +1,197 @@ +// use starknet::ContractAddress; +// use starknet::testing::{set_caller_address, set_block_timestamp}; +// use starknet::contract_address_const; +// use starkremit_contract::component::analytics::{ +// analytics_component, IAnalyticsDispatcher, IAnalyticsDispatcherTrait, +// ContributionAnalytics, MemberAnalytics, RoundPerformanceMetrics, FinancialReport, SystemHealthMetrics +// }; + +// const ADMIN: felt252 = 0x123; +// const MEMBER1: felt252 = 0x789; +// const MEMBER2: felt252 = 0xABC; + +// fn setup() -> ContractAddress { +// let admin_address = contract_address_const::(); +// let contract_address = contract_address_const::<0x1>(); + +// set_caller_address(admin_address); +// set_block_timestamp(1000); + +// contract_address +// } + +// #[test] +// fn test_generate_contribution_report() { +// let contract_address = setup(); +// let analytics = IAnalyticsDispatcher { contract_address }; + +// let report = analytics.generate_contribution_report(); + +// // Initial report should have default values +// assert!(report.total_rounds == 0, "Initial total rounds should be 0"); +// assert!(report.successful_rounds == 0, "Initial successful rounds should be 0"); +// assert!(report.failed_rounds == 0, "Initial failed rounds should be 0"); +// } + +// #[test] +// fn test_get_member_performance_new_member() { +// let contract_address = setup(); +// let analytics = IAnalyticsDispatcher { contract_address }; +// let member_address = contract_address_const::(); + +// let performance = analytics.get_member_performance(member_address); + +// // New member should have default values +// assert!(performance.total_contributions == 0, "New member should have 0 contributions"); +// assert!(performance.on_time_payments == 0, "New member should have 0 on-time payments"); +// assert!(performance.late_payments == 0, "New member should have 0 late payments"); +// assert!(performance.missed_payments == 0, "New member should have 0 missed payments"); +// assert!(performance.reliability_score == 50, "New member should have neutral reliability score"); +// } + +// #[test] +// fn test_calculate_system_health() { +// let contract_address = setup(); +// let analytics = IAnalyticsDispatcher { contract_address }; + +// let health_score = analytics.calculate_system_health(); + +// // Health score should be between 0 and 100 +// assert!(health_score <= 100, "Health score should not exceed 100"); +// assert!(health_score >= 0, "Health score should not be negative"); +// } + +// #[test] +// fn test_member_performance_tracking() { +// let contract_address = setup(); +// let analytics = IAnalyticsDispatcher { contract_address }; +// let member_address = contract_address_const::(); + +// // Get initial performance +// let initial_performance = analytics.get_member_performance(member_address); +// assert!(initial_performance.reliability_score == 50, "Should start with neutral score"); + +// // Performance should remain consistent on multiple calls +// let second_call = analytics.get_member_performance(member_address); +// assert!(second_call.reliability_score == initial_performance.reliability_score, "Performance should be consistent"); +// } + +// #[test] +// fn test_multiple_members_analytics() { +// let contract_address = setup(); +// let analytics = IAnalyticsDispatcher { contract_address }; +// let member1_address = contract_address_const::(); +// let member2_address = contract_address_const::(); + +// let performance1 = analytics.get_member_performance(member1_address); +// let performance2 = analytics.get_member_performance(member2_address); + +// // Both members should have independent analytics +// assert!(performance1.total_contributions == 0, "Member 1 should start with 0 contributions"); +// assert!(performance2.total_contributions == 0, "Member 2 should start with 0 contributions"); +// assert!(performance1.reliability_score == 50, "Member 1 should have neutral score"); +// assert!(performance2.reliability_score == 50, "Member 2 should have neutral score"); +// } + +// #[test] +// fn test_system_health_consistency() { +// let contract_address = setup(); +// let analytics = IAnalyticsDispatcher { contract_address }; + +// let health1 = analytics.calculate_system_health(); +// let health2 = analytics.calculate_system_health(); + +// // Health should be consistent (assuming no state changes) +// assert!(health1 == health2, "System health should be consistent"); +// } + +// #[test] +// fn test_contribution_report_structure() { +// let contract_address = setup(); +// let analytics = IAnalyticsDispatcher { contract_address }; + +// let report = analytics.generate_contribution_report(); + +// // Verify report structure +// assert!(report.average_completion_time >= 0, "Completion time should be non-negative"); +// assert!(report.total_penalties_collected >= 0, "Penalties should be non-negative"); + +// // Total rounds should equal successful + failed +// assert!( +// report.total_rounds == report.successful_rounds + report.failed_rounds, +// "Total rounds should equal successful plus failed rounds" +// ); +// } + +// #[test] +// fn test_member_analytics_initialization() { +// let contract_address = setup(); +// let analytics = IAnalyticsDispatcher { contract_address }; +// let member_address = contract_address_const::(); + +// let performance = analytics.get_member_performance(member_address); + +// // Verify all fields are properly initialized +// assert!(performance.total_contributions == 0, "Contributions should start at 0"); +// assert!(performance.on_time_payments == 0, "On-time payments should start at 0"); +// assert!(performance.late_payments == 0, "Late payments should start at 0"); +// assert!(performance.missed_payments == 0, "Missed payments should start at 0"); +// assert!(performance.reliability_score == 50, "Reliability should start at neutral"); +// assert!(performance.last_updated > 0, "Last updated should be set"); +// } + +// #[test] +// fn test_system_health_bounds() { +// let contract_address = setup(); +// let analytics = IAnalyticsDispatcher { contract_address }; + +// // Test multiple calls to ensure bounds are maintained +// for _i in 0..10 { +// let health = analytics.calculate_system_health(); +// assert!(health >= 0 && health <= 100, "Health score must be between 0 and 100"); +// } +// } + +// #[test] +// fn test_analytics_timestamp_tracking() { +// let contract_address = setup(); +// let analytics = IAnalyticsDispatcher { contract_address }; +// let member_address = contract_address_const::(); + +// let initial_timestamp = 1000_u64; +// set_block_timestamp(initial_timestamp); + +// let performance = analytics.get_member_performance(member_address); +// assert!(performance.last_updated >= initial_timestamp, "Should have current or later timestamp"); + +// // Move time forward +// set_block_timestamp(2000); + +// let updated_performance = analytics.get_member_performance(member_address); +// // For a new member, the timestamp should be updated +// assert!(updated_performance.last_updated >= 2000, "Should reflect new timestamp"); +// } + +// #[test] +// fn test_contribution_report_reliability_distribution() { +// let contract_address = setup(); +// let analytics = IAnalyticsDispatcher { contract_address }; + +// let report = analytics.generate_contribution_report(); + +// // Reliability distribution array should be valid (though may be empty initially) +// assert!(report.member_reliability_distribution.len() >= 0, "Distribution should be valid array"); +// } + +// #[test] +// fn test_empty_state_analytics() { +// let contract_address = setup(); +// let analytics = IAnalyticsDispatcher { contract_address }; + +// // Test that analytics work correctly with no data +// let report = analytics.generate_contribution_report(); +// let health = analytics.calculate_system_health(); + +// assert!(report.total_rounds == 0, "No rounds should exist initially"); +// assert!(health >= 0, "Health should still be calculable"); +// } \ No newline at end of file diff --git a/tests/test_auto_schedule_component.cairo b/tests/test_auto_schedule_component.cairo new file mode 100644 index 0000000..7376685 --- /dev/null +++ b/tests/test_auto_schedule_component.cairo @@ -0,0 +1,241 @@ +// use starknet::ContractAddress; +// use starknet::testing::{set_caller_address, set_block_timestamp}; +// use starknet::contract_address_const; +// use starkremit_contract::component::auto_schedule::{ +// auto_schedule_component, IAutoScheduleDispatcher, IAutoScheduleDispatcherTrait, +// AutoScheduleConfig, ScheduledRound +// }; +// use starkremit_contract::base::types::RoundStatus; + +// const ADMIN: felt252 = 0x123; +// const NON_ADMIN: felt252 = 0x456; +// const MEMBER1: felt252 = 0x789; +// const MEMBER2: felt252 = 0xABC; + +// fn setup() -> ContractAddress { +// let admin_address = contract_address_const::(); +// let contract_address = contract_address_const::<0x1>(); + +// set_caller_address(admin_address); +// set_block_timestamp(1000); + +// contract_address +// } + +// fn get_default_auto_schedule_config() -> AutoScheduleConfig { +// AutoScheduleConfig { +// round_duration_days: 30, +// start_date: 1000, +// auto_activation_enabled: true, +// auto_completion_enabled: true, +// rolling_schedule_count: 3, +// } +// } + +// #[test] +// fn test_setup_auto_schedule() { +// let contract_address = setup(); +// let auto_schedule = IAutoScheduleDispatcher { contract_address }; + +// let config = get_default_auto_schedule_config(); +// auto_schedule.setup_auto_schedule(config); + +// let retrieved_config = auto_schedule.get_auto_schedule_config(); +// assert!(retrieved_config.round_duration_days == 30, "Round duration should match"); +// assert!(retrieved_config.auto_activation_enabled, "Auto activation should be enabled"); +// assert!(retrieved_config.rolling_schedule_count == 3, "Rolling schedule count should match"); +// } + +// #[test] +// fn test_get_current_active_round() { +// let contract_address = setup(); +// let auto_schedule = IAutoScheduleDispatcher { contract_address }; + +// let config = get_default_auto_schedule_config(); +// auto_schedule.setup_auto_schedule(config); + +// // Initially no active round +// let active_round_id = auto_schedule.get_current_active_round(); +// assert!(active_round_id == 0, "No active round should exist initially"); +// } + +// #[test] +// fn test_get_next_scheduled_rounds() { +// let contract_address = setup(); +// let auto_schedule = IAutoScheduleDispatcher { contract_address }; + +// let config = get_default_auto_schedule_config(); +// auto_schedule.setup_auto_schedule(config); + +// // Get next scheduled rounds +// let scheduled_rounds = auto_schedule.get_next_scheduled_rounds(5); +// // Initially should be empty or have few rounds +// assert!(scheduled_rounds.len() <= 5, "Should not return more than requested"); +// } + +// #[test] +// fn test_maintain_rolling_schedule() { +// let contract_address = setup(); +// let auto_schedule = IAutoScheduleDispatcher { contract_address }; + +// let config = get_default_auto_schedule_config(); +// auto_schedule.setup_auto_schedule(config); + +// // Maintain rolling schedule +// auto_schedule.maintain_rolling_schedule(); + +// // Should create future rounds based on rolling_schedule_count +// let scheduled_rounds = auto_schedule.get_next_scheduled_rounds(5); +// assert!(scheduled_rounds.len() >= 0, "Should have created scheduled rounds"); +// } + +// #[test] +// fn test_modify_schedule() { +// let contract_address = setup(); +// let auto_schedule = IAutoScheduleDispatcher { contract_address }; + +// let config = get_default_auto_schedule_config(); +// auto_schedule.setup_auto_schedule(config); + +// let round_id = 1_u256; +// let new_deadline = 2000_u64; + +// // This should work even if round doesn't exist yet (implementation handles it) +// auto_schedule.modify_schedule(round_id, new_deadline); +// } + +// #[test] +// fn test_auto_complete_expired_rounds() { +// let contract_address = setup(); +// let auto_schedule = IAutoScheduleDispatcher { contract_address }; + +// let config = get_default_auto_schedule_config(); +// auto_schedule.setup_auto_schedule(config); + +// // Move time forward to simulate expired rounds +// set_block_timestamp(5000); + +// // Try to complete expired rounds +// auto_schedule.auto_complete_expired_rounds(); + +// // This should complete any rounds that have expired +// // The function should not fail even if no rounds exist +// } + +// #[test] +// #[should_panic(expected: ('NOT_ADMIN',))] +// fn test_setup_auto_schedule_unauthorized() { +// let contract_address = setup(); +// let auto_schedule = IAutoScheduleDispatcher { contract_address }; +// let non_admin_address = contract_address_const::(); + +// set_caller_address(non_admin_address); + +// let config = get_default_auto_schedule_config(); +// auto_schedule.setup_auto_schedule(config); +// } + +// #[test] +// #[should_panic(expected: ('INVALID_CONFIG',))] +// fn test_setup_invalid_config() { +// let contract_address = setup(); +// let auto_schedule = IAutoScheduleDispatcher { contract_address }; + +// let invalid_config = AutoScheduleConfig { +// round_duration_days: 0, // Invalid: zero duration +// start_date: 1000, +// auto_activation_enabled: true, +// auto_completion_enabled: true, +// rolling_schedule_count: 3, +// }; + +// auto_schedule.setup_auto_schedule(invalid_config); +// } + +// #[test] +// #[should_panic(expected: ('INVALID_CONFIG',))] +// fn test_setup_invalid_rolling_count() { +// let contract_address = setup(); +// let auto_schedule = IAutoScheduleDispatcher { contract_address }; + +// let invalid_config = AutoScheduleConfig { +// round_duration_days: 30, +// start_date: 1000, +// auto_activation_enabled: true, +// auto_completion_enabled: true, +// rolling_schedule_count: 0, // Invalid: zero rolling count +// }; + +// auto_schedule.setup_auto_schedule(invalid_config); +// } + +// #[test] +// #[should_panic(expected: ('INVALID_CONFIG',))] +// fn test_setup_excessive_rolling_count() { +// let contract_address = setup(); +// let auto_schedule = IAutoScheduleDispatcher { contract_address }; + +// let invalid_config = AutoScheduleConfig { +// round_duration_days: 30, +// start_date: 1000, +// auto_activation_enabled: true, +// auto_completion_enabled: true, +// rolling_schedule_count: 10, // Invalid: too high +// }; + +// auto_schedule.setup_auto_schedule(invalid_config); +// } + +// #[test] +// #[should_panic(expected: ('NOT_ADMIN',))] +// fn test_modify_schedule_unauthorized() { +// let contract_address = setup(); +// let auto_schedule = IAutoScheduleDispatcher { contract_address }; +// let non_admin_address = contract_address_const::(); + +// // Setup as admin first +// let config = get_default_auto_schedule_config(); +// auto_schedule.setup_auto_schedule(config); + +// // Try to modify as non-admin +// set_caller_address(non_admin_address); +// auto_schedule.modify_schedule(1_u256, 2000_u64); +// } + +// #[test] +// fn test_schedule_with_disabled_features() { +// let contract_address = setup(); +// let auto_schedule = IAutoScheduleDispatcher { contract_address }; + +// let config = AutoScheduleConfig { +// round_duration_days: 30, +// start_date: 1000, +// auto_activation_enabled: false, // Disabled +// auto_completion_enabled: false, // Disabled +// rolling_schedule_count: 2, +// }; + +// auto_schedule.setup_auto_schedule(config); + +// // Auto completion should do nothing when disabled +// auto_schedule.auto_complete_expired_rounds(); + +// let retrieved_config = auto_schedule.get_auto_schedule_config(); +// assert!(!retrieved_config.auto_activation_enabled, "Auto activation should be disabled"); +// assert!(!retrieved_config.auto_completion_enabled, "Auto completion should be disabled"); +// } + +// #[test] +// fn test_future_round_limit() { +// let contract_address = setup(); +// let auto_schedule = IAutoScheduleDispatcher { contract_address }; + +// let config = get_default_auto_schedule_config(); +// auto_schedule.setup_auto_schedule(config); + +// // Request more rounds than available +// let scheduled_rounds = auto_schedule.get_next_scheduled_rounds(100); + +// // Should not return more than reasonable number of future rounds +// assert!(scheduled_rounds.len() <= 100, "Should handle large requests gracefully"); +// } \ No newline at end of file diff --git a/tests/test_emergency_component.cairo b/tests/test_emergency_component.cairo new file mode 100644 index 0000000..5303839 --- /dev/null +++ b/tests/test_emergency_component.cairo @@ -0,0 +1,277 @@ +use starknet::ContractAddress; +// Replaced starknet::testing imports with snforge_std cheatcodes +use starknet::contract_address_const; +use starkremit_contract::component::emergency::{ + emergency_component, IEmergency, IEmergencyDispatcher, IEmergencyDispatcherTrait, + EmergencyConfig +}; +// Required snforge_std imports for deployment and cheatcodes +use snforge_std::{ + ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address, + stop_cheat_caller_address, start_cheat_block_timestamp, stop_cheat_block_timestamp +}; +use core::array::ArrayTrait; // Needed for array! macro and Serde + +const ADMIN: felt252 = 0x123; +const NON_ADMIN: felt252 = 0x456; +const MEMBER: felt252 = 0x789; + +#[starknet::contract] +mod TestContract { + use super::*; + + component!(path: emergency_component, storage: emergency, event: EmergencyEvent); + + #[storage] + struct Storage { + #[substorage(v0)] + emergency: emergency_component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + EmergencyEvent: emergency_component::Event, + } + + #[constructor] + fn constructor(ref self: ContractState, admin: ContractAddress) { + self.emergency.initializer(admin); + } + + #[abi(embed_v0)] + impl EmergencyInternalImpl = emergency_component::InternalImpl; +} + +// Fixed setup function to correctly deploy the contract and set initial cheats +fn setup() -> ContractAddress { + let admin_address = contract_address_const::(); + + let contract_class = declare("TestContract").unwrap().contract_class(); + + let mut constructor_calldata = array![]; + Serde::serialize(@admin_address, ref constructor_calldata); + + let (contract_address, _) = contract_class.deploy(@constructor_calldata).unwrap(); + + start_cheat_caller_address(contract_address, admin_address); + start_cheat_block_timestamp(contract_address, 1000); + + contract_address +} + +#[test] +fn test_emergency_initialization() { + let contract_address = setup(); + let emergency = IEmergencyDispatcher { contract_address }; + + // Test that component is initialized correctly + assert!(!emergency.is_paused(), "Contract should start unpaused"); + assert!(emergency.get_pause_reason() == 0, "Initial pause reason should be 0"); + // Assuming initializer does not set a timestamp for pause, so it should be 0 initially + assert!(emergency.get_pause_timestamp() == 0, "Initial pause timestamp should be 0"); + + stop_cheat_caller_address(contract_address); + stop_cheat_block_timestamp(contract_address); +} + +#[test] +fn test_emergency_pause_unpause() { + let contract_address = setup(); + let emergency = IEmergencyDispatcher { contract_address }; + + // Test pause + emergency.pause(); + assert!(emergency.is_paused(), "Contract should be paused"); + + // Test unpause + emergency.unpause(); + assert!(!emergency.is_paused(), "Contract should be unpaused"); + + stop_cheat_caller_address(contract_address); + stop_cheat_block_timestamp(contract_address); +} + +#[test] +fn test_emergency_pause_with_metadata() { + let contract_address = setup(); + let emergency = IEmergencyDispatcher { contract_address }; + + // First pause the contract + emergency.pause(); + + // Set pause metadata + let reason = 'emergency_maintenance'; + emergency.set_pause_meta(reason); + + assert!(emergency.get_pause_reason() == reason, "Pause reason should match"); + // The timestamp should be the cheated block timestamp from setup (1000) or a later value + assert!(emergency.get_pause_timestamp() > 0, "Pause timestamp should be set"); + + stop_cheat_caller_address(contract_address); + stop_cheat_block_timestamp(contract_address); +} + +#[test] +fn test_emergency_unpause_with_metadata_clear() { + let contract_address = setup(); + let emergency = IEmergencyDispatcher { contract_address }; + + // First pause the contract with metadata + emergency.pause(); + emergency.set_pause_meta('test_reason'); + + // Unpause and clear metadata + emergency.unpause_with_metadata_clear(); + + assert!(!emergency.is_paused(), "Contract should be unpaused"); + assert!(emergency.get_pause_reason() == 0, "Pause reason should be cleared"); + assert!(emergency.get_pause_timestamp() == 0, "Pause timestamp should be cleared"); + + stop_cheat_caller_address(contract_address); + stop_cheat_block_timestamp(contract_address); +} + +#[test] +fn test_emergency_ban_unban_member() { + let contract_address = setup(); + let emergency = IEmergencyDispatcher { contract_address }; + let member_address = contract_address_const::(); + + // Test ban member + emergency.set_ban(member_address, true); + assert!(emergency.is_banned(member_address), "Member should be banned"); + + // Test unban member + emergency.set_ban(member_address, false); + assert!(!emergency.is_banned(member_address), "Member should be unbanned"); + + stop_cheat_caller_address(contract_address); + stop_cheat_block_timestamp(contract_address); +} + +#[test] +fn test_emergency_config() { + let contract_address = setup(); + let emergency = IEmergencyDispatcher { contract_address }; + + let config = EmergencyConfig { + emergency_cooldown: 86400, + required_approvals: 3, + }; + + emergency.set_config(config); + let retrieved_config = emergency.get_config(); + + assert!(retrieved_config.emergency_cooldown == 86400, "Cooldown should match"); + assert!(retrieved_config.required_approvals == 3, "Required approvals should match"); + + stop_cheat_caller_address(contract_address); + stop_cheat_block_timestamp(contract_address); +} + +#[test] +fn test_emergency_assert_not_paused_success() { + let contract_address = setup(); + let emergency = IEmergencyDispatcher { contract_address }; + + // Should not panic when contract is not paused + emergency.assert_not_paused(); + + stop_cheat_caller_address(contract_address); + stop_cheat_block_timestamp(contract_address); +} + +#[test] +#[should_panic(expected: ('Emergency: contract is paused',))] +fn test_emergency_assert_not_paused_panic() { + let contract_address = setup(); + let emergency = IEmergencyDispatcher { contract_address }; + + // Pause the contract + emergency.pause(); + + // Should panic when contract is paused + emergency.assert_not_paused(); + + stop_cheat_caller_address(contract_address); + stop_cheat_block_timestamp(contract_address); +} + +#[test] +#[should_panic(expected: ('Emergency: not admin',))] +fn test_emergency_pause_unauthorized() { + let contract_address = setup(); + let emergency = IEmergencyDispatcher { contract_address }; + let non_admin_address = contract_address_const::(); + + start_cheat_caller_address(contract_address, non_admin_address); + emergency.pause(); // This should panic + + + stop_cheat_caller_address(contract_address); + stop_cheat_block_timestamp(contract_address); +} + +#[test] +#[should_panic(expected: ('Emergency: contract is paused',))] +fn test_emergency_double_pause() { + let contract_address = setup(); + let emergency = IEmergencyDispatcher { contract_address }; + + emergency.pause(); + emergency.pause(); // Should panic + + + stop_cheat_caller_address(contract_address); + stop_cheat_block_timestamp(contract_address); +} + +#[test] +#[should_panic(expected: ('Emergency: contract not paused',))] +fn test_emergency_set_pause_meta_when_not_paused() { + let contract_address = setup(); + let emergency = IEmergencyDispatcher { contract_address }; + + // Try to set pause metadata without pausing first + emergency.set_pause_meta('test_reason'); // Should panic + + + stop_cheat_caller_address(contract_address); + stop_cheat_block_timestamp(contract_address); +} + +#[test] +#[should_panic(expected: ('Emergency: contract not paused',))] +fn test_emergency_unpause_when_not_paused() { + let contract_address = setup(); + let emergency = IEmergencyDispatcher { contract_address }; + + emergency.unpause(); // Should panic as contract is not paused + + + stop_cheat_caller_address(contract_address); + stop_cheat_block_timestamp(contract_address); +} + +#[test] +fn test_emergency_component_integration() { + let contract_address = setup(); + let emergency = IEmergencyDispatcher { contract_address }; + + assert!(!emergency.is_paused(), "Should start unpaused"); + + // Pause with reason + emergency.pause(); + emergency.set_pause_meta('emergency_stop'); + assert!(emergency.is_paused(), "Should be paused"); + assert!(emergency.get_pause_reason() == 'emergency_stop', "Reason should match"); + + emergency.unpause_with_metadata_clear(); + assert!(!emergency.is_paused(), "Should be unpaused"); + assert!(emergency.get_pause_reason() == 0, "Reason should be cleared"); + + + stop_cheat_caller_address(contract_address); + stop_cheat_block_timestamp(contract_address); +} \ No newline at end of file diff --git a/tests/test_integration.cairo b/tests/test_integration.cairo new file mode 100644 index 0000000..439c99e --- /dev/null +++ b/tests/test_integration.cairo @@ -0,0 +1,226 @@ +// use starknet::ContractAddress; +// use starknet::testing::{set_caller_address, set_block_timestamp}; +// use starknet::contract_address_const; +// use starkremit_contract::starkremit::StarkRemit; +// use starkremit_contract::interfaces::IStarkRemit::{IStarkRemitDispatcher, IStarkRemitDispatcherTrait}; +// use starkremit_contract::base::types::{UserProfile, KYCLevel, RegistrationRequest}; + +// const ADMIN: felt252 = 0x123; +// const MEMBER1: felt252 = 0x789; +// const MEMBER2: felt252 = 0xABC; + +// fn setup() -> ContractAddress { +// let admin_address = contract_address_const::(); +// let contract_address = contract_address_const::<0x1>(); + +// set_caller_address(admin_address); +// set_block_timestamp(1000); + +// contract_address +// } + +// #[test] +// fn test_emergency_and_penalty_integration() { +// let contract_address = setup(); +// let stark_remit = IStarkRemitDispatcher { contract_address }; +// let member_address = contract_address_const::(); + +// // Test emergency pause +// stark_remit.emergency_pause_contract('SECURITY_BREACH'); + +// // Test member ban through emergency functions +// stark_remit.ban_member(member_address); + +// // Test penalty application (should work with emergency system) +// stark_remit.apply_late_fee(member_address, 1_u256); +// } + +// #[test] +// fn test_penalty_and_analytics_integration() { +// let contract_address = setup(); +// let stark_remit = IStarkRemitDispatcher { contract_address }; +// let member_address = contract_address_const::(); + +// // Apply penalty +// stark_remit.add_strike(member_address, 1_u256); + +// // Check that analytics are updated (if integrated properly) +// // This tests that penalty actions trigger analytics updates +// } + +// #[test] +// fn test_member_lifecycle_with_all_components() { +// let contract_address = setup(); +// let stark_remit = IStarkRemitDispatcher { contract_address }; +// let member_address = contract_address_const::(); + +// // Register member (user management) +// let registration_data = RegistrationRequest { +// email_hash: 'test@email.com', +// phone_hash: '1234567890', +// full_name: 'Test User', +// country_code: 'US', +// }; + +// set_caller_address(member_address); +// stark_remit.register_user(registration_data); + +// // Check if member is registered +// assert!(stark_remit.is_user_registered(member_address), "Member should be registered"); + +// // Apply penalties as admin +// set_caller_address(contract_address_const::()); +// stark_remit.add_strike(member_address, 1_u256); + +// // Test emergency functions +// stark_remit.emergency_reset_member_strikes(member_address); +// } + +// #[test] +// fn test_automated_scheduling_with_penalties() { +// let contract_address = setup(); +// let stark_remit = IStarkRemitDispatcher { contract_address }; +// let member_address = contract_address_const::(); + +// // Setup automated scheduling would be integrated here +// // Apply penalties that could affect scheduling +// stark_remit.add_strike(member_address, 1_u256); +// stark_remit.add_strike(member_address, 2_u256); + +// // Member should be affected in future scheduling decisions +// // (Implementation specific - depends on integration) +// } + +// #[test] +// fn test_payment_flexibility_with_analytics() { +// let contract_address = setup(); +// let stark_remit = IStarkRemitDispatcher { contract_address }; +// let member_address = contract_address_const::(); + +// // Test that payment flexibility features integrate with analytics +// // This would involve setting up auto-payments, early payments, etc. +// // and verifying that analytics track these properly +// } + +// #[test] +// fn test_comprehensive_emergency_scenario() { +// let contract_address = setup(); +// let stark_remit = IStarkRemitDispatcher { contract_address }; +// let member1_address = contract_address_const::(); +// let member2_address = contract_address_const::(); + +// // Simulate emergency scenario +// stark_remit.emergency_pause_contract('SYSTEM_COMPROMISE'); + +// // Emergency actions should still work +// stark_remit.ban_member(member1_address); +// stark_remit.emergency_withdraw_member(member2_address); + +// // Resume operations +// stark_remit.emergency_unpause_contract(); + +// // Verify member states +// stark_remit.unban_member(member1_address); +// } + +// #[test] +// fn test_system_health_after_penalties() { +// let contract_address = setup(); +// let stark_remit = IStarkRemitDispatcher { contract_address }; +// let member_address = contract_address_const::(); + +// // Apply multiple penalties +// stark_remit.add_strike(member_address, 1_u256); +// stark_remit.apply_late_fee(member_address, 2_u256); +// stark_remit.add_strike(member_address, 3_u256); + +// // System health should be affected but still calculable +// // (This depends on the integration between analytics and penalty systems) +// } + +// #[test] +// fn test_round_completion_with_all_systems() { +// let contract_address = setup(); +// let stark_remit = IStarkRemitDispatcher { contract_address }; + +// // Create group and round +// stark_remit.create_group(5); + +// // Test that when a round completes, all systems are updated: +// // - Analytics track the completion +// // - Penalties are applied to late members +// // - Next round is automatically scheduled +// // - Member profiles are updated + +// // This integration test verifies cross-component communication +// } + +// #[test] +// fn test_emergency_fund_recovery_integration() { +// let contract_address = setup(); +// let stark_remit = IStarkRemitDispatcher { contract_address }; +// let token_address = contract_address_const::<0x999>(); + +// // Test emergency token recovery +// stark_remit.emergency_recover_tokens(token_address, 1000_u256); + +// // Test emergency fund migration +// let new_contract = contract_address_const::<0x888>(); +// stark_remit.emergency_migrate_funds(new_contract); + +// // Verify that analytics are updated to reflect emergency actions +// } + +// #[test] +// fn test_multi_member_penalty_scenario() { +// let contract_address = setup(); +// let stark_remit = IStarkRemitDispatcher { contract_address }; +// let member1_address = contract_address_const::(); +// let member2_address = contract_address_const::(); + +// // Apply different penalties to different members +// stark_remit.add_strike(member1_address, 1_u256); +// stark_remit.apply_late_fee(member2_address, 1_u256); +// stark_remit.add_strike(member1_address, 2_u256); +// stark_remit.add_strike(member1_address, 3_u256); // Should trigger ban + +// // Test that analytics properly track different member states +// // Test that emergency functions can handle banned members +// stark_remit.emergency_reset_member_strikes(member1_address); +// } + +// #[test] +// fn test_cross_component_state_consistency() { +// let contract_address = setup(); +// let stark_remit = IStarkRemitDispatcher { contract_address }; +// let member_address = contract_address_const::(); + +// // Perform actions across multiple components +// stark_remit.add_strike(member_address, 1_u256); // Penalty system +// stark_remit.ban_member(member_address); // Emergency system + +// // Verify that all systems reflect consistent state +// // Member should be banned in all relevant systems + +// // Reset and verify consistency +// stark_remit.unban_member(member_address); // Should reset penalties too +// stark_remit.emergency_reset_member_strikes(member_address); // Double-check cleanup +// } + +// #[test] +// fn test_automated_system_maintenance() { +// let contract_address = setup(); +// let stark_remit = IStarkRemitDispatcher { contract_address }; + +// // Test that automated functions work together: +// // - Schedule maintenance creates future rounds +// // - Auto-completion processes expired rounds +// // - Penalties are applied to late members +// // - Analytics track all activities + +// // Move time forward to trigger automated actions +// set_block_timestamp(10000); + +// // Trigger various automated processes +// // (Implementation depends on how components are integrated) +// } \ No newline at end of file diff --git a/tests/test_penalty_component.cairo b/tests/test_penalty_component.cairo new file mode 100644 index 0000000..f2936d6 --- /dev/null +++ b/tests/test_penalty_component.cairo @@ -0,0 +1,192 @@ +// use starknet::ContractAddress; +// use starknet::testing::{set_caller_address, set_block_timestamp}; +// use starknet::contract_address_const; +// use starkremit_contract::component::penalty::{ +// penalty_component, IPenaltyDispatcher, IPenaltyDispatcherTrait, +// PenaltyConfig, MemberPenaltyRecord +// }; + +// const ADMIN: felt252 = 0x123; +// const NON_ADMIN: felt252 = 0x456; +// const MEMBER1: felt252 = 0x789; +// const MEMBER2: felt252 = 0xABC; + +// fn setup() -> ContractAddress { +// let admin_address = contract_address_const::(); +// let contract_address = contract_address_const::<0x1>(); + +// set_caller_address(admin_address); +// set_block_timestamp(1000); + +// contract_address +// } + +// fn get_default_penalty_config() -> PenaltyConfig { +// PenaltyConfig { +// late_fee_percentage: 300, // 3% in basis points +// grace_period_hours: 24, +// max_strikes: 3, +// security_deposit_multiplier: 2, +// penalty_pool_enabled: true, +// } +// } + +// #[test] +// fn test_penalty_config() { +// let contract_address = setup(); +// let penalty = IPenaltyDispatcher { contract_address }; + +// let config = get_default_penalty_config(); + +// // Get initial config (should have default values) +// let retrieved_config = penalty.get_penalty_config(); +// assert!(retrieved_config.late_fee_percentage >= 0, "Initial config should be valid"); +// } + +// #[test] +// fn test_apply_late_fee() { +// let contract_address = setup(); +// let penalty = IPenaltyDispatcher { contract_address }; +// let member_address = contract_address_const::(); +// let round_id = 1_u256; + +// // Apply late fee +// penalty.apply_late_fee(member_address, round_id); + +// // Check member penalty record +// let record = penalty.get_member_penalty_record(member_address); +// assert!(record.total_penalties_paid > 0, "Penalty should be applied"); +// } + +// #[test] +// fn test_strike_system() { +// let contract_address = setup(); +// let penalty = IPenaltyDispatcher { contract_address }; +// let member_address = contract_address_const::(); +// let round_id = 1_u256; + +// // Add first strike +// penalty.add_strike(member_address, round_id); +// let record = penalty.get_member_penalty_record(member_address); +// assert!(record.strikes == 1, "Member should have 1 strike"); +// assert!(!record.is_banned, "Member should not be banned yet"); + +// // Add second strike +// penalty.add_strike(member_address, round_id + 1); +// let record = penalty.get_member_penalty_record(member_address); +// assert!(record.strikes == 2, "Member should have 2 strikes"); +// assert!(!record.is_banned, "Member should not be banned yet"); + +// // Add third strike (should result in ban if max_strikes is 3) +// penalty.add_strike(member_address, round_id + 2); +// let record = penalty.get_member_penalty_record(member_address); +// assert!(record.strikes == 3, "Member should have 3 strikes"); + +// // Remove a strike +// penalty.remove_strike(member_address); +// let record = penalty.get_member_penalty_record(member_address); +// assert!(record.strikes == 2, "Member should have 2 strikes after removal"); +// } + +// #[test] +// fn test_ban_unban_member() { +// let contract_address = setup(); +// let penalty = IPenaltyDispatcher { contract_address }; +// let member_address = contract_address_const::(); + +// // Ban member +// penalty.ban_member(member_address); +// let record = penalty.get_member_penalty_record(member_address); +// assert!(record.is_banned, "Member should be banned"); + +// // Unban member +// penalty.unban_member(member_address); +// let record = penalty.get_member_penalty_record(member_address); +// assert!(!record.is_banned, "Member should be unbanned"); +// assert!(record.strikes == 0, "Strikes should be reset on unban"); +// } + +// #[test] +// #[should_panic(expected: ('NOT_ADMIN',))] +// fn test_apply_late_fee_unauthorized() { +// let contract_address = setup(); +// let penalty = IPenaltyDispatcher { contract_address }; +// let non_admin_address = contract_address_const::(); +// let member_address = contract_address_const::(); +// let round_id = 1_u256; + +// set_caller_address(non_admin_address); +// penalty.apply_late_fee(member_address, round_id); +// } + +// #[test] +// #[should_panic(expected: ('ALREADY_BANNED',))] +// fn test_ban_already_banned_member() { +// let contract_address = setup(); +// let penalty = IPenaltyDispatcher { contract_address }; +// let member_address = contract_address_const::(); + +// penalty.ban_member(member_address); +// penalty.ban_member(member_address); // Should panic +// } + +// #[test] +// #[should_panic(expected: ('NOT_BANNED',))] +// fn test_unban_not_banned_member() { +// let contract_address = setup(); +// let penalty = IPenaltyDispatcher { contract_address }; +// let member_address = contract_address_const::(); + +// penalty.unban_member(member_address); // Should panic as member is not banned +// } + +// #[test] +// fn test_member_penalty_record_initialization() { +// let contract_address = setup(); +// let penalty = IPenaltyDispatcher { contract_address }; +// let member_address = contract_address_const::(); + +// let record = penalty.get_member_penalty_record(member_address); +// assert!(record.strikes == 0, "New member should have 0 strikes"); +// assert!(record.total_penalties_paid == 0, "New member should have paid 0 penalties"); +// assert!(!record.is_banned, "New member should not be banned"); +// assert!(record.credit_score == 0, "New member should have initial credit score"); +// } + +// #[test] +// fn test_multiple_members_penalty_tracking() { +// let contract_address = setup(); +// let penalty = IPenaltyDispatcher { contract_address }; +// let member1_address = contract_address_const::(); +// let member2_address = contract_address_const::(); +// let round_id = 1_u256; + +// // Apply penalties to both members +// penalty.add_strike(member1_address, round_id); +// penalty.apply_late_fee(member2_address, round_id); + +// // Check that penalties are tracked separately +// let record1 = penalty.get_member_penalty_record(member1_address); +// let record2 = penalty.get_member_penalty_record(member2_address); + +// assert!(record1.strikes == 1, "Member 1 should have 1 strike"); +// assert!(record2.strikes == 0, "Member 2 should have 0 strikes"); +// assert!(record1.total_penalties_paid == 0, "Member 1 should have no late fees"); +// assert!(record2.total_penalties_paid > 0, "Member 2 should have late fees"); +// } + +// #[test] +// fn test_credit_score_updates() { +// let contract_address = setup(); +// let penalty = IPenaltyDispatcher { contract_address }; +// let member_address = contract_address_const::(); +// let round_id = 1_u256; + +// // Apply penalty and check credit score change +// let initial_record = penalty.get_member_penalty_record(member_address); +// penalty.apply_late_fee(member_address, round_id); +// let updated_record = penalty.get_member_penalty_record(member_address); + +// // Credit score should be updated (implementation may vary) +// assert!(updated_record.last_penalty_date > initial_record.last_penalty_date, "Last penalty date should be updated"); +// } \ No newline at end of file From fd7def794d333ad3e0605d1c34874d30131f555b Mon Sep 17 00:00:00 2001 From: wheval Date: Tue, 2 Sep 2025 11:06:05 +0100 Subject: [PATCH 2/2] feat: integrate payment_flexibility and member_profile component --- src/base/errors.cairo | 33 +- src/base/events.cairo | 162 +- src/base/types.cairo | 200 +- src/component/analytics.cairo | 899 ++++--- src/component/auto_schedule.cairo | 239 +- src/component/contribution/contribution.cairo | 330 ++- src/component/emergency.cairo | 35 +- src/component/member_profile.cairo | 777 +++---- src/component/payment_flexibility.cairo | 385 +-- src/component/penalty.cairo | 298 +-- src/interfaces/IStarkRemit.cairo | 27 +- src/lib.cairo | 4 +- src/starkremit/StarkRemit.cairo | 2062 ++++++----------- tests/test_analytics_component.cairo | 388 ++-- tests/test_auto_schedule_component.cairo | 72 +- tests/test_emergency_component.cairo | 112 +- tests/test_integration.cairo | 75 +- tests/test_penalty_component.cairo | 47 +- 18 files changed, 2986 insertions(+), 3159 deletions(-) diff --git a/src/base/errors.cairo b/src/base/errors.cairo index fca9f1e..f938a22 100644 --- a/src/base/errors.cairo +++ b/src/base/errors.cairo @@ -391,7 +391,6 @@ pub mod AutoScheduleErrors { pub const NEW_DEADLINE_NOT_IN_FUTURE: felt252 = 'AutoSchedule: not future'; /// Error triggered when new deadline is not after start pub const NEW_DEADLINE_NOT_AFTER_START: felt252 = 'AutoSchedule: not after start'; - } pub mod ContributionErrors { @@ -446,4 +445,34 @@ pub mod PaymentFlexibilityErrors { pub const AUTO_PAYMENT_ACTIVE: felt252 = 'Payment: already active'; /// Error triggered when frequency is invalid pub const INVALID_FREQUENCY: felt252 = 'Payment: invalid frequency'; -} \ No newline at end of file +} + +pub mod AnalyticsComponentErrors { + /// Error triggered when caller is not an Admin or higher + pub const NOT_ADMIN: felt252 = 'Analytics: not admin'; + /// Error triggered when analytics is disabled + pub const ANALYTICS_DISABLED: felt252 = 'Analytics: disabled'; + /// Error triggered when member not found + pub const MEMBER_NOT_FOUND: felt252 = 'Analytics: member not found'; + /// Error triggered when round not found + pub const ROUND_NOT_FOUND: felt252 = 'Analytics: round not found'; + /// Error triggered when period is invalid + pub const INVALID_PERIOD: felt252 = 'Analytics: invalid period'; + /// Error triggered when insufficient data + pub const INSUFFICIENT_DATA: felt252 = 'Analytics: insufficient data'; +} + +pub mod MemberProfileComponentErrors { + /// Error triggered when caller is not an Admin or higher + pub const NOT_ADMIN: felt252 = 'MemberProfile: not admin'; + /// Error triggered when profile not found + pub const PROFILE_NOT_FOUND: felt252 = 'MemberProfile: invalid profile'; + /// Error triggered when profile already exists + pub const PROFILE_ALREADY_EXISTS: felt252 = 'MemberProfile: profile exists'; + /// Error triggered when rating is invalid + pub const INVALID_RATING: felt252 = 'MemberProfile: invalid rating'; + /// Error triggered when preferences are invalid + pub const INVALID_PREFERENCES: felt252 = 'MemberProfile: invalid pref'; + /// Error triggered when member not on waitlist + pub const MEMBER_NOT_ON_WAITLIST: felt252 = 'MemberProfile: not on waitlist'; +} diff --git a/src/base/events.cairo b/src/base/events.cairo index cd3e899..ece1255 100644 --- a/src/base/events.cairo +++ b/src/base/events.cairo @@ -410,84 +410,84 @@ pub struct UpdateCancelled { } #[derive(Drop, starknet::Event)] - pub struct EmergencyWithdrawalAll { - pub total_amount: u256, - pub member_count: u32, - pub executed_by: ContractAddress, - pub timestamp: u64, - } - - #[derive(Drop, starknet::Event)] - pub struct EmergencyWithdrawalMember { - pub member: ContractAddress, - pub amount: u256, - pub executed_by: ContractAddress, - pub timestamp: u64, - } - - #[derive(Drop, starknet::Event)] - pub struct RoundEmergencyCompleted { - pub round_id: u256, - pub recipient: ContractAddress, - pub amount: u256, - pub completed_by: ContractAddress, - pub timestamp: u64, - } - - #[derive(Drop, starknet::Event)] - pub struct RoundEmergencyCancelled { - pub round_id: u256, - pub cancelled_by: ContractAddress, - pub reason: felt252, - pub timestamp: u64, - } - - #[derive(Drop, starknet::Event)] - pub struct RecipientChanged { - pub round_id: u256, - pub old_recipient: ContractAddress, - pub new_recipient: ContractAddress, - pub changed_by: ContractAddress, - pub timestamp: u64, - } - - #[derive(Drop, starknet::Event)] - pub struct TokensRecovered { - pub token: ContractAddress, - pub amount: u256, - pub recovered_by: ContractAddress, - pub recipient: ContractAddress, - pub timestamp: u64, - } - - #[derive(Drop, starknet::Event)] - pub struct FundsMigrated { - pub new_contract: ContractAddress, - pub amount: u256, - pub migrated_by: ContractAddress, - pub timestamp: u64, - } - - #[derive(Drop, starknet::Event)] - pub struct PenaltyPoolDistributed { - pub total_amount: u256, - pub recipient_count: u32, - pub distribution_type: felt252, - pub timestamp: u64, - } - - #[derive(Drop, starknet::Event)] - pub struct MemberUnbanned { - pub member: ContractAddress, - pub unbanned_by: ContractAddress, - pub timestamp: u64, - } - - #[derive(Drop, starknet::Event)] - pub struct MemberBanned { - pub member: ContractAddress, - pub reason: felt252, - pub strikes: u32, - pub banned_by: ContractAddress, - pub timestamp: u64, - } \ No newline at end of file +pub struct EmergencyWithdrawalAll { + pub total_amount: u256, + pub member_count: u32, + pub executed_by: ContractAddress, + pub timestamp: u64, +} + +#[derive(Drop, starknet::Event)] +pub struct EmergencyWithdrawalMember { + pub member: ContractAddress, + pub amount: u256, + pub executed_by: ContractAddress, + pub timestamp: u64, +} + +#[derive(Drop, starknet::Event)] +pub struct RoundEmergencyCompleted { + pub round_id: u256, + pub recipient: ContractAddress, + pub amount: u256, + pub completed_by: ContractAddress, + pub timestamp: u64, +} + +#[derive(Drop, starknet::Event)] +pub struct RoundEmergencyCancelled { + pub round_id: u256, + pub cancelled_by: ContractAddress, + pub reason: felt252, + pub timestamp: u64, +} + +#[derive(Drop, starknet::Event)] +pub struct RecipientChanged { + pub round_id: u256, + pub old_recipient: ContractAddress, + pub new_recipient: ContractAddress, + pub changed_by: ContractAddress, + pub timestamp: u64, +} + +#[derive(Drop, starknet::Event)] +pub struct TokensRecovered { + pub token: ContractAddress, + pub amount: u256, + pub recovered_by: ContractAddress, + pub recipient: ContractAddress, + pub timestamp: u64, +} + +#[derive(Drop, starknet::Event)] +pub struct FundsMigrated { + pub new_contract: ContractAddress, + pub amount: u256, + pub migrated_by: ContractAddress, + pub timestamp: u64, +} + +#[derive(Drop, starknet::Event)] +pub struct PenaltyPoolDistributed { + pub total_amount: u256, + pub recipient_count: u32, + pub distribution_type: felt252, + pub timestamp: u64, +} + +#[derive(Drop, starknet::Event)] +pub struct MemberUnbanned { + pub member: ContractAddress, + pub unbanned_by: ContractAddress, + pub timestamp: u64, +} + +#[derive(Drop, starknet::Event)] +pub struct MemberBanned { + pub member: ContractAddress, + pub reason: felt252, + pub strikes: u32, + pub banned_by: ContractAddress, + pub timestamp: u64, +} diff --git a/src/base/types.cairo b/src/base/types.cairo index 7c7a568..02c0f08 100644 --- a/src/base/types.cairo +++ b/src/base/types.cairo @@ -1,6 +1,6 @@ -use starknet::ContractAddress; +use core::array::{Array, ArrayTrait}; use core::serde::Serde; -use core::array::{ArrayTrait, Array}; +use starknet::ContractAddress; /// User profile structure containing user information @@ -217,6 +217,8 @@ pub struct ContributionRound { pub recipient: ContractAddress, /// Deadline for contributions pub deadline: u64, + /// Timestamp when the round was actually completed (0 if not completed) + pub completed_at: u64, /// Total contributions collected pub total_contributions: u256, /// Current status of the round @@ -326,22 +328,22 @@ pub struct ParameterHistory { // Penalty configuration structure #[derive(Copy, Drop, Serde, starknet::Store)] pub struct PenaltyConfig { - pub late_fee_percentage: u256, // Late fee as basis points (e.g., 250 = 2.5%) - pub grace_period_hours: u64, // Grace period before late fees apply - pub max_strikes: u32, // Maximum strikes before automatic ban + pub late_fee_percentage: u256, // Late fee as basis points (e.g., 250 = 2.5%) + pub grace_period_hours: u64, // Grace period before late fees apply + pub max_strikes: u32, // Maximum strikes before automatic ban pub security_deposit_multiplier: u256, // Security deposit amount in tokens - pub penalty_pool_enabled: bool, // Whether penalty pool distribution is enabled + pub penalty_pool_enabled: bool // Whether penalty pool distribution is enabled } // Member penalty record structure #[derive(Copy, Drop, Serde, starknet::Store)] pub struct MemberPenaltyRecord { - pub total_penalties_paid: u256, // Total penalties paid by member - pub strikes: u32, // Current strike count - pub is_banned: bool, // Whether member is currently banned - pub last_penalty_date: u64, // Timestamp of last penalty - pub last_strike_date: u64, // Timestamp of last strike - pub total_rounds_missed: u32, // Total rounds where contribution was missed + pub total_penalties_paid: u256, // Total penalties paid by member + pub strikes: u32, // Current strike count + pub is_banned: bool, // Whether member is currently banned + pub last_penalty_date: u64, // Timestamp of last penalty + pub last_strike_date: u64, // Timestamp of last strike + pub total_rounds_missed: u32 // Total rounds where contribution was missed } #[derive(Copy, Drop, Serde, starknet::Store)] @@ -366,29 +368,29 @@ pub struct ScheduledRound { // Penalty event structure for history tracking #[derive(Copy, Drop, Serde, starknet::Store)] pub struct PenaltyEventRecord { - pub member: ContractAddress, // Member who received penalty - pub round_id: u256, // Round where penalty occurred - pub event_type: PenaltyEventType, // Type of penalty event - pub amount: u256, // Penalty amount - pub timestamp: u64, // When penalty occurred - pub admin: ContractAddress, // Admin who applied penalty + pub member: ContractAddress, // Member who received penalty + pub round_id: u256, // Round where penalty occurred + pub event_type: PenaltyEventType, // Type of penalty event + pub amount: u256, // Penalty amount + pub timestamp: u64, // When penalty occurred + pub admin: ContractAddress // Admin who applied penalty } // Distribution data structure for penalty pool distribution // Note: This struct is not stored, only used for calculations #[derive(Clone, Drop, Serde)] pub struct DistributionData { - pub total_amount: u256, // Total penalty pool amount + pub total_amount: u256, // Total penalty pool amount pub member_shares: Array, // Array of member shares - pub total_compliant_contributions: u256, // Total contributions from compliant members + pub total_compliant_contributions: u256 // Total contributions from compliant members } // Individual member share structure #[derive(Copy, Drop, Serde, starknet::Store)] pub struct MemberShare { - pub member: ContractAddress, // Member address - pub share: u256, // Share amount to receive - pub contribution: u256, // Member's total contribution + pub member: ContractAddress, // Member address + pub share: u256, // Share amount to receive + pub contribution: u256 // Member's total contribution } // Penalty event types @@ -405,6 +407,156 @@ pub enum PenaltyEventType { #[derive(Copy, Drop, starknet::Store)] pub struct RoundData { pub deadline: u64, + pub completed_at: u64, pub status: RoundStatus, pub total_contributions: u256, -} \ No newline at end of file +} + +// Data structures for payment flexibility functionality +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct PaymentConfig { + pub grace_period_hours: u64, + pub early_payment_discount_basis_points: u256, // E.g., 500 for 5% + pub auto_payment_enabled: bool, + pub usd_oracle_address: ContractAddress, + pub max_grace_period_extension: u64, // Maximum extension in hours + pub min_early_payment_days: u64 // Minimum days before deadline for early payment +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +pub enum PaymentFrequency { + Once, + Daily, + Weekly, + Monthly, +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct AutoPaymentSetup { + pub member: ContractAddress, + pub token: ContractAddress, + pub amount: u256, + pub frequency: PaymentFrequency, + pub next_payment_date: u64, + pub is_active: bool, + pub created_at: u64, + pub last_payment_date: u64, +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +pub enum PaymentStatus { + Pending, + Paid, + Late, + Missed, + PaidAfterGrace, + Overpaid, + Early, +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct PaymentRecord { + pub member: ContractAddress, + pub round_id: u256, + pub amount: u256, + pub token: ContractAddress, + pub payment_date: u64, + pub status: PaymentStatus, + pub is_early_payment: bool, + pub discount_applied: u256, + pub grace_period_used: u64, +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct EarlyPaymentInfo { + pub member: ContractAddress, + pub round_id: u256, + pub original_amount: u256, + pub discount_amount: u256, + pub final_amount: u256, + pub payment_date: u64, +} + +// Data structures for analytics functionality +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct ContributionAnalytics { + pub total_rounds: u256, + pub successful_rounds: u256, + pub failed_rounds: u256, + pub average_completion_time: u64, + pub total_penalties_collected: u256, + pub total_contributions: u256, + pub last_updated: u64, +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct MemberAnalytics { + pub total_contributions: u256, + pub on_time_payments: u256, + pub late_payments: u256, + pub missed_payments: u256, + pub reliability_score: u8, + pub last_updated: u64, + pub average_contribution_amount: u256, + pub total_rounds_participated: u256, +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct RoundPerformanceMetrics { + pub round_id: u256, + pub completion_rate: u8, + pub average_delay: u64, + pub total_fees_collected: u256, + pub success_status: RoundSuccessStatus, + pub total_contributions: u256, + pub participant_count: u32, + pub completion_time: u64, +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +pub enum RoundSuccessStatus { + Outstanding, + Good, + Average, + Poor, + Failed, +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct FinancialReport { + pub period_start: u64, + pub period_end: u64, + pub total_contributions: u256, + pub total_fees_collected: u256, + pub total_penalties_collected: u256, + pub active_members: u32, + pub rounds_completed: u32, + pub average_round_completion_time: u64, + pub system_uptime_percentage: u8, +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct SystemHealthMetrics { + pub system_uptime_percentage: u8, + pub active_rounds: u32, + pub total_locked_value: u256, + pub security_score: u8, + pub member_satisfaction_score: u8, + pub last_health_check: u64, +} + +/// Member profile structure for savings group members +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct MemberProfileData { + pub join_date: u64, + pub total_contributions: u256, + pub missed_contributions: u8, + pub credit_score: u8, + pub last_recipient_round: u256, + pub reliability_rating: u8, + pub preferred_payment_method: felt252, + pub communication_preferences: felt252, + pub is_on_waitlist: bool, + pub waitlist_position: u32, + pub last_message_timestamp: u64, +} diff --git a/src/component/analytics.cairo b/src/component/analytics.cairo index 4e50edd..f2cddec 100644 --- a/src/component/analytics.cairo +++ b/src/component/analytics.cairo @@ -1,367 +1,532 @@ -// use starknet::ContractAddress; - -// #[starknet::interface] -// pub trait IAnalytics { -// fn generate_contribution_report(self: @TContractState) -> ContributionAnalytics; -// fn get_member_performance(self: @TContractState, member: ContractAddress) -> MemberAnalytics; -// fn calculate_system_health(self: @TContractState) -> u8; -// } - -// // Data structures for analytics functionality -// #[derive(Copy, Drop, Serde, starknet::Store)] -// pub struct ContributionAnalytics { -// pub total_rounds: u256, -// pub successful_rounds: u256, -// pub failed_rounds: u256, -// pub average_completion_time: u64, -// pub total_penalties_collected: u256, -// pub member_reliability_distribution: Array, -// } - -// #[derive(Copy, Drop, Serde, starknet::Store)] -// pub struct MemberAnalytics { -// pub total_contributions: u256, -// pub on_time_payments: u256, -// pub late_payments: u256, -// pub missed_payments: u256, -// pub reliability_score: u8, -// pub last_updated: u64, -// } - -// #[derive(Copy, Drop, Serde, starknet::Store)] -// pub struct RoundPerformanceMetrics { -// pub round_id: u256, -// pub completion_rate: u8, -// pub average_delay: u64, -// pub total_fees_collected: u256, -// pub success_status: RoundSuccessStatus, -// } - -// #[derive(Copy, Drop, Serde, starknet::Store)] -// pub enum RoundSuccessStatus { -// Outstanding, -// Good, -// Average, -// Poor, -// Failed, -// } - -// #[derive(Copy, Drop, Serde, starknet::Store)] -// pub struct FinancialReport { -// pub period_start: u64, -// pub period_end: u64, -// pub total_contributions: u256, -// pub total_fees_collected: u256, -// pub total_penalties_collected: u256, -// pub active_members: u32, -// pub rounds_completed: u32, -// } - -// #[derive(Copy, Drop, Serde, starknet::Store)] -// pub struct SystemHealthMetrics { -// pub system_uptime_percentage: u8, -// pub active_rounds: u32, -// pub total_locked_value: u256, -// pub security_score: u8, -// } - -// #[generate_trait] -// pub impl IAnalyticsInternal of IAnalyticsInternalTrait { -// fn initializer(ref self: ComponentState); -// fn _update_member_analytics(ref self: ComponentState, member: ContractAddress, payment_made: bool); -// fn _calculate_system_health(self: @ComponentState) -> u8; -// } - -// #[starknet::component] -// pub mod analytics_component { -// use core::starknet::{ContractAddress, get_block_timestamp, get_caller_address}; -// use core::starknet::storage::{ -// Map, StoragePointerReadAccess, StoragePointerWriteAccess, -// }; -// use super::{ContributionAnalytics, MemberAnalytics, RoundPerformanceMetrics, FinancialReport, SystemHealthMetrics, RoundSuccessStatus}; - -// #[derive(Drop)] -// pub enum Errors { -// NOT_ADMIN: (), -// INVALID_PERIOD: (), -// MEMBER_NOT_FOUND: (), -// INSUFFICIENT_DATA: (), -// } - -// #[storage] -// pub struct Storage { -// member_analytics: Map, -// contribution_analytics: ContributionAnalytics, -// round_metrics: Map, -// financial_reports: Map, // timestamp -> report -// system_metrics: SystemHealthMetrics, -// admin: ContractAddress, -// last_update_timestamp: u64, -// total_system_value: u256, -// } - -// #[event] -// #[derive(Drop, starknet::Event)] -// pub enum Event { -// AnalyticsUpdated: AnalyticsUpdated, -// ReportGenerated: ReportGenerated, -// SystemHealthUpdated: SystemHealthUpdated, -// MemberPerformanceUpdated: MemberPerformanceUpdated, -// } - -// #[derive(Drop, starknet::Event)] -// pub struct AnalyticsUpdated { -// member: ContractAddress, -// new_score: u8, -// timestamp: u64, -// } - -// #[derive(Drop, starknet::Event)] -// pub struct ReportGenerated { -// report_type: felt252, -// timestamp: u64, -// } - -// #[derive(Drop, starknet::Event)] -// pub struct SystemHealthUpdated { -// health_score: u8, -// timestamp: u64, -// } - -// #[derive(Drop, starknet::Event)] -// pub struct MemberPerformanceUpdated { -// member: ContractAddress, -// total_contributions: u256, -// reliability_score: u8, -// timestamp: u64, -// } - -// impl AnalyticsImpl< -// TContractState, +HasComponent, -// > of super::IAnalytics> { -// fn generate_contribution_report(self: @ComponentState) -> ContributionAnalytics { -// let analytics = self.contribution_analytics.read(); -// self.emit(Event::ReportGenerated(ReportGenerated { -// report_type: 'CONTRIBUTION_REPORT', -// timestamp: get_block_timestamp(), -// })); -// analytics -// } - -// fn get_member_performance(self: @ComponentState, member: ContractAddress) -> MemberAnalytics { -// let analytics = self.member_analytics.read(member); -// if analytics.last_updated == 0 { -// // Return default analytics for new member -// MemberAnalytics { -// total_contributions: 0, -// on_time_payments: 0, -// late_payments: 0, -// missed_payments: 0, -// reliability_score: 50, -// last_updated: get_block_timestamp(), -// } -// } else { -// analytics -// } -// } - -// fn calculate_system_health(self: @ComponentState) -> u8 { -// let health_score = self._calculate_system_health(); -// self.emit(Event::SystemHealthUpdated(SystemHealthUpdated { -// health_score, -// timestamp: get_block_timestamp(), -// })); -// health_score -// } -// } - -// // Additional analytics functionality -// impl AdditionalAnalyticsImpl< -// TContractState, +HasComponent, -// > of AdditionalAnalyticsTrait { -// fn update_member_performance(ref self: ComponentState, member: ContractAddress, amount: u256, on_time: bool) { -// let mut analytics = self.member_analytics.read(member); - -// analytics.total_contributions += amount; -// if on_time { -// analytics.on_time_payments += 1; -// } else { -// analytics.late_payments += 1; -// } - -// // Recalculate reliability score -// let total_payments = analytics.on_time_payments + analytics.late_payments + analytics.missed_payments; -// if total_payments > 0 { -// analytics.reliability_score = ((analytics.on_time_payments * 100) / total_payments).try_into().unwrap(); -// } - -// analytics.last_updated = get_block_timestamp(); -// self.member_analytics.write(member, analytics); - -// self.emit(Event::MemberPerformanceUpdated(MemberPerformanceUpdated { -// member, -// total_contributions: analytics.total_contributions, -// reliability_score: analytics.reliability_score, -// timestamp: get_block_timestamp(), -// })); -// } - -// fn record_missed_payment(ref self: ComponentState, member: ContractAddress) { -// let mut analytics = self.member_analytics.read(member); -// analytics.missed_payments += 1; - -// // Update reliability score -// let total_payments = analytics.on_time_payments + analytics.late_payments + analytics.missed_payments; -// if total_payments > 0 { -// analytics.reliability_score = ((analytics.on_time_payments * 100) / total_payments).try_into().unwrap(); -// } - -// analytics.last_updated = get_block_timestamp(); -// self.member_analytics.write(member, analytics); -// } - -// fn update_round_metrics(ref self: ComponentState, round_id: u256, completion_rate: u8, fees_collected: u256) { -// let success_status = if completion_rate >= 95 { -// RoundSuccessStatus::Outstanding -// } else if completion_rate >= 85 { -// RoundSuccessStatus::Good -// } else if completion_rate >= 70 { -// RoundSuccessStatus::Average -// } else if completion_rate >= 50 { -// RoundSuccessStatus::Poor -// } else { -// RoundSuccessStatus::Failed -// }; - -// let metrics = RoundPerformanceMetrics { -// round_id, -// completion_rate, -// average_delay: 0, // Would be calculated based on payment timestamps -// total_fees_collected: fees_collected, -// success_status, -// }; - -// self.round_metrics.write(round_id, metrics); -// } - -// fn generate_financial_report(ref self: ComponentState, period_start: u64, period_end: u64) -> FinancialReport { -// assert(period_start < period_end, Errors::INVALID_PERIOD); - -// let report = FinancialReport { -// period_start, -// period_end, -// total_contributions: self.total_system_value.read(), -// total_fees_collected: self.total_system_value.read() / 100, // 1% fee assumption -// total_penalties_collected: self.total_system_value.read() / 200, // 0.5% penalty assumption -// active_members: 50, // Placeholder -// rounds_completed: 20, // Placeholder -// }; - -// self.financial_reports.write(period_start, report); -// report -// } - -// fn update_system_metrics(ref self: ComponentState, total_value: u256, active_rounds: u32) { -// self.total_system_value.write(total_value); - -// let mut metrics = self.system_metrics.read(); -// metrics.total_locked_value = total_value; -// metrics.active_rounds = active_rounds; -// metrics.system_uptime_percentage = 99; // High uptime assumption -// metrics.security_score = 95; // High security score - -// self.system_metrics.write(metrics); -// self.last_update_timestamp.write(get_block_timestamp()); -// } - -// fn get_system_metrics(self: @ComponentState) -> SystemHealthMetrics { -// self.system_metrics.read() -// } - -// fn get_round_performance(self: @ComponentState, round_id: u256) -> RoundPerformanceMetrics { -// self.round_metrics.read(round_id) -// } - -// fn get_financial_report(self: @ComponentState, timestamp: u64) -> FinancialReport { -// self.financial_reports.read(timestamp) -// } -// } - -// #[generate_trait] -// pub trait AdditionalAnalyticsTrait { -// fn update_member_performance(ref self: ComponentState, member: ContractAddress, amount: u256, on_time: bool); -// fn record_missed_payment(ref self: ComponentState, member: ContractAddress); -// fn update_round_metrics(ref self: ComponentState, round_id: u256, completion_rate: u8, fees_collected: u256); -// fn generate_financial_report(ref self: ComponentState, period_start: u64, period_end: u64) -> FinancialReport; -// fn update_system_metrics(ref self: ComponentState, total_value: u256, active_rounds: u32); -// fn get_system_metrics(self: @ComponentState) -> SystemHealthMetrics; -// fn get_round_performance(self: @ComponentState, round_id: u256) -> RoundPerformanceMetrics; -// fn get_financial_report(self: @ComponentState, timestamp: u64) -> FinancialReport; -// } - -// #[generate_trait] -// pub impl InternalImpl< -// TContractState, +HasComponent, -// > of super::IAnalyticsInternal { -// fn initializer(ref self: ComponentState) { -// self.admin.write(get_caller_address()); -// self.total_system_value.write(0); -// self.last_update_timestamp.write(get_block_timestamp()); - -// // Initialize default analytics -// let default_analytics = ContributionAnalytics { -// total_rounds: 0, -// successful_rounds: 0, -// failed_rounds: 0, -// average_completion_time: 0, -// total_penalties_collected: 0, -// member_reliability_distribution: array![], -// }; -// self.contribution_analytics.write(default_analytics); -// } - -// fn _update_member_analytics(ref self: ComponentState, member: ContractAddress, payment_made: bool) { -// let mut analytics = self.member_analytics.read(member); - -// if payment_made { -// analytics.on_time_payments += 1; -// } else { -// analytics.missed_payments += 1; -// } - -// // Recalculate reliability score -// let total_attempts = analytics.on_time_payments + analytics.late_payments + analytics.missed_payments; -// if total_attempts > 0 { -// let successful = analytics.on_time_payments + analytics.late_payments; -// analytics.reliability_score = ((successful * 100) / total_attempts).try_into().unwrap(); -// } - -// analytics.last_updated = get_block_timestamp(); -// self.member_analytics.write(member, analytics); - -// self.emit(Event::AnalyticsUpdated(AnalyticsUpdated { -// member, -// new_score: analytics.reliability_score, -// timestamp: get_block_timestamp(), -// })); -// } - -// fn _calculate_system_health(self: @ComponentState) -> u8 { -// let metrics = self.system_metrics.read(); -// let analytics = self.contribution_analytics.read(); - -// // Simple health calculation based on multiple factors -// let uptime_score = metrics.system_uptime_percentage; -// let security_score = metrics.security_score; -// let success_rate = if analytics.total_rounds > 0 { -// ((analytics.successful_rounds * 100) / analytics.total_rounds).try_into().unwrap() -// } else { -// 100_u8 -// }; - -// // Weighted average -// let health = (uptime_score + security_score + success_rate) / 3; -// health -// } -// } -// } +use starknet::ContractAddress; +use starkremit_contract::base::types::{ + ContributionAnalytics, FinancialReport, MemberAnalytics, MemberContribution, RoundData, + RoundPerformanceMetrics, RoundStatus, RoundSuccessStatus, SystemHealthMetrics, +}; + +// Trait that the main contract must implement to provide data access +pub trait IMainContractData { + fn get_round_data(self: @TContractState, round_id: u256) -> RoundData; + fn get_member_contribution_data( + self: @TContractState, round_id: u256, member: ContractAddress, + ) -> MemberContribution; + fn get_member_status(self: @TContractState, member: ContractAddress) -> bool; + fn get_member_count(self: @TContractState) -> u32; + fn get_member_by_index(self: @TContractState, index: u32) -> ContractAddress; + fn get_round_ids(self: @TContractState) -> u256; +} + +#[starknet::interface] +pub trait IAnalytics { + // Configuration and query functions (simple operations) + fn get_contribution_analytics(self: @TContractState) -> ContributionAnalytics; + fn get_member_analytics(self: @TContractState, member: ContractAddress) -> MemberAnalytics; + fn get_round_performance(self: @TContractState, round_id: u256) -> RoundPerformanceMetrics; + fn get_system_health(self: @TContractState) -> SystemHealthMetrics; + fn generate_financial_report( + self: @TContractState, period_start: u64, period_end: u64, + ) -> FinancialReport; + + // Utility functions (simple operations) + fn get_member_reliability_score(self: @TContractState, member: ContractAddress) -> u8; + fn get_round_success_rate(self: @TContractState, round_id: u256) -> u8; + fn get_total_system_value(self: @TContractState) -> u256; +} + + +#[starknet::component] +pub mod analytics_component { + use core::array::ArrayTrait; + use starknet::storage::{ + Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, + StoragePointerWriteAccess, + }; + use starknet::{ContractAddress, get_block_timestamp, get_caller_address}; + use starkremit_contract::base::errors::AnalyticsComponentErrors; + use super::*; + + #[storage] + pub struct Storage { + contribution_analytics: ContributionAnalytics, + member_analytics: Map, + round_metrics: Map, + financial_reports: Map, // timestamp -> report + system_metrics: SystemHealthMetrics, + admin: ContractAddress, + last_update_timestamp: u64, + total_system_value: u256, + analytics_enabled: bool, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + AnalyticsUpdated: AnalyticsUpdated, + MemberAnalyticsUpdated: MemberAnalyticsUpdated, + RoundMetricsUpdated: RoundMetricsUpdated, + FinancialReportGenerated: FinancialReportGenerated, + SystemHealthUpdated: SystemHealthUpdated, + AnalyticsConfigUpdated: AnalyticsConfigUpdated, + } + + #[derive(Drop, starknet::Event)] + pub struct AnalyticsUpdated { + admin: ContractAddress, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct MemberAnalyticsUpdated { + member: ContractAddress, + reliability_score: u8, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct RoundMetricsUpdated { + round_id: u256, + completion_rate: u8, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct FinancialReportGenerated { + period_start: u64, + period_end: u64, + total_contributions: u256, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct SystemHealthUpdated { + security_score: u8, + uptime_percentage: u8, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct AnalyticsConfigUpdated { + admin: ContractAddress, + enabled: bool, + timestamp: u64, + } + + #[embeddable_as(Analytics)] + pub impl AnalyticsImpl< + TContractState, +HasComponent, +IMainContractData, + > of super::IAnalytics> { + fn get_contribution_analytics( + self: @ComponentState, + ) -> ContributionAnalytics { + self.contribution_analytics.read() + } + + fn get_member_analytics( + self: @ComponentState, member: ContractAddress, + ) -> MemberAnalytics { + self.member_analytics.read(member) + } + + fn get_round_performance( + self: @ComponentState, round_id: u256, + ) -> RoundPerformanceMetrics { + self.round_metrics.read(round_id) + } + + fn get_system_health(self: @ComponentState) -> SystemHealthMetrics { + self.system_metrics.read() + } + + fn generate_financial_report( + self: @ComponentState, period_start: u64, period_end: u64, + ) -> FinancialReport { + self._generate_financial_report(period_start, period_end) + } + + fn get_member_reliability_score( + self: @ComponentState, member: ContractAddress, + ) -> u8 { + let analytics = self.member_analytics.read(member); + analytics.reliability_score + } + + fn get_round_success_rate(self: @ComponentState, round_id: u256) -> u8 { + let metrics = self.round_metrics.read(round_id); + metrics.completion_rate + } + + fn get_total_system_value(self: @ComponentState) -> u256 { + self.total_system_value.read() + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, +HasComponent, +IMainContractData, + > of InternalTrait { + fn initializer(ref self: ComponentState, admin: ContractAddress) { + self.admin.write(admin); + self.analytics_enabled.write(true); + self.last_update_timestamp.write(get_block_timestamp()); + + // Initialize default analytics + let default_analytics = ContributionAnalytics { + total_rounds: 0, + successful_rounds: 0, + failed_rounds: 0, + average_completion_time: 0, + total_penalties_collected: 0, + total_contributions: 0, + last_updated: get_block_timestamp(), + }; + self.contribution_analytics.write(default_analytics); + + // Initialize system health metrics + let default_health = SystemHealthMetrics { + system_uptime_percentage: 100, + active_rounds: 0, + total_locked_value: 0, + security_score: 100, + member_satisfaction_score: 100, + last_health_check: get_block_timestamp(), + }; + self.system_metrics.write(default_health); + + // Initialize total system value + self.total_system_value.write(0); + } + + // Internal function to assert that the caller is the admin + fn _assert_admin(self: @ComponentState) { + let admin: ContractAddress = self.admin.read(); + let caller: ContractAddress = get_caller_address(); + assert(caller == admin, AnalyticsComponentErrors::NOT_ADMIN); + } + + // Complex operations that will be called by the main contract + fn _update_member_analytics( + ref self: ComponentState, + member: ContractAddress, + payment_made: bool, + amount: u256, + round_id: u256, + ) { + let contract_state = self.get_contract(); + let mut analytics = self.member_analytics.read(member); + + // Update contribution statistics + analytics.total_contributions += amount; + analytics.total_rounds_participated += 1; + + if payment_made { + analytics.on_time_payments += 1; + } else { + analytics.missed_payments += 1; + } + + // Calculate average contribution amount + if analytics.total_rounds_participated > 0 { + analytics.average_contribution_amount = analytics.total_contributions + / analytics.total_rounds_participated; + } + + // Update reliability score + analytics + .reliability_score = self + ._calculate_reliability_score( + analytics.on_time_payments, analytics.total_rounds_participated, + ); + analytics.last_updated = get_block_timestamp(); + + self.member_analytics.write(member, analytics); + + self + .emit( + Event::MemberAnalyticsUpdated( + MemberAnalyticsUpdated { + member, + reliability_score: analytics.reliability_score, + timestamp: get_block_timestamp(), + }, + ), + ); + } + + fn _update_round_metrics( + ref self: ComponentState, + round_id: u256, + status: RoundStatus, + total_contributions: u256, + participant_count: u32, + ) { + let contract_state = self.get_contract(); + let round = contract_state.get_round_data(round_id); + let current_time = get_block_timestamp(); + + let mut metrics = self.round_metrics.read(round_id); + metrics.round_id = round_id; + metrics.total_contributions = total_contributions; + metrics.participant_count = participant_count; + + // Calculate completion rate based on member count + let member_count = contract_state.get_member_count(); + if member_count > 0 { + metrics + .completion_rate = ((participant_count * 100) / member_count) + .try_into() + .unwrap(); + } + + // Calculate completion time if round is completed + if status == RoundStatus::Completed { + // Use the round's recorded completion timestamp, and compute delay vs deadline + let completed_at = round.completed_at; + metrics.completion_time = completed_at; + if completed_at > 0 { + if completed_at > round.deadline { + metrics.average_delay = completed_at - round.deadline; + } else { + metrics.average_delay = 0; + } + } + } + + // Determine success status + metrics.success_status = self._determine_success_status(metrics.completion_rate); + + self.round_metrics.write(round_id, metrics); + + self + .emit( + Event::RoundMetricsUpdated( + RoundMetricsUpdated { + round_id, + completion_rate: metrics.completion_rate, + timestamp: get_block_timestamp(), + }, + ), + ); + } + + fn _update_contribution_analytics(ref self: ComponentState) { + let contract_state = self.get_contract(); + let mut analytics = self.contribution_analytics.read(); + + let total_rounds = contract_state.get_round_ids(); + let mut successful_rounds = 0; + let mut failed_rounds = 0; + let mut total_completion_time = 0; + let mut completed_rounds_count = 0; + + // Calculate round statistics + let mut round_id = 1; + while round_id <= total_rounds { + let round = contract_state.get_round_data(round_id); + if round.status == RoundStatus::Completed { + successful_rounds += 1; + // Use recorded round metrics completion time if available + let metrics = self.round_metrics.read(round_id); + if metrics.completion_time > 0 { + total_completion_time += metrics.completion_time; + completed_rounds_count += 1; + } + } else if round.status == RoundStatus::Cancelled { + failed_rounds += 1; + } + round_id += 1; + } + + analytics.total_rounds = total_rounds; + analytics.successful_rounds = successful_rounds; + analytics.failed_rounds = failed_rounds; + + if completed_rounds_count > 0 { + analytics.average_completion_time = total_completion_time / completed_rounds_count; + } + + analytics.last_updated = get_block_timestamp(); + self.contribution_analytics.write(analytics); + + self + .emit( + Event::AnalyticsUpdated( + AnalyticsUpdated { + admin: get_caller_address(), timestamp: get_block_timestamp(), + }, + ), + ); + } + + fn _update_system_health(ref self: ComponentState) { + let contract_state = self.get_contract(); + let mut health = self.system_metrics.read(); + + // Calculate active rounds + let total_rounds = contract_state.get_round_ids(); + let mut active_rounds = 0; + let mut round_id = 1; + while round_id <= total_rounds { + let round = contract_state.get_round_data(round_id); + if round.status == RoundStatus::Active { + active_rounds += 1; + } + round_id += 1; + } + + health.active_rounds = active_rounds.try_into().unwrap(); + health.total_locked_value = self.total_system_value.read(); + health.last_health_check = get_block_timestamp(); + + // Calculate security score based on various factors + health.security_score = self._calculate_security_score(); + + self.system_metrics.write(health); + + self + .emit( + Event::SystemHealthUpdated( + SystemHealthUpdated { + security_score: health.security_score, + uptime_percentage: health.system_uptime_percentage, + timestamp: get_block_timestamp(), + }, + ), + ); + } + + fn _generate_financial_report( + self: @ComponentState, period_start: u64, period_end: u64, + ) -> FinancialReport { + let contract_state = self.get_contract(); + let analytics = self.contribution_analytics.read(); + + let report = FinancialReport { + period_start, + period_end, + total_contributions: analytics.total_contributions, + total_fees_collected: analytics.total_penalties_collected, // Simplified + total_penalties_collected: analytics.total_penalties_collected, + active_members: contract_state.get_member_count(), + rounds_completed: analytics.successful_rounds.try_into().unwrap(), + average_round_completion_time: analytics.average_completion_time, + system_uptime_percentage: 100 // Simplified + }; + + report + } + + // Helper functions + fn _calculate_reliability_score( + self: @ComponentState, on_time_payments: u256, total_rounds: u256, + ) -> u8 { + if total_rounds == 0 { + return 100; + } + + let score = (on_time_payments * 100) / total_rounds; + score.try_into().unwrap_or(100) + } + + fn _determine_success_status( + self: @ComponentState, completion_rate: u8, + ) -> RoundSuccessStatus { + if completion_rate >= 95 { + RoundSuccessStatus::Outstanding + } else if completion_rate >= 85 { + RoundSuccessStatus::Good + } else if completion_rate >= 70 { + RoundSuccessStatus::Average + } else if completion_rate >= 50 { + RoundSuccessStatus::Poor + } else { + RoundSuccessStatus::Failed + } + } + + fn _calculate_security_score(self: @ComponentState) -> u8 { + // Weighted security score based on available metrics + // Metrics used: + // - System uptime percentage (positive weight) + // - Average round completion rate (positive weight) + // - Verified/active members percentage via get_member_status (positive weight) + // - Average member reliability score (positive weight) + // Weights: uptime 30, completion 25, verified 25, reliability 20 + + let contract_state = self.get_contract(); + + // Uptime (0..100) + let health = self.system_metrics.read(); + let uptime_score: u8 = health.system_uptime_percentage; + + // Average round completion rate (0..100) + let total_rounds: u256 = contract_state.get_round_ids(); + let mut completion_sum: u256 = 0; + let mut completion_count: u256 = 0; + let mut r_id: u256 = 1; + while r_id <= total_rounds { + let rm = self.round_metrics.read(r_id); + // Only count rounds that have a non-zero recorded completion rate + if rm.completion_rate > 0_u8 { + completion_sum += (rm.completion_rate).into(); + completion_count += 1; + } + r_id += 1; + } + let completion_avg_u8: u8 = if completion_count > 0 { + let avg: u256 = completion_sum / completion_count; + avg.try_into().unwrap_or(0_u8) + } else { + 50_u8 // neutral default when there are no rounds + }; + + // Verified/active members percentage (0..100) + let members_total: u32 = contract_state.get_member_count(); + let mut verified_count: u32 = 0; + let mut idx: u32 = 0; + while idx < members_total { + let addr = contract_state.get_member_by_index(idx); + if contract_state.get_member_status(addr) { + verified_count += 1; + } + idx += 1; + } + let verified_pct_u8: u8 = if members_total > 0 { + let pct: u32 = (verified_count * 100_u32) / members_total; + pct.try_into().unwrap_or(0_u8) + } else { + 50_u8 // neutral default when there are no members + }; + + // Average member reliability (0..100) + let mut reliability_sum: u256 = 0; + let mut m_idx: u32 = 0; + while m_idx < members_total { + let m_addr = contract_state.get_member_by_index(m_idx); + let ma = self.member_analytics.read(m_addr); + reliability_sum += (ma.reliability_score).into(); + m_idx += 1; + } + let reliability_avg_u8: u8 = if members_total > 0 { + let avg_rel: u256 = reliability_sum / (members_total.into()); + avg_rel.try_into().unwrap_or(0_u8) + } else { + 50_u8 + }; + + // Weighted combination; use u256 for intermediate math + let w_uptime: u256 = (uptime_score).into() * 30_u256; + let w_completion: u256 = (completion_avg_u8).into() * 25_u256; + let w_verified: u256 = (verified_pct_u8).into() * 25_u256; + let w_reliability: u256 = (reliability_avg_u8).into() * 20_u256; + let total_weighted: u256 = w_uptime + w_completion + w_verified + w_reliability; + let mut combined: u256 = total_weighted / 100_u256; + + // Clamp to 0..100 + if combined > 100_u256 { + combined = 100_u256; + } + + combined.try_into().unwrap_or(100_u8) + } + } +} diff --git a/src/component/auto_schedule.cairo b/src/component/auto_schedule.cairo index 38323d9..cde64d0 100644 --- a/src/component/auto_schedule.cairo +++ b/src/component/auto_schedule.cairo @@ -17,22 +17,22 @@ pub trait IAutoSchedule { fn get_scheduled_round(self: @TContractState, round_id: u256) -> ScheduledRound; fn get_next_scheduled_rounds(self: @TContractState, count: u8) -> Array; fn get_current_rotation_index(self: @TContractState) -> u32; - + fn is_auto_schedule_enabled(self: @TContractState) -> bool; fn get_rotation_length(self: @TContractState) -> u32; } #[starknet::component] pub mod auto_schedule_component { - use starknet::{ContractAddress, get_block_timestamp, get_caller_address}; + use core::array::ArrayTrait; use starknet::storage::{ - Map, StoragePointerReadAccess, StoragePointerWriteAccess, StorageMapReadAccess, StorageMapWriteAccess, + Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, + StoragePointerWriteAccess, }; - - use core::array::ArrayTrait; - use starkremit_contract::base::types::RoundStatus; - use super::{AutoScheduleConfig, ScheduledRound, IMainContractData}; + use starknet::{ContractAddress, get_block_timestamp, get_caller_address}; use starkremit_contract::base::errors::AutoScheduleErrors; + use starkremit_contract::base::types::RoundStatus; + use super::{AutoScheduleConfig, IMainContractData, ScheduledRound}; const SECONDS_PER_DAY: u64 = 86400; @@ -44,6 +44,7 @@ pub mod auto_schedule_component { rotation_length: u32, current_rotation_index: u32, next_round_id: u256, + last_processed_round: u256, last_processed_timestamp: u64, admin: ContractAddress, } @@ -112,6 +113,7 @@ pub mod auto_schedule_component { #[derive(Drop, starknet::Event)] pub struct ScheduleProcessed { rounds_processed: u32, + more_work_remaining: bool, timestamp: u64, } @@ -119,16 +121,19 @@ pub mod auto_schedule_component { impl AutoScheduleImpl< TContractState, +HasComponent, +IMainContractData, > of super::IAutoSchedule> { - fn get_config(self: @ComponentState) -> AutoScheduleConfig { self.config.read() } - fn get_scheduled_round(self: @ComponentState, round_id: u256) -> ScheduledRound { + fn get_scheduled_round( + self: @ComponentState, round_id: u256, + ) -> ScheduledRound { self.scheduled_rounds.read(round_id) } - fn get_next_scheduled_rounds(self: @ComponentState, count: u8) -> Array { + fn get_next_scheduled_rounds( + self: @ComponentState, count: u8, + ) -> Array { let mut rounds = ArrayTrait::new(); let current_index = self.current_rotation_index.read(); let mut rounds_added = 0_u8; @@ -165,10 +170,9 @@ pub mod auto_schedule_component { pub impl InternalImpl< TContractState, +HasComponent, +IMainContractData, > of InternalTrait { - fn initializer(ref self: ComponentState, admin: ContractAddress) { self.admin.write(admin); - + // Set default auto-schedule configuration let default_config = AutoScheduleConfig { round_duration_days: 30, @@ -178,20 +182,26 @@ pub mod auto_schedule_component { rolling_schedule_count: 3, }; self.config.write(default_config); - + // Initialize rotation system self.rotation_length.write(0); self.current_rotation_index.write(0); self.next_round_id.write(1); + self.last_processed_round.write(1); self.last_processed_timestamp.write(get_block_timestamp()); - - self.emit(Event::AutoScheduleSetup(AutoScheduleSetup { - admin, - start_date: default_config.start_date, - round_duration_days: default_config.round_duration_days, - rolling_schedule_count: default_config.rolling_schedule_count, - timestamp: get_block_timestamp(), - })); + + self + .emit( + Event::AutoScheduleSetup( + AutoScheduleSetup { + admin, + start_date: default_config.start_date, + round_duration_days: default_config.round_duration_days, + rolling_schedule_count: default_config.rolling_schedule_count, + timestamp: get_block_timestamp(), + }, + ), + ); } // Internal function to assert that the caller is the admin @@ -202,23 +212,30 @@ pub mod auto_schedule_component { } // Complex operations that will be called by the main contract - fn _setup_auto_schedule(ref self: ComponentState, config: AutoScheduleConfig) { + fn _setup_auto_schedule( + ref self: ComponentState, config: AutoScheduleConfig, + ) { self._assert_admin(); - + let old_config = self.config.read(); self.config.write(config); - + // Initialize member rotation if not already set if self.rotation_length.read() == 0 { self._initialize_member_rotation(); } - - self.emit(Event::ConfigUpdated(ConfigUpdated { - old_config, - new_config: config, - updated_by: get_caller_address(), - timestamp: get_block_timestamp(), - })); + + self + .emit( + Event::ConfigUpdated( + ConfigUpdated { + old_config, + new_config: config, + updated_by: get_caller_address(), + timestamp: get_block_timestamp(), + }, + ), + ); } fn _maintain_rolling_schedule(ref self: ComponentState) { @@ -240,7 +257,8 @@ pub mod auto_schedule_component { // Calculate timing for the new round let round_duration_seconds: u64 = config.round_duration_days * SECONDS_PER_DAY; - let round_start = config.start_date + ((current_index - 1_u256).try_into().unwrap() * round_duration_seconds); + let round_start = config.start_date + + ((current_index - 1_u256).try_into().unwrap() * round_duration_seconds); let round_deadline = round_start + round_duration_seconds; // Determine recipient by rotating through the member list @@ -263,11 +281,16 @@ pub mod auto_schedule_component { self.next_round_id.write(current_index); self.last_processed_timestamp.write(current_time); - self.emit(Event::ScheduleMaintained(ScheduleMaintained { - rounds_created: rounds_created.try_into().unwrap(), - last_maintenance_timestamp: current_time, - timestamp: get_block_timestamp(), - })); + self + .emit( + Event::ScheduleMaintained( + ScheduleMaintained { + rounds_created: rounds_created.try_into().unwrap(), + last_maintenance_timestamp: current_time, + timestamp: get_block_timestamp(), + }, + ), + ); } fn _auto_activate_round(ref self: ComponentState, round_id: u256) { @@ -281,83 +304,139 @@ pub mod auto_schedule_component { scheduled_round.status = RoundStatus::Active; self.scheduled_rounds.write(round_id, scheduled_round); - self.emit(Event::RoundAutoActivated(RoundAutoActivated { - round_id, - recipient: scheduled_round.recipient, - scheduled_start: scheduled_round.scheduled_start, - scheduled_deadline: scheduled_round.scheduled_deadline, - timestamp: get_block_timestamp(), - })); + self + .emit( + Event::RoundAutoActivated( + RoundAutoActivated { + round_id, + recipient: scheduled_round.recipient, + scheduled_start: scheduled_round.scheduled_start, + scheduled_deadline: scheduled_round.scheduled_deadline, + timestamp: get_block_timestamp(), + }, + ), + ); } } - fn _auto_complete_expired_rounds(ref self: ComponentState) { + fn _auto_complete_expired_rounds( + ref self: ComponentState, max_iterations: u32, + ) -> (u32, bool) { let config = self.config.read(); if !config.auto_completion_enabled { - return; + return (0, false); } let current_time = get_block_timestamp(); - let mut rounds_processed = 0; + let mut rounds_processed = 0_u32; + + // Determine batch limit + let default_limit: u32 = 50; + let limit: u32 = if max_iterations == 0 { + default_limit + } else { + max_iterations + }; - // Check all scheduled rounds for expiration - let mut i = 1_u256; - while i <= self.next_round_id.read() { + // Iterate starting from the last processed cursor with wrap-around + let next_round_id = self.next_round_id.read(); + if next_round_id == 0_u256 { + return (0, false); + } + let mut i = self.last_processed_round.read(); + if i < 1_u256 { + i = 1_u256; + } + + let mut iterated: u32 = 0; + while i <= next_round_id && iterated < limit { let mut scheduled_round = self.scheduled_rounds.read(i); if scheduled_round.status == RoundStatus::Active && scheduled_round.scheduled_deadline <= current_time { scheduled_round.status = RoundStatus::Completed; self.scheduled_rounds.write(i, scheduled_round); - rounds_processed += 1; - - self.emit(Event::RoundAutoCompleted(RoundAutoCompleted { - round_id: i, - completed_at: current_time, - timestamp: get_block_timestamp(), - })); + rounds_processed += 1_u32; + + self + .emit( + Event::RoundAutoCompleted( + RoundAutoCompleted { + round_id: i, + completed_at: current_time, + timestamp: get_block_timestamp(), + }, + ), + ); } - i += 1; + i += 1_u256; + iterated += 1_u32; } - if rounds_processed > 0 { - self.emit(Event::ScheduleProcessed(ScheduleProcessed { - rounds_processed, - timestamp: get_block_timestamp(), - })); + // Update cursor: wrap to 1 if we've reached the end + let mut more_work_remaining = false; + if i <= next_round_id { + more_work_remaining = true; + self.last_processed_round.write(i); + } else { + self.last_processed_round.write(1); } + + self + .emit( + Event::ScheduleProcessed( + ScheduleProcessed { + rounds_processed, more_work_remaining, timestamp: get_block_timestamp(), + }, + ), + ); + + (rounds_processed, more_work_remaining) } - fn _modify_schedule(ref self: ComponentState, round_id: u256, new_deadline: u64) { + fn _modify_schedule( + ref self: ComponentState, round_id: u256, new_deadline: u64, + ) { self._assert_admin(); let mut scheduled_round = self.scheduled_rounds.read(round_id); let old_deadline = scheduled_round.scheduled_deadline; // Validate new deadline - assert(new_deadline > get_block_timestamp(), AutoScheduleErrors::NEW_DEADLINE_NOT_IN_FUTURE); - assert(new_deadline > scheduled_round.scheduled_start, AutoScheduleErrors::NEW_DEADLINE_NOT_AFTER_START); + assert( + new_deadline > get_block_timestamp(), + AutoScheduleErrors::NEW_DEADLINE_NOT_IN_FUTURE, + ); + assert( + new_deadline > scheduled_round.scheduled_start, + AutoScheduleErrors::NEW_DEADLINE_NOT_AFTER_START, + ); scheduled_round.scheduled_deadline = new_deadline; self.scheduled_rounds.write(round_id, scheduled_round); - self.emit(Event::RoundScheduleModified(RoundScheduleModified { - round_id, - old_deadline, - new_deadline, - modified_by: get_caller_address(), - timestamp: get_block_timestamp(), - })); + self + .emit( + Event::RoundScheduleModified( + RoundScheduleModified { + round_id, + old_deadline, + new_deadline, + modified_by: get_caller_address(), + timestamp: get_block_timestamp(), + }, + ), + ); } // Helper functions fn _initialize_member_rotation(ref self: ComponentState) { let contract_state = self.get_contract(); let member_count = contract_state.get_member_count(); - + if member_count > 0 { self.rotation_length.write(member_count); - + // Populate rotation array let mut i = 0; while i < member_count { @@ -375,13 +454,13 @@ pub mod auto_schedule_component { } let current_index = self.current_rotation_index.read(); + let recipient = self.member_rotation.read(current_index); let next_index = (current_index + 1_u32) % rotation_length; - - // Update rotation index + + // Update rotation index after selecting the recipient self.current_rotation_index.write(next_index); - - self.member_rotation.read(next_index) + + recipient } } - } diff --git a/src/component/contribution/contribution.cairo b/src/component/contribution/contribution.cairo index 9efe1d8..e4c3a0f 100644 --- a/src/component/contribution/contribution.cairo +++ b/src/component/contribution/contribution.cairo @@ -1,6 +1,6 @@ use starknet::ContractAddress; -use starkremit_contract::base::types::{ContributionRound, MemberContribution, RoundStatus}; use starkremit_contract::base::errors::ContributionErrors; +use starkremit_contract::base::types::{ContributionRound, MemberContribution, RoundStatus}; #[starknet::interface] pub trait IContribution { @@ -21,12 +21,18 @@ pub trait IContribution { fn get_current_round_id(self: @TContractState) -> u256; fn set_required_contribution(ref self: TContractState, amount: u256); fn get_required_contribution(self: @TContractState) -> u256; - - fn get_member_contribution_history(self: @TContractState, member: ContractAddress, limit: u32, offset: u32) -> Array; - fn get_round_statistics(self: @TContractState, round_id: u256) -> (u256, u32, u32); // total_amount, contributor_count, member_count - fn validate_contribution_eligibility(self: @TContractState, member: ContractAddress, round_id: u256) -> bool; - fn get_next_recipient(self: @TContractState) -> ContractAddress; + + fn get_member_contribution_history( + self: @TContractState, member: ContractAddress, limit: u32, offset: u32, + ) -> Array; + fn get_round_statistics( + self: @TContractState, round_id: u256, + ) -> (u256, u32, u32); // total_amount, contributor_count, member_count + fn validate_contribution_eligibility( + self: @TContractState, member: ContractAddress, round_id: u256, + ) -> bool; + fn get_next_recipient(ref self: TContractState) -> ContractAddress; fn advance_round_rotation(ref self: TContractState); } @@ -41,8 +47,8 @@ pub mod contribution_component { StoragePointerWriteAccess, }; use starknet::{ContractAddress, get_block_timestamp, get_caller_address, get_contract_address}; - use starkremit_contract::base::types::{ContributionRound, MemberContribution, RoundStatus}; use starkremit_contract::base::errors::ContributionErrors; + use starkremit_contract::base::types::{ContributionRound, MemberContribution, RoundStatus}; use super::*; #[storage] @@ -58,15 +64,18 @@ pub mod contribution_component { required_contribution: u256, member_index_map: Map, erc20_address: ContractAddress, - // Enhanced storage - member_contribution_history: Map<(ContractAddress, u32), u256>, // member -> (index -> round_id) + member_contribution_history: Map< + (ContractAddress, u32), u256, + >, // member -> (index -> round_id) member_contribution_count: Map, // member -> total contributions current_rotation_index: u32, // Current position in member rotation round_contributor_count: Map, // round_id -> number of contributors - member_last_contribution: Map, // member -> last contribution timestamp + member_last_contribution: Map< + ContractAddress, u64, + >, // member -> last contribution timestamp contribution_limits: Map, // member -> max contribution per round - grace_period_hours: u64, // Grace period for late contributions + grace_period_hours: u64 // Grace period for late contributions } #[event] @@ -83,7 +92,7 @@ pub mod contribution_component { GracePeriodUpdated: GracePeriodUpdated, RoundRotationAdvanced: RoundRotationAdvanced, } - + #[derive(Drop, starknet::Event)] pub struct ContributionMade { round_id: u256, @@ -92,7 +101,7 @@ pub mod contribution_component { timestamp: u64, is_on_time: bool, } - + #[derive(Drop, starknet::Event)] pub struct RoundDisbursed { round_id: u256, @@ -101,7 +110,7 @@ pub mod contribution_component { contributor_count: u32, timestamp: u64, } - + #[derive(Drop, starknet::Event)] pub struct RoundCompleted { round_id: u256, @@ -109,14 +118,14 @@ pub mod contribution_component { contributor_count: u32, timestamp: u64, } - + #[derive(Drop, starknet::Event)] pub struct ContributionMissed { round_id: u256, member: ContractAddress, timestamp: u64, } - + #[derive(Drop, starknet::Event)] pub struct MemberAdded { member: ContractAddress, @@ -130,7 +139,7 @@ pub mod contribution_component { removed_by: ContractAddress, timestamp: u64, } - + #[derive(Drop, starknet::Event)] pub struct RequiredContributionUpdated { old_amount: u256, @@ -138,7 +147,7 @@ pub mod contribution_component { updated_by: ContractAddress, timestamp: u64, } - + #[derive(Drop, starknet::Event)] pub struct ContributionLimitUpdated { member: ContractAddress, @@ -147,7 +156,7 @@ pub mod contribution_component { updated_by: ContractAddress, timestamp: u64, } - + #[derive(Drop, starknet::Event)] pub struct GracePeriodUpdated { old_hours: u64, @@ -155,7 +164,7 @@ pub mod contribution_component { updated_by: ContractAddress, timestamp: u64, } - + #[derive(Drop, starknet::Event)] pub struct RoundRotationAdvanced { old_index: u32, @@ -176,7 +185,7 @@ pub mod contribution_component { ) { let caller = get_caller_address(); let current_time = get_block_timestamp(); - + // Validate caller is a member assert(self.is_member(caller), ContributionErrors::NOT_MEMBER); @@ -186,7 +195,7 @@ pub mod contribution_component { let mut round = self.rounds.read(round_id); assert(round.status == RoundStatus::Active, ContributionErrors::ROUND_NOT_ACTIVE); - + // Check if contribution is within grace period let grace_period = self.grace_period_hours.read() * 3600; // Convert to seconds let is_on_time = current_time <= round.deadline + grace_period; @@ -195,7 +204,7 @@ pub mod contribution_component { // Validate contribution amount let required_amount = self.required_contribution.read(); assert(amount >= required_amount, ContributionErrors::INSUFFICIENT_AMOUNT); - + // Check contribution limits let member_limit = self.contribution_limits.read(caller); if member_limit > 0 { @@ -204,25 +213,23 @@ pub mod contribution_component { // Create contribution record let contribution = MemberContribution { - member: caller, - amount, - contributed_at: current_time, + member: caller, amount, contributed_at: current_time, }; self.member_contributions.write((round_id, caller), contribution); - + // Update round statistics round.total_contributions += amount; self.rounds.write(round_id, round); - + // Update contributor count let contributor_count = self.round_contributor_count.read(round_id); self.round_contributor_count.write(round_id, contributor_count + 1); - + // Update member statistics let member_contribution_count = self.member_contribution_count.read(caller); self.member_contribution_count.write(caller, member_contribution_count + 1); self.member_last_contribution.write(caller, current_time); - + // Add to member's contribution history let history_count = self.member_contribution_count.read(caller); self.member_contribution_history.write((caller, history_count - 1), round_id); @@ -232,13 +239,18 @@ pub mod contribution_component { IERC20Dispatcher { contract_address: erc20_address } .transfer_from(caller, get_contract_address(), amount); - self.emit(Event::ContributionMade(ContributionMade { - round_id, - member: caller, - amount, - timestamp: current_time, - is_on_time: current_time <= round.deadline, - })); + self + .emit( + Event::ContributionMade( + ContributionMade { + round_id, + member: caller, + amount, + timestamp: current_time, + is_on_time: current_time <= round.deadline, + }, + ), + ); } fn complete_round(ref self: ComponentState, round_id: u256) { @@ -246,19 +258,25 @@ pub mod contribution_component { let mut round = self.rounds.read(round_id); assert(round.status == RoundStatus::Active, ContributionErrors::ROUND_NOT_ACTIVE); - + let current_time = get_block_timestamp(); round.status = RoundStatus::Completed; + round.completed_at = current_time; self.rounds.write(round_id, round); - + let contributor_count = self.round_contributor_count.read(round_id); - - self.emit(Event::RoundCompleted(RoundCompleted { - round_id, - total_amount: round.total_contributions, - contributor_count, - timestamp: current_time, - })); + + self + .emit( + Event::RoundCompleted( + RoundCompleted { + round_id, + total_amount: round.total_contributions, + contributor_count, + timestamp: current_time, + }, + ), + ); } fn add_round_to_schedule( @@ -275,16 +293,17 @@ pub mod contribution_component { let round_id = self.round_ids.read() + 1; self.round_ids.write(round_id); self.rotation_schedule.write(round_id, recipient); - + let round = ContributionRound { - round_id, - recipient, - deadline, - total_contributions: 0, + round_id, + recipient, + deadline, + completed_at: 0, + total_contributions: 0, status: RoundStatus::Active, }; self.rounds.write(round_id, round); - + // Initialize contributor count self.round_contributor_count.write(round_id, 0); } @@ -296,10 +315,13 @@ pub mod contribution_component { fn check_missed_contributions(ref self: ComponentState, round_id: u256) { let round = self.rounds.read(round_id); assert(round.status == RoundStatus::Active, ContributionErrors::ROUND_NOT_ACTIVE); - + let current_time = get_block_timestamp(); let grace_period = self.grace_period_hours.read() * 3600; - assert(current_time > round.deadline + grace_period, ContributionErrors::ROUND_DEADLINE_NOT_PASSED); + assert( + current_time > round.deadline + grace_period, + ContributionErrors::ROUND_DEADLINE_NOT_PASSED, + ); // Check all members for missed contributions let all_members = self.get_all_members(); @@ -308,11 +330,14 @@ pub mod contribution_component { let member = *all_members[i]; let contribution = self.member_contributions.read((round_id, member)); if contribution.amount == 0 { - self.emit(Event::ContributionMissed(ContributionMissed { - round_id, - member: member, - timestamp: current_time, - })); + self + .emit( + Event::ContributionMissed( + ContributionMissed { + round_id, member: member, timestamp: current_time, + }, + ), + ); } i += 1; } @@ -350,16 +375,21 @@ pub mod contribution_component { self.member_index_map.write(address, count); self.member_count.write(count + 1); - + // Initialize member statistics self.member_contribution_count.write(address, 0); self.member_last_contribution.write(address, 0); - - self.emit(Event::MemberAdded(MemberAdded { - member: address, - added_by: get_caller_address(), - timestamp: get_block_timestamp(), - })); + + self + .emit( + Event::MemberAdded( + MemberAdded { + member: address, + added_by: get_caller_address(), + timestamp: get_block_timestamp(), + }, + ), + ); } fn disburse_round_contribution(ref self: ComponentState, round_id: u256) { @@ -376,13 +406,18 @@ pub mod contribution_component { IERC20Dispatcher { contract_address: erc20_address } .transfer(round.recipient, round.total_contributions); - self.emit(Event::RoundDisbursed(RoundDisbursed { - round_id, - recipient: round.recipient, - amount: round.total_contributions, - contributor_count, - timestamp: current_time, - })); + self + .emit( + Event::RoundDisbursed( + RoundDisbursed { + round_id, + recipient: round.recipient, + amount: round.total_contributions, + contributor_count, + timestamp: current_time, + }, + ), + ); } fn remove_member(ref self: ComponentState, address: ContractAddress) { @@ -409,11 +444,16 @@ pub mod contribution_component { self.member_count.write(last_index); self.member_index_map.write(address, 0); - self.emit(Event::MemberRemoved(MemberRemoved { - member: address, - removed_by: get_caller_address(), - timestamp: get_block_timestamp(), - })); + self + .emit( + Event::MemberRemoved( + MemberRemoved { + member: address, + removed_by: get_caller_address(), + timestamp: get_block_timestamp(), + }, + ), + ); } fn get_round_details( @@ -438,31 +478,33 @@ pub mod contribution_component { let old_amount = self.required_contribution.read(); self.required_contribution.write(amount); - self.emit(Event::RequiredContributionUpdated(RequiredContributionUpdated { - old_amount, - new_amount: amount, - updated_by: get_caller_address(), - timestamp: get_block_timestamp(), - })); + self + .emit( + Event::RequiredContributionUpdated( + RequiredContributionUpdated { + old_amount, + new_amount: amount, + updated_by: get_caller_address(), + timestamp: get_block_timestamp(), + }, + ), + ); } fn get_required_contribution(self: @ComponentState) -> u256 { self.required_contribution.read() } - + // Enhanced functions fn get_member_contribution_history( - self: @ComponentState, - member: ContractAddress, - limit: u32, - offset: u32 + self: @ComponentState, member: ContractAddress, limit: u32, offset: u32, ) -> Array { let mut contributions = ArrayTrait::new(); let total_count = self.member_contribution_count.read(member); - + let mut i = offset; let mut count = 0; - + while i < total_count && count < limit { let round_id = self.member_contribution_history.read((member, i)); if round_id > 0 { @@ -472,86 +514,124 @@ pub mod contribution_component { } i += 1; } - + contributions } - + fn get_round_statistics( - self: @ComponentState, - round_id: u256 + self: @ComponentState, round_id: u256, ) -> (u256, u32, u32) { let round = self.rounds.read(round_id); let contributor_count = self.round_contributor_count.read(round_id); let member_count = self.member_count.read(); - + (round.total_contributions, contributor_count, member_count) } - + fn validate_contribution_eligibility( - self: @ComponentState, - member: ContractAddress, - round_id: u256 + self: @ComponentState, member: ContractAddress, round_id: u256, ) -> bool { // Check if member exists and is active if !self.is_member(member) { return false; } - + // Check if round is active let round = self.rounds.read(round_id); if round.status != RoundStatus::Active { return false; } - + // Check if member already contributed let contribution = self.member_contributions.read((round_id, member)); if contribution.amount > 0 { return false; } - + // Check if deadline hasn't passed (including grace period) let current_time = get_block_timestamp(); let grace_period = self.grace_period_hours.read() * 3600; if current_time > round.deadline + grace_period { return false; } - + true } - - fn get_next_recipient(self: @ComponentState) -> ContractAddress { + + fn get_next_recipient(ref self: ComponentState) -> ContractAddress { let member_count = self.member_count.read(); if member_count == 0 { return 0.try_into().unwrap(); } - + + // Start scanning from the next index after the current rotation index let current_index = self.current_rotation_index.read(); - let next_index = (current_index + 1) % member_count; - - self.member_by_index.read(next_index) + let start_index = (current_index + 1) % member_count; + + // Scan up to member_count entries to find the first active member + let mut scanned = 0_u32; + while scanned < member_count { + let candidate_index = (start_index + scanned) % member_count; + let candidate = self.member_by_index.read(candidate_index); + if self.is_member(candidate) { + // Advance rotation to the found active member and return it + self.current_rotation_index.write(candidate_index); + return candidate; + } + scanned += 1_u32; + } + + // If no active member found, return zero address + 0.try_into().unwrap() } - + fn advance_round_rotation(ref self: ComponentState) { self.is_owner(); - + let member_count = self.member_count.read(); if member_count == 0 { return; } - + let current_index = self.current_rotation_index.read(); - let new_index = (current_index + 1) % member_count; - - self.current_rotation_index.write(new_index); - - let next_recipient = self.member_by_index.read(new_index); - - self.emit(Event::RoundRotationAdvanced(RoundRotationAdvanced { - old_index: current_index, - new_index, - next_recipient, - timestamp: get_block_timestamp(), - })); + let start_index = (current_index + 1) % member_count; + + // Scan up to member_count entries to find the first active member + let mut scanned = 0_u32; + let mut found = false; + let mut found_index = current_index; // default to current if none found + let mut next_recipient: ContractAddress = 0.try_into().unwrap(); + + while scanned < member_count { + let candidate_index = (start_index + scanned) % member_count; + let candidate = self.member_by_index.read(candidate_index); + if self.is_member(candidate) { + found = true; + found_index = candidate_index; + next_recipient = candidate; + break; + } + scanned += 1_u32; + } + + // If no active member is found, do not change the index + if !found { + return; + } + + self.current_rotation_index.write(found_index); + + self + .emit( + Event::RoundRotationAdvanced( + RoundRotationAdvanced { + old_index: current_index, + new_index: found_index, + next_recipient, + timestamp: get_block_timestamp(), + }, + ), + ); } } diff --git a/src/component/emergency.cairo b/src/component/emergency.cairo index 0798739..827b96b 100644 --- a/src/component/emergency.cairo +++ b/src/component/emergency.cairo @@ -7,7 +7,6 @@ pub struct EmergencyConfig { } - #[starknet::interface] pub trait IEmergency { // Configuration and query functions (simple operations) @@ -17,24 +16,24 @@ pub trait IEmergency { fn get_pause_timestamp(self: @TContractState) -> u64; fn is_paused(self: @TContractState) -> bool; fn is_banned(self: @TContractState, member: ContractAddress) -> bool; - + // Utility functions (simple operations) fn assert_paused(self: @TContractState); fn assert_not_paused(self: @TContractState); } - #[starknet::component] pub mod emergency_component { - use starknet::{ContractAddress, get_block_timestamp, get_caller_address}; use starknet::storage::{ - Map, StoragePointerReadAccess, StoragePointerWriteAccess, StorageMapReadAccess, StorageMapWriteAccess, + Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, + StoragePointerWriteAccess, }; - use super::EmergencyConfig; - use starkremit_contract::base::errors::{EmergencyComponentErrors}; + use starknet::{ContractAddress, get_block_timestamp, get_caller_address}; + use starkremit_contract::base::errors::EmergencyComponentErrors; + use super::EmergencyConfig; + - #[storage] pub struct Storage { is_paused: bool, @@ -45,7 +44,7 @@ pub mod emergency_component { config: EmergencyConfig, } - + #[event] #[derive(Drop, starknet::Event)] pub enum Event { @@ -94,8 +93,6 @@ pub mod emergency_component { impl EmergencyImpl< TContractState, +HasComponent, > of super::IEmergency> { - - fn get_pause_reason(self: @ComponentState) -> felt252 { self.pause_reason.read() } @@ -105,14 +102,13 @@ pub mod emergency_component { } - fn is_banned(self: @ComponentState, member: ContractAddress) -> bool { self.banned_members.read(member) } fn set_config(ref self: ComponentState, cfg: EmergencyConfig) { - self._assert_admin(); - assert(!self.is_paused.read(), EmergencyComponentErrors::CONTRACT_PAUSED); + self._assert_admin(); + assert(!self.is_paused.read(), EmergencyComponentErrors::CONTRACT_PAUSED); self.config.write(cfg); } @@ -141,9 +137,7 @@ pub mod emergency_component { self.emergency_admin.write(admin); self.is_paused.write(false); // Component starts unpaused by default // Initialize config to default values - self.config.write( - EmergencyConfig { emergency_cooldown: 0, required_approvals: 0 } - ); + self.config.write(EmergencyConfig { emergency_cooldown: 0, required_approvals: 0 }); self.emit(Event::Initialized(Initialized { admin })); } @@ -179,7 +173,6 @@ pub mod emergency_component { fn _pause_with_metadata(ref self: ComponentState, reason: felt252) { assert(!self.is_paused.read(), EmergencyComponentErrors::ALREADY_PAUSED); self.pause_reason.write(reason); - self.pause_timestamp.write(get_block_timestamp()); self._toggle_pause(true); } @@ -196,9 +189,11 @@ pub mod emergency_component { self.pause_timestamp.write(get_block_timestamp()); } - fn _set_ban(ref self: ComponentState, member: ContractAddress, banned: bool) { + fn _set_ban( + ref self: ComponentState, member: ContractAddress, banned: bool, + ) { assert(!self.is_paused.read(), EmergencyComponentErrors::CONTRACT_PAUSED); self.banned_members.write(member, banned); } } -} \ No newline at end of file +} diff --git a/src/component/member_profile.cairo b/src/component/member_profile.cairo index aa19c4b..253720f 100644 --- a/src/component/member_profile.cairo +++ b/src/component/member_profile.cairo @@ -1,398 +1,379 @@ -// // ENTIRE FILE COMMENTED OUT - EMERGENCY SYSTEM ONLY -// // This file is temporarily disabled to focus on emergency system implementation only - -// /* -// use starknet::ContractAddress; - -// #[starknet::interface] -// pub trait IMemberProfile { -// fn create_member_profile(ref self: TContractState, member: ContractAddress); -// fn update_reliability_rating(ref self: TContractState, member: ContractAddress, new_rating: u8); -// fn get_member_profile(self: @TContractState, member: ContractAddress) -> MemberProfile; -// } - -// // Data structures for member profile functionality -// #[derive(Copy, Drop, Serde, starknet::Store)] -// pub struct MemberProfile { -// pub join_date: u64, -// pub total_contributions: u256, -// pub missed_contributions: u8, -// pub credit_score: u8, -// pub last_recipient_round: u256, -// pub reliability_rating: u8, -// pub preferred_payment_method: felt252, -// pub communication_preferences: felt252, -// } - -// #[generate_trait] -// pub impl IMemberProfileInternal of IMemberProfileInternalTrait { -// fn initializer(ref self: ComponentState); -// fn _assert_admin(self: @ComponentState); -// fn _calculate_credit_score(self: @ComponentState, profile: @MemberProfile) -> u8; -// fn _update_contribution_stats(ref self: ComponentState, member: ContractAddress, amount: u256); -// } - -// #[starknet::component] -// pub mod member_profile_component { -// */ -// use core::starknet::{ContractAddress, get_block_timestamp, get_caller_address}; -// use core::starknet::storage::{ -// Map, StoragePointerReadAccess, StoragePointerWriteAccess, -// }; -// use super::MemberProfile; - -// #[derive(Drop)] -// pub enum Errors { -// NOT_ADMIN: (), -// PROFILE_NOT_FOUND: (), -// PROFILE_ALREADY_EXISTS: (), -// INVALID_RATING: (), -// INVALID_PREFERENCES: (), -// } - -// #[storage] -// pub struct Storage { -// member_profiles: Map, -// waitlist: Map, // Index -> Address -// waitlist_length: u32, -// total_members: u32, -// admin: ContractAddress, -// } - -// #[event] -// #[derive(Drop, starknet::Event)] -// pub enum Event { -// ProfileCreated: ProfileCreated, -// ProfileUpdated: ProfileUpdated, -// ReliabilityRatingUpdated: ReliabilityRatingUpdated, -// MemberAddedToWaitlist: MemberAddedToWaitlist, -// MemberRemovedFromWaitlist: MemberRemovedFromWaitlist, -// CommunicationSent: CommunicationSent, -// ContributionRecorded: ContributionRecorded, -// MissedContributionRecorded: MissedContributionRecorded, -// PaymentMethodUpdated: PaymentMethodUpdated, -// } - -// #[derive(Drop, starknet::Event)] -// pub struct ProfileCreated { -// member: ContractAddress, -// timestamp: u64, -// } - -// #[derive(Drop, starknet::Event)] -// pub struct ProfileUpdated { -// member: ContractAddress, -// updated_by: ContractAddress, -// timestamp: u64, -// } - -// #[derive(Drop, starknet::Event)] -// pub struct ReliabilityRatingUpdated { -// member: ContractAddress, -// old_rating: u8, -// new_rating: u8, -// timestamp: u64, -// } - -// #[derive(Drop, starknet::Event)] -// pub struct MemberAddedToWaitlist { -// member: ContractAddress, -// position: u32, -// timestamp: u64, -// } - -// #[derive(Drop, starknet::Event)] -// pub struct MemberRemovedFromWaitlist { -// member: ContractAddress, -// timestamp: u64, -// } - -// #[derive(Drop, starknet::Event)] -// pub struct CommunicationSent { -// message_hash: felt252, -// recipients_count: u32, -// timestamp: u64, -// } - -// #[derive(Drop, starknet::Event)] -// pub struct ContributionRecorded { -// member: ContractAddress, -// amount: u256, -// round_id: u256, -// timestamp: u64, -// } - -// #[derive(Drop, starknet::Event)] -// pub struct MissedContributionRecorded { -// member: ContractAddress, -// round_id: u256, -// timestamp: u64, -// } - -// #[derive(Drop, starknet::Event)] -// pub struct PaymentMethodUpdated { -// member: ContractAddress, -// old_method: felt252, -// new_method: felt252, -// timestamp: u64, -// } - -// impl MemberProfileImpl< -// TContractState, +HasComponent, -// > of super::IMemberProfile> { -// fn create_member_profile(ref self: ComponentState, member: ContractAddress) { -// // Check if profile already exists -// let existing_profile = self.member_profiles.read(member); -// assert(existing_profile.join_date == 0, Errors::PROFILE_ALREADY_EXISTS); - -// let current_time = get_block_timestamp(); -// let new_profile = MemberProfile { -// join_date: current_time, -// total_contributions: 0, -// missed_contributions: 0, -// credit_score: 50, // Start with neutral score -// last_recipient_round: 0, -// reliability_rating: 50, // Start with neutral rating -// preferred_payment_method: 'DEFAULT', -// communication_preferences: 'ALL', -// }; - -// self.member_profiles.write(member, new_profile); -// let total = self.total_members.read(); -// self.total_members.write(total + 1); - -// self.emit(Event::ProfileCreated(ProfileCreated { -// member, -// timestamp: current_time, -// })); -// } - -// fn update_reliability_rating(ref self: ComponentState, member: ContractAddress, new_rating: u8) { -// self._assert_admin(); -// assert(new_rating <= 100, Errors::INVALID_RATING); - -// let mut profile = self.member_profiles.read(member); -// assert(profile.join_date != 0, Errors::PROFILE_NOT_FOUND); - -// let old_rating = profile.reliability_rating; -// profile.reliability_rating = new_rating; -// self.member_profiles.write(member, profile); - -// self.emit(Event::ReliabilityRatingUpdated(ReliabilityRatingUpdated { -// member, -// old_rating, -// new_rating, -// timestamp: get_block_timestamp(), -// })); -// } - -// fn get_member_profile(self: @ComponentState, member: ContractAddress) -> MemberProfile { -// let profile = self.member_profiles.read(member); -// assert(profile.join_date != 0, Errors::PROFILE_NOT_FOUND); -// profile -// } -// } - -// // Additional public functions for enhanced member management -// impl AdditionalMemberProfileImpl< -// TContractState, +HasComponent, -// > of AdditionalMemberProfileTrait { -// fn add_to_waitlist(ref self: ComponentState, member: ContractAddress) { -// let current_length = self.waitlist_length.read(); -// self.waitlist.write(current_length, member); -// self.waitlist_length.write(current_length + 1); - -// self.emit(Event::MemberAddedToWaitlist(MemberAddedToWaitlist { -// member, -// position: current_length + 1, -// timestamp: get_block_timestamp(), -// })); -// } - -// fn remove_from_waitlist(ref self: ComponentState, member: ContractAddress) -> bool { -// let waitlist_length = self.waitlist_length.read(); -// let mut found = false; -// let mut i = 0; - -// // Find member in waitlist -// loop { -// if i >= waitlist_length { -// break; -// } - -// if self.waitlist.read(i) == member { -// found = true; -// // Shift remaining members -// let mut j = i; -// loop { -// if j + 1 >= waitlist_length { -// break; -// } -// let next_member = self.waitlist.read(j + 1); -// self.waitlist.write(j, next_member); -// j += 1; -// }; -// break; -// } -// i += 1; -// }; - -// if found { -// self.waitlist_length.write(waitlist_length - 1); -// self.emit(Event::MemberRemovedFromWaitlist(MemberRemovedFromWaitlist { -// member, -// timestamp: get_block_timestamp(), -// })); -// } - -// found -// } - -// fn record_contribution(ref self: ComponentState, member: ContractAddress, amount: u256, round_id: u256) { -// let mut profile = self.member_profiles.read(member); -// profile.total_contributions += amount; -// profile.credit_score = self._calculate_credit_score(@profile); -// self.member_profiles.write(member, profile); - -// self.emit(Event::ContributionRecorded(ContributionRecorded { -// member, -// amount, -// round_id, -// timestamp: get_block_timestamp(), -// })); -// } - -// fn record_missed_contribution(ref self: ComponentState, member: ContractAddress, round_id: u256) { -// let mut profile = self.member_profiles.read(member); -// profile.missed_contributions += 1; -// profile.credit_score = self._calculate_credit_score(@profile); -// // Decrease reliability rating -// if profile.reliability_rating > 5 { -// profile.reliability_rating -= 5; -// } else { -// profile.reliability_rating = 0; -// } -// self.member_profiles.write(member, profile); - -// self.emit(Event::MissedContributionRecorded(MissedContributionRecorded { -// member, -// round_id, -// timestamp: get_block_timestamp(), -// })); -// } - -// fn update_payment_method(ref self: ComponentState, member: ContractAddress, new_method: felt252) { -// let caller = get_caller_address(); -// assert(caller == member || caller == self.admin.read(), Errors::NOT_ADMIN); - -// let mut profile = self.member_profiles.read(member); -// assert(profile.join_date != 0, Errors::PROFILE_NOT_FOUND); - -// let old_method = profile.preferred_payment_method; -// profile.preferred_payment_method = new_method; -// self.member_profiles.write(member, profile); - -// self.emit(Event::PaymentMethodUpdated(PaymentMethodUpdated { -// member, -// old_method, -// new_method, -// timestamp: get_block_timestamp(), -// })); -// } - -// fn send_communication(ref self: ComponentState, message_hash: felt252, recipients_count: u32) { -// self._assert_admin(); - -// self.emit(Event::CommunicationSent(CommunicationSent { -// message_hash, -// recipients_count, -// timestamp: get_block_timestamp(), -// })); -// } - -// fn get_waitlist_position(self: @ComponentState, member: ContractAddress) -> u32 { -// let waitlist_length = self.waitlist_length.read(); -// let mut i = 0; - -// loop { -// if i >= waitlist_length { -// break 0; // Not found -// } - -// if self.waitlist.read(i) == member { -// break i + 1; // Position is 1-indexed -// } -// i += 1; -// } -// } - -// fn get_total_members(self: @ComponentState) -> u32 { -// self.total_members.read() -// } - -// fn get_waitlist_length(self: @ComponentState) -> u32 { -// self.waitlist_length.read() -// } -// } - -// #[generate_trait] -// pub trait AdditionalMemberProfileTrait { -// fn add_to_waitlist(ref self: ComponentState, member: ContractAddress); -// fn remove_from_waitlist(ref self: ComponentState, member: ContractAddress) -> bool; -// fn record_contribution(ref self: ComponentState, member: ContractAddress, amount: u256, round_id: u256); -// fn record_missed_contribution(ref self: ComponentState, member: ContractAddress, round_id: u256); -// fn update_payment_method(ref self: ComponentState, member: ContractAddress, new_method: felt252); -// fn send_communication(ref self: ComponentState, message_hash: felt252, recipients_count: u32); -// fn get_waitlist_position(self: @ComponentState, member: ContractAddress) -> u32; -// fn get_total_members(self: @ComponentState) -> u32; -// fn get_waitlist_length(self: @ComponentState) -> u32; -// } - -// #[generate_trait] -// pub impl InternalImpl< -// TContractState, +HasComponent, -// > of super::IMemberProfileInternal { -// fn initializer(ref self: ComponentState) { -// self.admin.write(get_caller_address()); -// self.total_members.write(0); -// self.waitlist_length.write(0); -// } - -// fn _assert_admin(self: @ComponentState) { -// let admin: ContractAddress = self.admin.read(); -// let caller: ContractAddress = get_caller_address(); -// assert(caller == admin, Errors::NOT_ADMIN); -// } - -// fn _calculate_credit_score(self: @ComponentState, profile: @MemberProfile) -> u8 { -// let total_contributions = *profile.total_contributions; -// let missed_contributions = *profile.missed_contributions; - -// if total_contributions == 0 && missed_contributions == 0 { -// return 50; // Neutral score for new members -// } - -// // Simple calculation: start with 50, add for successful contributions, subtract for missed -// let base_score = 50; -// let contribution_boost = if total_contributions > 0 { -// let contribution_count = total_contributions / 100; // Assuming each contribution is ~100 units -// if contribution_count > 50 { 50 } else { contribution_count.try_into().unwrap() } -// } else { 0 }; - -// let penalty = missed_contributions * 10; // 10 points per missed contribution - -// let calculated_score = base_score + contribution_boost - penalty.into(); - -// if calculated_score > 100 { 100 } -// else if calculated_score < 0 { 0 } -// else { calculated_score } -// } - -// fn _update_contribution_stats(ref self: ComponentState, member: ContractAddress, amount: u256) { -// let mut profile = self.member_profiles.read(member); -// profile.total_contributions += amount; -// profile.credit_score = self._calculate_credit_score(@profile); -// self.member_profiles.write(member, profile); -// } -// } -// } +use starknet::ContractAddress; +use starkremit_contract::base::types::MemberProfileData; + +// Trait that the main contract must implement to provide data access +pub trait IMainContractData { + fn get_member_status(self: @TContractState, member: ContractAddress) -> bool; + fn get_member_count(self: @TContractState) -> u32; + fn get_member_by_index(self: @TContractState, index: u32) -> ContractAddress; +} + +#[starknet::interface] +pub trait IMemberProfile { + fn create_member_profile(ref self: TContractState, member: ContractAddress); + fn update_reliability_rating(ref self: TContractState, member: ContractAddress, new_rating: u8); + fn get_member_profile(self: @TContractState, member: ContractAddress) -> MemberProfileData; + fn add_to_waitlist(ref self: TContractState, member: ContractAddress); + fn remove_from_waitlist(ref self: TContractState, member: ContractAddress) -> bool; + fn get_waitlist_position(self: @TContractState, member: ContractAddress) -> u32; + fn update_communication_preferences( + ref self: TContractState, member: ContractAddress, preferences: felt252, + ); + fn send_member_message(ref self: TContractState, member: ContractAddress, message: felt252); +} + +#[starknet::component] +pub mod member_profile_component { + use starknet::storage::{ + Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, + StoragePointerWriteAccess, + }; + use starknet::{ContractAddress, get_block_timestamp, get_caller_address}; + use starkremit_contract::base::errors::MemberProfileComponentErrors; + use super::*; + + + #[storage] + #[allow(starknet::invalid_storage_member_types)] + pub struct Storage { + member_profiles: Map, + waitlist: Map, // Index -> Address + waitlist_length: u32, + total_members: u32, + admin: ContractAddress, + member_messages: Map<(ContractAddress, u32), felt252>, // (member, message_index) -> message + message_counts: Map // member -> message_count + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + ProfileCreated: ProfileCreated, + ProfileUpdated: ProfileUpdated, + ReliabilityRatingUpdated: ReliabilityRatingUpdated, + MemberAddedToWaitlist: MemberAddedToWaitlist, + MemberRemovedFromWaitlist: MemberRemovedFromWaitlist, + CommunicationSent: CommunicationSent, + ContributionRecorded: ContributionRecorded, + MissedContributionRecorded: MissedContributionRecorded, + PaymentMethodUpdated: PaymentMethodUpdated, + } + + #[derive(Drop, starknet::Event)] + pub struct ProfileCreated { + member: ContractAddress, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct ProfileUpdated { + member: ContractAddress, + updated_by: ContractAddress, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct ReliabilityRatingUpdated { + member: ContractAddress, + old_rating: u8, + new_rating: u8, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct MemberAddedToWaitlist { + member: ContractAddress, + position: u32, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct MemberRemovedFromWaitlist { + member: ContractAddress, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct CommunicationSent { + message_hash: felt252, + recipients_count: u32, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct ContributionRecorded { + member: ContractAddress, + amount: u256, + round_id: u256, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct MissedContributionRecorded { + member: ContractAddress, + round_id: u256, + timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct PaymentMethodUpdated { + member: ContractAddress, + old_method: felt252, + new_method: felt252, + timestamp: u64, + } + + #[embeddable_as(MemberProfile)] + pub impl MemberProfileImpl< + TContractState, +HasComponent, +IMainContractData, + > of IMemberProfile> { + fn create_member_profile( + ref self: ComponentState, member: ContractAddress, + ) { + // Check if profile already exists + let existing_profile = self.member_profiles.read(member); + assert( + existing_profile.join_date == 0, + MemberProfileComponentErrors::PROFILE_ALREADY_EXISTS, + ); + + let current_time = get_block_timestamp(); + let new_profile = MemberProfileData { + join_date: current_time, + total_contributions: 0, + missed_contributions: 0, + credit_score: 50, // Start with neutral score + last_recipient_round: 0, + reliability_rating: 50, // Start with neutral rating + preferred_payment_method: 'DEFAULT', + communication_preferences: 'ALL', + is_on_waitlist: false, + waitlist_position: 0, + last_message_timestamp: 0, + }; + + self.member_profiles.write(member, new_profile); + let total = self.total_members.read(); + self.total_members.write(total + 1); + + self.emit(Event::ProfileCreated(ProfileCreated { member, timestamp: current_time })); + } + + fn update_reliability_rating( + ref self: ComponentState, member: ContractAddress, new_rating: u8, + ) { + self._assert_admin(); + assert(new_rating <= 100, MemberProfileComponentErrors::INVALID_RATING); + + let mut profile = self.member_profiles.read(member); + assert(profile.join_date != 0, MemberProfileComponentErrors::PROFILE_NOT_FOUND); + + let old_rating = profile.reliability_rating; + profile.reliability_rating = new_rating; + self.member_profiles.write(member, profile); + + self + .emit( + Event::ReliabilityRatingUpdated( + ReliabilityRatingUpdated { + member, old_rating, new_rating, timestamp: get_block_timestamp(), + }, + ), + ); + } + + fn get_member_profile( + self: @ComponentState, member: ContractAddress, + ) -> MemberProfileData { + let profile = self.member_profiles.read(member); + assert(profile.join_date != 0, MemberProfileComponentErrors::PROFILE_NOT_FOUND); + profile + } + + fn add_to_waitlist(ref self: ComponentState, member: ContractAddress) { + let current_length = self.waitlist_length.read(); + self.waitlist.write(current_length, member); + self.waitlist_length.write(current_length + 1); + + // Update member profile + let mut profile = self.member_profiles.read(member); + profile.is_on_waitlist = true; + profile.waitlist_position = current_length + 1; + self.member_profiles.write(member, profile); + + self + .emit( + Event::MemberAddedToWaitlist( + MemberAddedToWaitlist { + member, position: current_length + 1, timestamp: get_block_timestamp(), + }, + ), + ); + } + + fn remove_from_waitlist( + ref self: ComponentState, member: ContractAddress, + ) -> bool { + let waitlist_length = self.waitlist_length.read(); + let mut found = false; + let mut i = 0; + + // Find member in waitlist + loop { + if i >= waitlist_length { + break; + } + + if self.waitlist.read(i) == member { + found = true; + // Shift remaining members + let mut j = i; + loop { + if j + 1 >= waitlist_length { + break; + } + let next_member = self.waitlist.read(j + 1); + self.waitlist.write(j, next_member); + j += 1; + } + // Update waitlist_position for shifted members' profiles + let mut k = i; + loop { + if k + 1 > waitlist_length { + break; + } + // Only indices up to waitlist_length - 2 are valid after shift + if k >= waitlist_length - 1 { + break; + } + let shifted_member = self.waitlist.read(k); + let mut shifted_profile = self.member_profiles.read(shifted_member); + shifted_profile.waitlist_position = k + 1; + self.member_profiles.write(shifted_member, shifted_profile); + k += 1; + } + break; + } + i += 1; + } + + if found { + self.waitlist_length.write(waitlist_length - 1); + + // Update member profile + let mut profile = self.member_profiles.read(member); + profile.is_on_waitlist = false; + profile.waitlist_position = 0; + self.member_profiles.write(member, profile); + + self + .emit( + Event::MemberRemovedFromWaitlist( + MemberRemovedFromWaitlist { member, timestamp: get_block_timestamp() }, + ), + ); + } + + found + } + + fn get_waitlist_position( + self: @ComponentState, member: ContractAddress, + ) -> u32 { + let profile = self.member_profiles.read(member); + if profile.is_on_waitlist { + profile.waitlist_position + } else { + 0 + } + } + + fn update_communication_preferences( + ref self: ComponentState, member: ContractAddress, preferences: felt252, + ) { + let mut profile = self.member_profiles.read(member); + assert(profile.join_date != 0, MemberProfileComponentErrors::PROFILE_NOT_FOUND); + + let old_preferences = profile.communication_preferences; + profile.communication_preferences = preferences; + self.member_profiles.write(member, profile); + + self + .emit( + Event::CommunicationSent( + CommunicationSent { + message_hash: preferences, + recipients_count: 1, + timestamp: get_block_timestamp(), + }, + ), + ); + } + + fn send_member_message( + ref self: ComponentState, member: ContractAddress, message: felt252, + ) { + let mut profile = self.member_profiles.read(member); + assert(profile.join_date != 0, MemberProfileComponentErrors::PROFILE_NOT_FOUND); + + // Store message + let message_count = self.message_counts.read(member); + self.member_messages.write((member, message_count), message); + self.message_counts.write(member, message_count + 1); + + // Update last message timestamp + profile.last_message_timestamp = get_block_timestamp(); + self.member_profiles.write(member, profile); + + self + .emit( + Event::CommunicationSent( + CommunicationSent { + message_hash: message, + recipients_count: 1, + timestamp: get_block_timestamp(), + }, + ), + ); + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, +HasComponent, +IMainContractData, + > of InternalTrait { + fn initializer(ref self: ComponentState, admin: ContractAddress) { + self.admin.write(admin); + self.total_members.write(0); + self.waitlist_length.write(0); + } + + fn _assert_admin(self: @ComponentState) { + let admin = self.admin.read(); + assert(get_caller_address() == admin, MemberProfileComponentErrors::NOT_ADMIN); + } + + fn _calculate_credit_score( + self: @ComponentState, profile: @MemberProfileData, + ) -> u8 { + // Simple credit score calculation based on contributions vs missed + let total_rounds: u256 = *profile.total_contributions + + (*profile.missed_contributions).into(); + if total_rounds == 0 { + return 50; // Neutral score for new members + } + + let success_rate: u256 = (*profile.total_contributions * 100_u256) / total_rounds; + success_rate.try_into().unwrap_or(50) + } + + fn _update_contribution_stats( + ref self: ComponentState, member: ContractAddress, amount: u256, + ) { + let mut profile = self.member_profiles.read(member); + profile.total_contributions += amount; + profile.credit_score = self._calculate_credit_score(@profile); + self.member_profiles.write(member, profile); + } + } +} diff --git a/src/component/payment_flexibility.cairo b/src/component/payment_flexibility.cairo index 63a1ece..57363cd 100644 --- a/src/component/payment_flexibility.cairo +++ b/src/component/payment_flexibility.cairo @@ -1,6 +1,8 @@ use starknet::ContractAddress; -use starkremit_contract::base::types::{RoundStatus, RoundData}; -use starkremit_contract::base::errors::PaymentFlexibilityErrors; +use starkremit_contract::base::types::{ + AutoPaymentSetup, EarlyPaymentInfo, PaymentConfig, PaymentFrequency, PaymentRecord, + PaymentStatus, RoundData, RoundStatus, +}; // Trait that the main contract must implement to provide data access pub trait IMainContractData { @@ -15,88 +17,27 @@ pub trait IPaymentFlexibility { // Configuration and query functions (simple operations) fn get_payment_config(self: @TContractState) -> PaymentConfig; fn get_auto_payment_setup(self: @TContractState, member: ContractAddress) -> AutoPaymentSetup; - fn get_payment_status(self: @TContractState, member: ContractAddress, round_id: u256) -> PaymentStatus; + fn get_payment_status( + self: @TContractState, member: ContractAddress, round_id: u256, + ) -> PaymentStatus; fn get_supported_tokens(self: @TContractState) -> Array; fn is_token_supported(self: @TContractState, token: ContractAddress) -> bool; - + // Utility functions (simple operations) fn get_grace_period_extension(self: @TContractState, member: ContractAddress) -> u64; fn get_early_payment_discount(self: @TContractState, amount: u256) -> u256; } -// Data structures for payment flexibility functionality -#[derive(Copy, Drop, Serde, starknet::Store)] -pub struct PaymentConfig { - pub grace_period_hours: u64, - pub early_payment_discount_basis_points: u256, // E.g., 500 for 5% - pub auto_payment_enabled: bool, - pub usd_oracle_address: ContractAddress, - pub max_grace_period_extension: u64, // Maximum extension in hours - pub min_early_payment_days: u64, // Minimum days before deadline for early payment -} - -#[derive(Copy, Drop, Serde, starknet::Store)] -pub enum PaymentFrequency { - Once, - Daily, - Weekly, - Monthly, -} - -#[derive(Copy, Drop, Serde, starknet::Store)] -pub struct AutoPaymentSetup { - pub member: ContractAddress, - pub token: ContractAddress, - pub amount: u256, - pub frequency: PaymentFrequency, - pub next_payment_date: u64, - pub is_active: bool, - pub created_at: u64, - pub last_payment_date: u64, -} - -#[derive(Copy, Drop, Serde, starknet::Store)] -pub enum PaymentStatus { - Pending, - Paid, - Late, - Missed, - Overpaid, - Early, -} - -#[derive(Copy, Drop, Serde, starknet::Store)] -pub struct PaymentRecord { - pub member: ContractAddress, - pub round_id: u256, - pub amount: u256, - pub token: ContractAddress, - pub payment_date: u64, - pub status: PaymentStatus, - pub is_early_payment: bool, - pub discount_applied: u256, - pub grace_period_used: u64, -} - -#[derive(Copy, Drop, Serde, starknet::Store)] -pub struct EarlyPaymentInfo { - pub member: ContractAddress, - pub round_id: u256, - pub original_amount: u256, - pub discount_amount: u256, - pub final_amount: u256, - pub payment_date: u64, -} #[starknet::component] pub mod payment_flexibility_component { - use starknet::{ContractAddress, get_block_timestamp, get_caller_address}; - use starknet::storage::{ - Map, StoragePointerReadAccess, StoragePointerWriteAccess, StorageMapReadAccess, StorageMapWriteAccess, - }; use core::array::ArrayTrait; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; - use super::{PaymentConfig, PaymentFrequency, AutoPaymentSetup, PaymentStatus, PaymentRecord, EarlyPaymentInfo, IMainContractData}; + use starknet::storage::{ + Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, + StoragePointerWriteAccess, + }; + use starknet::{ContractAddress, get_block_timestamp, get_caller_address, get_contract_address}; use starkremit_contract::base::errors::PaymentFlexibilityErrors; use super::*; @@ -108,14 +49,16 @@ pub mod payment_flexibility_component { pub struct Storage { payment_config: PaymentConfig, auto_payment_setups: Map, - payment_records: Map<(ContractAddress, u256), PaymentRecord>, // (member, round_id) -> record + payment_records: Map< + (ContractAddress, u256), PaymentRecord, + >, // (member, round_id) -> record supported_tokens: Map, // Index -> Token supported_tokens_count: u32, early_payments: Map, // round_id -> early payment info grace_period_extensions: Map, // member -> extension hours admin: ContractAddress, last_auto_payment_processing: u64, - auto_payment_interval: u64, // How often to process auto-payments + auto_payment_interval: u64 // How often to process auto-payments } #[event] @@ -127,6 +70,7 @@ pub mod payment_flexibility_component { TokenValueConverted: TokenValueConverted, PaymentStatusUpdated: PaymentStatusUpdated, AutoPaymentExecuted: AutoPaymentExecuted, + BulkAutoPaymentsProcessed: BulkAutoPaymentsProcessed, SupportedTokenAdded: SupportedTokenAdded, SupportedTokenRemoved: SupportedTokenRemoved, PaymentConfigUpdated: PaymentConfigUpdated, @@ -186,6 +130,12 @@ pub mod payment_flexibility_component { timestamp: u64, } + #[derive(Drop, starknet::Event)] + pub struct BulkAutoPaymentsProcessed { + processed_count: u32, + timestamp: u64, + } + #[derive(Drop, starknet::Event)] pub struct SupportedTokenAdded { token: ContractAddress, @@ -213,19 +163,22 @@ pub mod payment_flexibility_component { } #[embeddable_as(PaymentFlexibility)] - impl PaymentFlexibilityImpl< + pub impl PaymentFlexibilityImpl< TContractState, +HasComponent, +IMainContractData, > of super::IPaymentFlexibility> { - fn get_payment_config(self: @ComponentState) -> PaymentConfig { self.payment_config.read() } - fn get_auto_payment_setup(self: @ComponentState, member: ContractAddress) -> AutoPaymentSetup { + fn get_auto_payment_setup( + self: @ComponentState, member: ContractAddress, + ) -> AutoPaymentSetup { self.auto_payment_setups.read(member) } - fn get_payment_status(self: @ComponentState, member: ContractAddress, round_id: u256) -> PaymentStatus { + fn get_payment_status( + self: @ComponentState, member: ContractAddress, round_id: u256, + ) -> PaymentStatus { self._calculate_payment_status(member, round_id) } @@ -241,11 +194,15 @@ pub mod payment_flexibility_component { tokens } - fn is_token_supported(self: @ComponentState, token: ContractAddress) -> bool { + fn is_token_supported( + self: @ComponentState, token: ContractAddress, + ) -> bool { self._is_token_supported(token) } - fn get_grace_period_extension(self: @ComponentState, member: ContractAddress) -> u64 { + fn get_grace_period_extension( + self: @ComponentState, member: ContractAddress, + ) -> u64 { self.grace_period_extensions.read(member) } @@ -259,10 +216,9 @@ pub mod payment_flexibility_component { pub impl InternalImpl< TContractState, +HasComponent, +IMainContractData, > of InternalTrait { - fn initializer(ref self: ComponentState, admin: ContractAddress) { self.admin.write(admin); - + // Set default payment configuration let default_config = PaymentConfig { grace_period_hours: 48, // 48 hours default grace period @@ -270,14 +226,14 @@ pub mod payment_flexibility_component { auto_payment_enabled: true, usd_oracle_address: 0.try_into().unwrap(), // No oracle by default max_grace_period_extension: 168, // Maximum 1 week extension - min_early_payment_days: 7, // Minimum 7 days before deadline for early payment + min_early_payment_days: 7 // Minimum 7 days before deadline for early payment }; self.payment_config.write(default_config); - + // Initialize auto-payment processing self.last_auto_payment_processing.write(get_block_timestamp()); self.auto_payment_interval.write(3600); // Process every hour - + // Initialize supported tokens count self.supported_tokens_count.write(0); } @@ -301,14 +257,14 @@ pub mod payment_flexibility_component { assert(config.auto_payment_enabled, PaymentFlexibilityErrors::AUTO_PAYMENT_DISABLED); assert(amount > 0, PaymentFlexibilityErrors::INVALID_AMOUNT); assert(self._is_token_supported(token), PaymentFlexibilityErrors::INVALID_TOKEN); - + // Check if member already has auto-payment setup let existing_setup = self.auto_payment_setups.read(member); assert(!existing_setup.is_active, PaymentFlexibilityErrors::AUTO_PAYMENT_ACTIVE); - + // Calculate next payment date based on frequency let next_payment_date = self._calculate_next_payment_date(frequency); - + // Create auto-payment setup let auto_payment = AutoPaymentSetup { member, @@ -320,16 +276,17 @@ pub mod payment_flexibility_component { created_at: get_block_timestamp(), last_payment_date: 0, }; - + self.auto_payment_setups.write(member, auto_payment); - - self.emit(Event::AutoPaymentSetup(AutoPaymentSetupEvent { - member, - token, - amount, - frequency, - timestamp: get_block_timestamp(), - })); + + self + .emit( + Event::AutoPaymentSetup( + AutoPaymentSetupEvent { + member, token, amount, frequency, timestamp: get_block_timestamp(), + }, + ), + ); } fn _process_early_payment( @@ -337,23 +294,29 @@ pub mod payment_flexibility_component { member: ContractAddress, round_id: u256, amount: u256, + token: ContractAddress, ) -> (u256, u256) { let config = self.payment_config.read(); let contract_state = self.get_contract(); let round = contract_state.get_round_data(round_id); - + // Validate round is active assert(round.status == RoundStatus::Active, PaymentFlexibilityErrors::ROUND_NOT_FOUND); - + // Validate token is non-zero (retain actual token info) + assert(token != 0.try_into().unwrap(), PaymentFlexibilityErrors::INVALID_TOKEN); + // Check if payment is early enough to qualify for discount let current_time = get_block_timestamp(); let days_until_deadline = (round.deadline - current_time) / SECONDS_PER_DAY; - assert(days_until_deadline >= config.min_early_payment_days, PaymentFlexibilityErrors::INVALID_AMOUNT); - + assert( + days_until_deadline >= config.min_early_payment_days, + PaymentFlexibilityErrors::INVALID_AMOUNT, + ); + // Calculate early payment discount let discount_amount = self._calculate_early_payment_discount(amount); let final_amount = amount - discount_amount; - + // Store early payment info let early_payment = EarlyPaymentInfo { member, @@ -364,13 +327,13 @@ pub mod payment_flexibility_component { payment_date: current_time, }; self.early_payments.write(round_id, early_payment); - + // Update payment record let payment_record = PaymentRecord { member, round_id, amount: final_amount, - token: 0.try_into().unwrap(), // Default token + token, payment_date: current_time, status: PaymentStatus::Early, is_early_payment: true, @@ -378,43 +341,55 @@ pub mod payment_flexibility_component { grace_period_used: 0, }; self.payment_records.write((member, round_id), payment_record); - - self.emit(Event::EarlyPaymentProcessed(EarlyPaymentProcessed { - member, - round_id, - original_amount: amount, - discount_amount, - final_amount, - timestamp: current_time, - })); - + + self + .emit( + Event::EarlyPaymentProcessed( + EarlyPaymentProcessed { + member, + round_id, + original_amount: amount, + discount_amount, + final_amount, + timestamp: current_time, + }, + ), + ); + (final_amount, discount_amount) } fn _extend_grace_period( - ref self: ComponentState, - member: ContractAddress, - extension_hours: u64, + ref self: ComponentState, member: ContractAddress, extension_hours: u64, ) { self._assert_admin(); - + let config = self.payment_config.read(); assert(extension_hours > 0, PaymentFlexibilityErrors::INVALID_AMOUNT); - assert(extension_hours <= config.max_grace_period_extension, PaymentFlexibilityErrors::INVALID_AMOUNT); - + assert( + extension_hours <= config.max_grace_period_extension, + PaymentFlexibilityErrors::INVALID_AMOUNT, + ); + // Get current grace period extension let current_extension = self.grace_period_extensions.read(member); let new_extension = current_extension + extension_hours; - + // Update grace period extension self.grace_period_extensions.write(member, new_extension); - - self.emit(Event::GracePeriodExtended(GracePeriodExtended { - member, - extension_hours, - new_deadline: get_block_timestamp() + (new_extension * SECONDS_PER_HOUR), - timestamp: get_block_timestamp(), - })); + + self + .emit( + Event::GracePeriodExtended( + GracePeriodExtended { + member, + extension_hours, + new_deadline: get_block_timestamp() + + (new_extension * SECONDS_PER_HOUR), + timestamp: get_block_timestamp(), + }, + ), + ); } fn _process_auto_payments(ref self: ComponentState) { @@ -422,25 +397,25 @@ pub mod payment_flexibility_component { if !config.auto_payment_enabled { return; } - + let current_time = get_block_timestamp(); let last_processing = self.last_auto_payment_processing.read(); let interval = self.auto_payment_interval.read(); - + // Check if it's time to process auto-payments if current_time < last_processing + interval { return; } - + let mut processed_count = 0; let member_count = self._get_member_count(); - + // Process auto-payments for all members let mut i = 0; while i < member_count { let member = self._get_member_by_index(i); let auto_setup = self.auto_payment_setups.read(member); - + if auto_setup.is_active && current_time >= auto_setup.next_payment_date { // Execute auto-payment self._execute_auto_payment(member, auto_setup); @@ -448,91 +423,108 @@ pub mod payment_flexibility_component { } i += 1; } - + // Update last processing timestamp self.last_auto_payment_processing.write(current_time); - + if processed_count > 0_u32 { - self.emit(Event::AutoPaymentExecuted(AutoPaymentExecuted { - member: 0.try_into().unwrap(), // Not applicable for bulk processing - amount: 0, // Not applicable for bulk processing - token: 0.try_into().unwrap(), // Not applicable for bulk processing - timestamp: current_time, - })); + self + .emit( + Event::BulkAutoPaymentsProcessed( + BulkAutoPaymentsProcessed { processed_count, timestamp: current_time }, + ), + ); } } fn _add_supported_token(ref self: ComponentState, token: ContractAddress) { self._assert_admin(); - + // Check if token is already supported assert(!self._is_token_supported(token), PaymentFlexibilityErrors::INVALID_TOKEN); - + let count = self.supported_tokens_count.read(); self.supported_tokens.write(count, token); self.supported_tokens_count.write(count + 1); - - self.emit(Event::SupportedTokenAdded(SupportedTokenAdded { - token, - timestamp: get_block_timestamp(), - })); + + self + .emit( + Event::SupportedTokenAdded( + SupportedTokenAdded { token, timestamp: get_block_timestamp() }, + ), + ); } - fn _remove_supported_token(ref self: ComponentState, token: ContractAddress) { + fn _remove_supported_token( + ref self: ComponentState, token: ContractAddress, + ) { self._assert_admin(); - + // Check if token is supported assert(self._is_token_supported(token), PaymentFlexibilityErrors::INVALID_TOKEN); - - // Find and remove token + + // Find token index, swap with last element, and decrement count to keep array compact let count = self.supported_tokens_count.read(); let mut i = 0; while i < count { let stored_token = self.supported_tokens.read(i); if stored_token == token { - // Remove by setting to zero address - self.supported_tokens.write(i, 0.try_into().unwrap()); + let last_index = count - 1_u32; + if i != last_index { + let last_token = self.supported_tokens.read(last_index); + self.supported_tokens.write(i, last_token); + } + // Clear last slot and update count + self.supported_tokens.write(last_index, 0.try_into().unwrap()); + self.supported_tokens_count.write(last_index); break; } i += 1; } - - self.emit(Event::SupportedTokenRemoved(SupportedTokenRemoved { - token, - timestamp: get_block_timestamp(), - })); + + self + .emit( + Event::SupportedTokenRemoved( + SupportedTokenRemoved { token, timestamp: get_block_timestamp() }, + ), + ); } - fn _update_payment_config(ref self: ComponentState, new_config: PaymentConfig) { + fn _update_payment_config( + ref self: ComponentState, new_config: PaymentConfig, + ) { self._assert_admin(); - + let old_config = self.payment_config.read(); self.payment_config.write(new_config); - - self.emit(Event::PaymentConfigUpdated(PaymentConfigUpdated { - admin: get_caller_address(), - timestamp: get_block_timestamp(), - })); + + self + .emit( + Event::PaymentConfigUpdated( + PaymentConfigUpdated { + admin: get_caller_address(), timestamp: get_block_timestamp(), + }, + ), + ); } // Helper functions fn _calculate_payment_status( - self: @ComponentState, - member: ContractAddress, - round_id: u256, + self: @ComponentState, member: ContractAddress, round_id: u256, ) -> PaymentStatus { let contract_state = self.get_contract(); let round = contract_state.get_round_data(round_id); let payment_record = self.payment_records.read((member, round_id)); let current_time = get_block_timestamp(); let config = self.payment_config.read(); - + if payment_record.amount == 0 { // No payment made - let grace_period_end = round.deadline + (config.grace_period_hours * SECONDS_PER_HOUR); + let grace_period_end = round.deadline + + (config.grace_period_hours * SECONDS_PER_HOUR); let member_extension = self.grace_period_extensions.read(member); let extended_deadline = grace_period_end + (member_extension * SECONDS_PER_HOUR); - + if current_time > extended_deadline { return PaymentStatus::Missed; } else if current_time > round.deadline { @@ -546,17 +538,20 @@ pub mod payment_flexibility_component { return PaymentStatus::Early; } else if current_time <= round.deadline { return PaymentStatus::Paid; - } else if current_time <= round.deadline + (config.grace_period_hours * SECONDS_PER_HOUR) { + } else if current_time <= round.deadline + + (config.grace_period_hours * SECONDS_PER_HOUR) { return PaymentStatus::Late; } else { - return PaymentStatus::Overpaid; // Payment made after grace period + return PaymentStatus::PaidAfterGrace; // Payment made after grace period } } } - fn _calculate_next_payment_date(self: @ComponentState, frequency: PaymentFrequency) -> u64 { + fn _calculate_next_payment_date( + self: @ComponentState, frequency: PaymentFrequency, + ) -> u64 { let current_time = get_block_timestamp(); - + match frequency { PaymentFrequency::Once => current_time, PaymentFrequency::Daily => current_time + SECONDS_PER_DAY, @@ -565,12 +560,16 @@ pub mod payment_flexibility_component { } } - fn _calculate_early_payment_discount(self: @ComponentState, amount: u256) -> u256 { + fn _calculate_early_payment_discount( + self: @ComponentState, amount: u256, + ) -> u256 { let config = self.payment_config.read(); (amount * config.early_payment_discount_basis_points) / BASIS_POINTS } - fn _is_token_supported(self: @ComponentState, token: ContractAddress) -> bool { + fn _is_token_supported( + self: @ComponentState, token: ContractAddress, + ) -> bool { let count = self.supported_tokens_count.read(); let mut i = 0; while i < count { @@ -588,22 +587,30 @@ pub mod payment_flexibility_component { member: ContractAddress, mut auto_setup: AutoPaymentSetup, ) { + // Execute the actual payment transfer from the member to this contract + let token_dispatcher = IERC20Dispatcher { contract_address: auto_setup.token }; + token_dispatcher.transfer_from(member, get_contract_address(), auto_setup.amount); // Update last payment date auto_setup.last_payment_date = get_block_timestamp(); - + // Calculate next payment date auto_setup.next_payment_date = self._calculate_next_payment_date(auto_setup.frequency); - + // Save updated auto-payment setup self.auto_payment_setups.write(member, auto_setup); - + // Emit auto-payment executed event - self.emit(Event::AutoPaymentExecuted(AutoPaymentExecuted { - member, - amount: auto_setup.amount, - token: auto_setup.token, - timestamp: get_block_timestamp(), - })); + self + .emit( + Event::AutoPaymentExecuted( + AutoPaymentExecuted { + member, + amount: auto_setup.amount, + token: auto_setup.token, + timestamp: get_block_timestamp(), + }, + ), + ); } fn _get_member_count(self: @ComponentState) -> u32 { @@ -611,7 +618,9 @@ pub mod payment_flexibility_component { contract_state.get_member_count() } - fn _get_member_by_index(self: @ComponentState, index: u32) -> ContractAddress { + fn _get_member_by_index( + self: @ComponentState, index: u32, + ) -> ContractAddress { let contract_state = self.get_contract(); contract_state.get_member_by_index(index) } diff --git a/src/component/penalty.cairo b/src/component/penalty.cairo index 2a1f3e2..ecac371 100644 --- a/src/component/penalty.cairo +++ b/src/component/penalty.cairo @@ -1,13 +1,16 @@ -use starknet::ContractAddress; -use starknet::get_block_timestamp; -use starknet::get_caller_address; -use core::array::{ArrayTrait, Array}; +use core::array::{Array, ArrayTrait}; use core::serde::Serde; -use starkremit_contract::base::types::{PenaltyConfig, MemberPenaltyRecord, MemberContribution, RoundStatus, PenaltyEventRecord, DistributionData, MemberShare, PenaltyEventType, RoundData}; +use starknet::{ContractAddress, get_block_timestamp, get_caller_address}; +use starkremit_contract::base::types::{ + DistributionData, MemberContribution, MemberPenaltyRecord, MemberShare, PenaltyConfig, + PenaltyEventRecord, PenaltyEventType, RoundData, RoundStatus, +}; // Trait that the main contract must implement to provide data access pub trait IMainContractData { - fn get_member_contribution_data(self: @TContractState, round_id: u256, member: ContractAddress) -> MemberContribution; + fn get_member_contribution_data( + self: @TContractState, round_id: u256, member: ContractAddress, + ) -> MemberContribution; fn get_round_data(self: @TContractState, round_id: u256) -> RoundData; fn get_member_status(self: @TContractState, member: ContractAddress) -> bool; fn get_member_count(self: @TContractState) -> u32; @@ -20,14 +23,18 @@ pub trait IMainContractData { pub trait IPenalty { fn set_penalty_config(ref self: TContractState, config: PenaltyConfig); fn get_penalty_config(self: @TContractState) -> PenaltyConfig; - fn get_member_penalty_record(self: @TContractState, member: ContractAddress) -> MemberPenaltyRecord; + fn get_member_penalty_record( + self: @TContractState, member: ContractAddress, + ) -> MemberPenaltyRecord; fn get_penalty_pool(self: @TContractState) -> u256; // Distribution calculation function fn calculate_distribution_data(self: @TContractState) -> DistributionData; // Reset penalty pool after distribution fn reset_penalty_pool(ref self: TContractState); // History functions - fn get_penalty_history(self: @TContractState, member: ContractAddress, limit: u32, offset: u32) -> Array; + fn get_penalty_history( + self: @TContractState, member: ContractAddress, limit: u32, offset: u32, + ) -> Array; } @@ -85,13 +92,14 @@ pub struct GracePeriodExtended { #[starknet::component] pub mod penalty_component { - use super::*; + use core::array::ArrayTrait; use starknet::storage::{ - Map, StoragePointerReadAccess, StoragePointerWriteAccess, StorageMapReadAccess, StorageMapWriteAccess, + Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, + StoragePointerWriteAccess, }; - use core::array::ArrayTrait; use starkremit_contract::base::errors::PenaltyComponentErrors; - + use super::*; + #[storage] pub struct Storage { penalty_config: PenaltyConfig, @@ -102,7 +110,7 @@ pub mod penalty_component { grace_period_extensions: Map, admin: ContractAddress, } - + #[event] #[derive(Drop, starknet::Event)] pub enum Event { @@ -113,38 +121,44 @@ pub mod penalty_component { PenaltyPoolDistributed: PenaltyPoolDistributed, GracePeriodExtended: GracePeriodExtended, } - + #[embeddable_as(Penalty)] impl PenaltyImpl< TContractState, +HasComponent, +IMainContractData, > of super::IPenalty> { - fn set_penalty_config(ref self: ComponentState, config: PenaltyConfig) { self._assert_admin(); - + let old_config = self.penalty_config.read(); self.penalty_config.write(config); - - self.emit(Event::PenaltyConfigUpdated(PenaltyConfigUpdated { - old_config, - new_config: config, - updated_by: get_caller_address(), - timestamp: get_block_timestamp(), - })); + + self + .emit( + Event::PenaltyConfigUpdated( + PenaltyConfigUpdated { + old_config, + new_config: config, + updated_by: get_caller_address(), + timestamp: get_block_timestamp(), + }, + ), + ); } - + fn get_penalty_config(self: @ComponentState) -> PenaltyConfig { self.penalty_config.read() } - - fn get_member_penalty_record(self: @ComponentState, member: ContractAddress) -> MemberPenaltyRecord { + + fn get_member_penalty_record( + self: @ComponentState, member: ContractAddress, + ) -> MemberPenaltyRecord { self.member_penalties.read(member) } - + fn get_penalty_pool(self: @ComponentState) -> u256 { self.penalty_pool.read() } - + fn reset_penalty_pool(ref self: ComponentState) { self._reset_penalty_pool(); } @@ -152,7 +166,7 @@ pub mod penalty_component { fn calculate_distribution_data(self: @ComponentState) -> DistributionData { let contract_state = self.get_contract(); let penalty_pool_amount = self.penalty_pool.read(); - + if penalty_pool_amount == 0 { return DistributionData { total_amount: 0, @@ -160,14 +174,14 @@ pub mod penalty_component { total_compliant_contributions: 0, }; } - + let mut total_compliant_contributions = 0; let mut member_shares = ArrayTrait::new(); - + // Calculate total contributions from compliant members let mut member_index = 0; let total_members = contract_state.get_member_count(); - + while member_index < total_members { let member_address = contract_state.get_member_by_index(member_index); if contract_state.get_member_status(member_address) { @@ -177,82 +191,101 @@ pub mod penalty_component { let mut member_contribution = 0; let mut round_id = 1; while round_id <= contract_state.get_round_ids() { - let contribution = contract_state.get_member_contribution_data(round_id, member_address); + let contribution = contract_state + .get_member_contribution_data(round_id, member_address); member_contribution += contribution.amount; round_id += 1; } - + total_compliant_contributions += member_contribution; - + if member_contribution > 0 { - let share = (member_contribution * penalty_pool_amount) / total_compliant_contributions; - if share > 0 { - member_shares.append(MemberShare { - member: member_address, - share, - contribution: member_contribution, - }); - } + member_shares + .append( + MemberShare { + member: member_address, + share: 0, + contribution: member_contribution, + }, + ); } } } member_index += 1; } - + + // Compute final shares after total_compliant_contributions is known + let mut computed_member_shares = ArrayTrait::new(); + let shares_span = member_shares.span(); + for member_share in shares_span { + let mut computed_share: u256 = 0; + if total_compliant_contributions > 0 { + computed_share = (*member_share.contribution * penalty_pool_amount) + / total_compliant_contributions; + } + if computed_share > 0 { + computed_member_shares + .append( + MemberShare { + member: *member_share.member, + share: computed_share, + contribution: *member_share.contribution, + }, + ); + } + } + member_shares = computed_member_shares; + + let final_total_contributions: u256 = total_compliant_contributions; DistributionData { total_amount: penalty_pool_amount, member_shares, - total_compliant_contributions, + total_compliant_contributions: final_total_contributions, } } - + fn get_penalty_history( - self: @ComponentState, - member: ContractAddress, - limit: u32, - offset: u32 + self: @ComponentState, member: ContractAddress, limit: u32, offset: u32, ) -> Array { let mut history = ArrayTrait::new(); let total_count = self.penalty_history_count.read(member); - + let mut i = offset; let mut count = 0; - + while i < total_count && count < limit { let event = self.penalty_history.read((member, i)); history.append(event); count += 1; i += 1; } - + history } } - - + #[generate_trait] pub impl InternalImpl< TContractState, +HasComponent, +IMainContractData, > of InternalTrait { - fn initializer(ref self: ComponentState, admin: ContractAddress) { self.admin.write(admin); - + // Set default penalty configuration let default_config = PenaltyConfig { late_fee_percentage: 250, // 2.5% in basis points - grace_period_hours: 48, // 48 hours - max_strikes: 2, // 2 strikes before ban + grace_period_hours: 48, // 48 hours + max_strikes: 2, // 2 strikes before ban security_deposit_multiplier: 100000000000000000000, // 100 tokens penalty_pool_enabled: true, }; self.penalty_config.write(default_config); - + // Initialize penalty pool self.penalty_pool.write(0); } - + fn _assert_admin(self: @ComponentState) { let admin = self.admin.read(); assert(get_caller_address() == admin, PenaltyComponentErrors::NOT_ADMIN); @@ -260,163 +293,169 @@ pub mod penalty_component { // Core penalty functions that need access to main contract data fn apply_late_fee( - ref self: ComponentState, - member: ContractAddress, - round_id: u256 + ref self: ComponentState, member: ContractAddress, round_id: u256, ) { // Get main contract state to access member contributions and rounds let contract_state = self.get_contract(); - + // Get penalty configuration and round data let penalty_config = self.penalty_config.read(); let round = contract_state.get_round_data(round_id); let member_ext: u64 = self.grace_period_extensions.read(member); let total_grace_secs: u64 = penalty_config.grace_period_hours * 3600 + member_ext; - assert(get_block_timestamp() > round.deadline + total_grace_secs, PenaltyComponentErrors::NOT_LATE); + assert( + get_block_timestamp() > round.deadline + total_grace_secs, + PenaltyComponentErrors::NOT_LATE, + ); // If pool is disabled, do not collect late fees into pool - assert(penalty_config.penalty_pool_enabled, PenaltyComponentErrors::PENALTY_POOL_DISABLED); - + assert( + penalty_config.penalty_pool_enabled, PenaltyComponentErrors::PENALTY_POOL_DISABLED, + ); + // Get member contribution from main contract storage via trait let contribution = contract_state.get_member_contribution_data(round_id, member); assert(contribution.amount > 0, PenaltyComponentErrors::NO_CONTRIBUTION_FOR_ROUND); - + // Calculate late fee let late_fee = (contribution.amount * penalty_config.late_fee_percentage) / 10000; - + // Update penalty pool self._update_penalty_pool(late_fee); - + // Update member penalty record let mut penalty_record = self.member_penalties.read(member); penalty_record.total_penalties_paid += late_fee; penalty_record.last_penalty_date = get_block_timestamp(); self.member_penalties.write(member, penalty_record); - + // Record penalty event self._record_penalty_event(member, round_id, PenaltyEventType::LateFee, late_fee); - + // Emit component event - self.emit(Event::LateFeeApplied(LateFeeApplied { - member, - round_id, - fee_amount: late_fee, - contribution_amount: contribution.amount, - timestamp: get_block_timestamp(), - })); + self + .emit( + Event::LateFeeApplied( + LateFeeApplied { + member, + round_id, + fee_amount: late_fee, + contribution_amount: contribution.amount, + timestamp: get_block_timestamp(), + }, + ), + ); } fn add_strike( - ref self: ComponentState, - member: ContractAddress, - round_id: u256 + ref self: ComponentState, member: ContractAddress, round_id: u256, ) { // Get penalty configuration from component storage let penalty_config = self.penalty_config.read(); - + // Get current penalty record let mut penalty_record = self.member_penalties.read(member); penalty_record.strikes += 1; penalty_record.last_penalty_date = get_block_timestamp(); - + // Check if member should be banned if penalty_record.strikes >= penalty_config.max_strikes { penalty_record.is_banned = true; } - + // Save updated penalty record self.member_penalties.write(member, penalty_record); - + // Record penalty event self._record_penalty_event(member, round_id, PenaltyEventType::Strike, 0); - + // Emit strike event - self.emit(Event::StrikeAdded(StrikeAdded { - member, - round_id, - current_strikes: penalty_record.strikes, - timestamp: get_block_timestamp(), - })); + self + .emit( + Event::StrikeAdded( + StrikeAdded { + member, + round_id, + current_strikes: penalty_record.strikes, + timestamp: get_block_timestamp(), + }, + ), + ); } - fn remove_strike( - ref self: ComponentState, - member: ContractAddress - ) { + fn remove_strike(ref self: ComponentState, member: ContractAddress) { // Get current penalty record let mut penalty_record = self.member_penalties.read(member); - + if penalty_record.strikes > 0 { penalty_record.strikes -= 1; - + // Check if member should be unbanned let penalty_config = self.penalty_config.read(); if penalty_record.is_banned && penalty_record.strikes < penalty_config.max_strikes { penalty_record.is_banned = false; - // Note: Member re-addition is handled by the main contract - // The component only manages its own penalty state + // The component only manages its own penalty state } - + // Save updated penalty record self.member_penalties.write(member, penalty_record); - + // Record penalty event self._record_penalty_event(member, 0, PenaltyEventType::StrikeRemoved, 0); - + // Emit event - self.emit(Event::StrikeRemoved(StrikeRemoved { - member, - removed_by: get_caller_address(), - new_strikes: penalty_record.strikes, - timestamp: get_block_timestamp(), - })); + self + .emit( + Event::StrikeRemoved( + StrikeRemoved { + member, + removed_by: get_caller_address(), + new_strikes: penalty_record.strikes, + timestamp: get_block_timestamp(), + }, + ), + ); } } - fn ban_member( - ref self: ComponentState, - member: ContractAddress - ) { + fn ban_member(ref self: ComponentState, member: ContractAddress) { // Get current penalty record let mut penalty_record = self.member_penalties.read(member); penalty_record.is_banned = true; penalty_record.strikes = self.penalty_config.read().max_strikes; - + // Save updated penalty record self.member_penalties.write(member, penalty_record); - + // Note: Member removal is handled by the main contract // The component only manages its own penalty state - + // Record penalty event self._record_penalty_event(member, 0, PenaltyEventType::Ban, 0); } - fn unban_member( - ref self: ComponentState, - member: ContractAddress - ) { + fn unban_member(ref self: ComponentState, member: ContractAddress) { // Get current penalty record let mut penalty_record = self.member_penalties.read(member); penalty_record.is_banned = false; penalty_record.strikes = 0; - + // Save updated penalty record self.member_penalties.write(member, penalty_record); - + // Note: Member re-addition is handled by the main contract // The component only manages its own penalty state - + // Record penalty event self._record_penalty_event(member, 0, PenaltyEventType::Unban, 0); } - + fn _reset_penalty_pool(ref self: ComponentState) { self.penalty_pool.write(0); } - - + + fn _record_penalty_event( ref self: ComponentState, member: ContractAddress, @@ -433,7 +472,7 @@ pub mod penalty_component { timestamp: get_block_timestamp(), admin: get_caller_address(), }; - + self.penalty_history.write((member, history_count), event); self.penalty_history_count.write(member, history_count + 1); } @@ -444,3 +483,4 @@ pub mod penalty_component { } } } + diff --git a/src/interfaces/IStarkRemit.cairo b/src/interfaces/IStarkRemit.cairo index 0b2038c..234f24c 100644 --- a/src/interfaces/IStarkRemit.cairo +++ b/src/interfaces/IStarkRemit.cairo @@ -1,5 +1,5 @@ use starknet::ContractAddress; -use starkremit_contract::base::types::*; +use starkremit_contract::base::types::{*, MemberProfileData}; #[starknet::interface] pub trait IStarkRemit { @@ -280,6 +280,7 @@ pub trait IStarkRemit { fn emergency_set_pause_meta(ref self: TContractState, reason: felt252); fn emergency_set_ban(ref self: TContractState, member: ContractAddress, banned: bool); + // Penalty Management Functions fn apply_late_fee(ref self: TContractState, member: ContractAddress, round_id: u256); fn add_strike(ref self: TContractState, member: ContractAddress, round_id: u256); @@ -292,7 +293,7 @@ pub trait IStarkRemit { fn setup_auto_schedule(ref self: TContractState, config: AutoScheduleConfig); fn maintain_rolling_schedule(ref self: TContractState); fn auto_activate_round(ref self: TContractState, round_id: u256); - fn auto_complete_expired_rounds(ref self: TContractState); + fn auto_complete_expired_rounds(ref self: TContractState, max_iterations: u32) -> (u32, bool); fn modify_schedule(ref self: TContractState, round_id: u256, new_deadline: u64); // Payment Flexibility Functions @@ -308,14 +309,26 @@ pub trait IStarkRemit { member: ContractAddress, round_id: u256, amount: u256, + token: ContractAddress, ) -> (u256, u256); - fn extend_grace_period( - ref self: TContractState, - member: ContractAddress, - extension_hours: u64, - ); + fn extend_grace_period(ref self: TContractState, member: ContractAddress, extension_hours: u64); fn add_supported_token(ref self: TContractState, token: ContractAddress); fn remove_supported_token(ref self: TContractState, token: ContractAddress); fn update_payment_config(ref self: TContractState, config: PaymentConfig); fn process_auto_payments(ref self: TContractState); + + // Contribution wrapper to centralize analytics update + fn contribute_round(ref self: TContractState, round_id: u256, amount: u256); + + // Member Profile Functions + fn create_member_profile(ref self: TContractState, member: ContractAddress); + fn update_reliability_rating(ref self: TContractState, member: ContractAddress, new_rating: u8); + fn get_member_profile(self: @TContractState, member: ContractAddress) -> MemberProfileData; + fn add_to_waitlist(ref self: TContractState, member: ContractAddress); + fn remove_from_waitlist(ref self: TContractState, member: ContractAddress) -> bool; + fn get_waitlist_position(self: @TContractState, member: ContractAddress) -> u32; + fn update_communication_preferences( + ref self: TContractState, member: ContractAddress, preferences: felt252, + ); + fn send_member_message(ref self: TContractState, member: ContractAddress, message: felt252); } diff --git a/src/lib.cairo b/src/lib.cairo index 56468a5..6434c90 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -29,9 +29,9 @@ pub mod component { pub mod test; } pub mod emergency; - pub mod penalty; pub mod kyc; pub mod loan; + pub mod penalty; pub mod savings_group; pub mod token_management; pub mod transfer; @@ -40,8 +40,8 @@ pub mod component { pub mod test; pub mod user_management; } + pub mod analytics; pub mod auto_schedule; pub mod member_profile; pub mod payment_flexibility; - pub mod analytics; } diff --git a/src/starkremit/StarkRemit.cairo b/src/starkremit/StarkRemit.cairo index c222a55..e6d94a6 100644 --- a/src/starkremit/StarkRemit.cairo +++ b/src/starkremit/StarkRemit.cairo @@ -3,33 +3,44 @@ use core::num::traits::Zero; use openzeppelin::access::accesscontrol::AccessControlComponent; use openzeppelin::access::ownable::OwnableComponent; use openzeppelin::introspection::src5::SRC5Component; -use openzeppelin::upgrades::UpgradeableComponent; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use openzeppelin::upgrades::UpgradeableComponent; use starknet::storage::{ Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess, }; use starknet::{ContractAddress, get_block_timestamp, get_caller_address}; use starkremit_contract::base::errors::{ - GovernanceErrors, GroupErrors, KYCErrors, RegistrationErrors, TransferErrors, EmergencyErrors, + EmergencyErrors, GovernanceErrors, GroupErrors, KYCErrors, RegistrationErrors, TransferErrors, }; use starkremit_contract::base::events::*; use starkremit_contract::base::types::{ - Agent, AgentStatus, ContributionRound, GovRole, KYCLevel, KycLevel, KycStatus, LoanRequest, - LoanStatus, MemberContribution, ParameterBounds, ParameterHistory, RegistrationRequest, - RegistrationStatus, RoundStatus, SavingsGroup, TimelockChange, TransferData, TransferHistory, - TransferStatus, UserKycData, UserProfile, PenaltyConfig, MemberPenaltyRecord, PenaltyEventRecord, DistributionData, MemberShare, PenaltyEventType, - AutoScheduleConfig, ScheduledRound, RoundData, + Agent, AgentStatus, AutoPaymentSetup, AutoScheduleConfig, ContributionAnalytics, + ContributionRound, DistributionData, FinancialReport, GovRole, KYCLevel, KycLevel, KycStatus, + LoanRequest, LoanStatus, MemberAnalytics, MemberContribution, MemberPenaltyRecord, + MemberProfileData, MemberShare, ParameterBounds, ParameterHistory, PaymentConfig, + PaymentFrequency, PenaltyConfig, PenaltyEventRecord, PenaltyEventType, RegistrationRequest, + RegistrationStatus, RoundData, RoundPerformanceMetrics, RoundStatus, SavingsGroup, + ScheduledRound, SystemHealthMetrics, TimelockChange, TransferData, TransferHistory, + TransferStatus, UserKycData, UserProfile, +}; +use starkremit_contract::component::analytics::{ + IAnalytics, IMainContractData as AnalyticsMainContractData, +}; +use starkremit_contract::component::auto_schedule::{ + IAutoSchedule, IMainContractData as AutoScheduleMainContractData, }; -use starkremit_contract::interfaces::IStarkRemit; use starkremit_contract::component::emergency::IEmergency; -use starkremit_contract::component::penalty::{IPenalty, IMainContractData as PenaltyMainContractData}; -use starkremit_contract::component::auto_schedule::{IAutoSchedule, IMainContractData as AutoScheduleMainContractData}; -use starkremit_contract::component::payment_flexibility::{PaymentConfig, PaymentFrequency, AutoPaymentSetup, PaymentStatus, PaymentRecord, IMainContractData as PaymentFlexibilityMainContractData}; -// use starkremit_contract::component::member_profile::MemberProfile; -// use starkremit_contract::component::analytics::{ -// ContributionAnalytics, MemberAnalytics, RoundPerformanceMetrics, FinancialReport, SystemHealthMetrics -// }; +use starkremit_contract::component::member_profile::{ + IMainContractData as MemberProfileMainContractData, IMemberProfile, +}; +use starkremit_contract::component::payment_flexibility::{ + IMainContractData as PaymentFlexibilityMainContractData, IPaymentFlexibility, +}; +use starkremit_contract::component::penalty::{ + IMainContractData as PenaltyMainContractData, IPenalty, +}; +use starkremit_contract::interfaces::IStarkRemit; const INTEREST_RATE: u256 = 500; // 5% in basis points (0.05 * 10000) @@ -40,20 +51,19 @@ const LOAN_TERM_DAYS: u64 = 30 * 24 * 60 * 60; // 30 days in seconds #[starknet::contract] pub mod StarkRemit { use starkremit_contract::component::agent::agent_component; + use starkremit_contract::component::analytics::analytics_component; + use starkremit_contract::component::auto_schedule::auto_schedule_component; use starkremit_contract::component::contribution::contribution::contribution_component; + use starkremit_contract::component::emergency::emergency_component; use starkremit_contract::component::kyc::kyc_component; use starkremit_contract::component::loan::loan_component; + use starkremit_contract::component::member_profile::member_profile_component; + use starkremit_contract::component::payment_flexibility::payment_flexibility_component; + use starkremit_contract::component::penalty::penalty_component; use starkremit_contract::component::savings_group::savings_group_component; use starkremit_contract::component::token_management::token_management_component; use starkremit_contract::component::transfer::transfer_component; use starkremit_contract::component::user_management::user_management::user_management_component; - use starkremit_contract::component::emergency::emergency_component; - use starkremit_contract::component::penalty::penalty_component; - use starkremit_contract::component::payment_flexibility::payment_flexibility_component; - use starkremit_contract::component::auto_schedule::auto_schedule_component; - // use starkremit_contract::component::member_profile::member_profile_component; - // use starkremit_contract::component::analytics::analytics_component; - use super::*; component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent); @@ -83,9 +93,13 @@ pub mod StarkRemit { component!(path: emergency_component, storage: emergency, event: EmergencyEvent); component!(path: penalty_component, storage: penalty, event: PenaltyEvent); component!(path: auto_schedule_component, storage: auto_schedule, event: AutoScheduleEvent); - component!(path: payment_flexibility_component, storage: payment_flexibility, event: PaymentFlexibilityEvent); - // component!(path: member_profile_component, storage: member_profile, event: MemberProfileEvent); - // component!(path: analytics_component, storage: analytics, event: AnalyticsEvent); + component!( + path: payment_flexibility_component, + storage: payment_flexibility, + event: PaymentFlexibilityEvent, + ); + component!(path: analytics_component, storage: analytics, event: AnalyticsEvent); + component!(path: member_profile_component, storage: member_profile, event: MemberProfileEvent); #[abi(embed_v0)] @@ -124,26 +138,23 @@ pub mod StarkRemit { // Emergency component internal methods impl EmergencyInternalImpl = emergency_component::InternalImpl; - // Penalty Component + // Penalty Component impl PenaltyInternalImpl = penalty_component::InternalImpl; // Auto Schedule Component (internal use only - functions exposed via IStarkRemit) impl AutoScheduleInternalImpl = auto_schedule_component::InternalImpl; - - // Payment Flexibility Component (internal use only - functions exposed via IStarkRemit) - impl PaymentFlexibilityInternalImpl = payment_flexibility_component::InternalImpl; - - // Member Profile Component (internal use only - functions exposed via IStarkRemit) - // impl MemberProfileImpl = member_profile_component::MemberProfileImpl; - // impl MemberProfileInternalImpl = member_profile_component::InternalImpl; // Payment Flexibility Component (internal use only - functions exposed via IStarkRemit) - // impl PaymentFlexibilityImpl = payment_flexibility_component::PaymentFlexibilityImpl; - // impl PaymentFlexibilityInternalImpl = payment_flexibility_component::InternalImpl; + impl PaymentFlexibilityInternalImpl = + payment_flexibility_component::InternalImpl; + + // Analytics Component (exposed directly via component interface) + impl AnalyticsImpl = analytics_component::AnalyticsImpl; + impl AnalyticsInternalImpl = analytics_component::InternalImpl; - // Analytics Component (internal use only - functions exposed via IStarkRemit) - // impl AnalyticsImpl = analytics_component::AnalyticsImpl; - // impl AnalyticsInternalImpl = analytics_component::InternalImpl; + // Member Profile Component (exposed directly via component interface) + impl MemberProfileImpl = member_profile_component::MemberProfileImpl; + impl MemberProfileInternalImpl = member_profile_component::InternalImpl; impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; @@ -313,6 +324,8 @@ pub mod StarkRemit { PenaltyEvent: penalty_component::Event, AutoScheduleEvent: auto_schedule_component::Event, PaymentFlexibilityEvent: payment_flexibility_component::Event, + AnalyticsEvent: analytics_component::Event, + MemberProfileEvent: member_profile_component::Event, // System Management Events AgentAuthorized: AgentAuthorized, AgentPermissionUpdated: AgentPermissionUpdated, @@ -387,106 +400,9 @@ pub mod StarkRemit { MemberBanned: MemberBanned, MemberUnbanned: MemberUnbanned, PenaltyPoolDistributed: PenaltyPoolDistributed, - // LateFeeApplied: LateFeeApplied, - // StrikeAdded: StrikeAdded, - // StrikeRemoved: StrikeRemoved, - // AutoPaymentSetup: AutoPaymentSetup, - // EarlyPaymentProcessed: EarlyPaymentProcessed, - // GracePeriodExtended: GracePeriodExtended, - // TokenValueConverted: TokenValueConverted, - // MemberProfileUpdated: MemberProfileUpdated, - // RollingScheduleMaintained: RollingScheduleMaintained, } - // #[derive(Drop, starknet::Event)] - // pub struct LateFeeApplied { - // pub member: ContractAddress, - // pub round_id: u256, - // pub fee_amount: u256, - // pub contribution_amount: u256, - // pub timestamp: u64, - // } - - - - // #[derive(Drop, starknet::Event)] - // pub struct StrikeAdded { - // pub member: ContractAddress, - // pub round_id: u256, - // pub current_strikes: u32, - // pub timestamp: u64, - // } - - // #[derive(Drop, starknet::Event)] - // pub struct StrikeRemoved { - // pub member: ContractAddress, - // pub removed_by: ContractAddress, - // pub new_strikes: u32, - // pub timestamp: u64, - // } - - - - // #[derive(Drop, starknet::Event)] - // pub struct AutoPaymentSetup { - // pub member: ContractAddress, - // pub token: ContractAddress, - // pub amount: u256, - // pub frequency: PaymentFrequency, - // pub next_payment_date: u64, - // pub timestamp: u64, - // } - - // #[derive(Drop, starknet::Event)] - // pub struct EarlyPaymentProcessed { - // pub member: ContractAddress, - // pub round_id: u256, - // pub original_amount: u256, - // pub discount_amount: u256, - // pub final_amount: u256, - // pub timestamp: u64, - // } - - // #[derive(Drop, starknet::Event)] - // pub struct GracePeriodExtended { - // pub member: ContractAddress, - // pub extension_hours: u64, - // pub total_extension: u64, - // pub extended_by: ContractAddress, - // pub timestamp: u64, - // } - - // #[derive(Drop, starknet::Event)] - // pub struct TokenValueConverted { - // pub member: ContractAddress, - // pub from_token: ContractAddress, - // pub to_token: ContractAddress, - // pub original_amount: u256, - // pub converted_amount: u256, - // pub from_price: u256, - // pub to_price: u256, - // pub timestamp: u64, - // } - - // #[derive(Drop, starknet::Event)] - // pub struct MemberProfileUpdated { - // pub member: ContractAddress, - // pub field: felt252, - // pub old_value: felt252, - // pub new_value: felt252, - // pub updated_by: ContractAddress, - // pub timestamp: u64, - // } - - - - // #[derive(Drop, starknet::Event)] - // pub struct RollingScheduleMaintained { - // pub rounds_created: u32, - // pub last_maintenance_timestamp: u64, - // } - // Contract storage definition #[storage] #[allow(starknet::colliding_storage_paths)] @@ -516,8 +432,6 @@ pub mod StarkRemit { #[substorage(v0)] transfer_component: transfer_component::Storage, #[substorage(v0)] - payment_flexibility_component: payment_flexibility_component::Storage, - #[substorage(v0)] emergency: emergency_component::Storage, #[substorage(v0)] penalty: penalty_component::Storage, @@ -525,33 +439,12 @@ pub mod StarkRemit { auto_schedule: auto_schedule_component::Storage, #[substorage(v0)] payment_flexibility: payment_flexibility_component::Storage, + #[substorage(v0)] + analytics: analytics_component::Storage, + #[substorage(v0)] + member_profile: member_profile_component::Storage, // Emergency and Penalty System Storage emergency_approvals: Map>, - // penalty_config: PenaltyConfig, - // emergency_operations: Map, - // Penalty System Storage - // member_penalties: Map, - // penalty_pool: u256, - // penalty history stored as (member, index) -> event and per-member count - // penalty_history: Map<(ContractAddress, u32), PenaltyEvent>, - // penalty_history_count: Map, - // Auto-Schedule System Storage - // auto_schedule_config: AutoScheduleConfig, - // scheduled_rounds: Map, - // round_schedule_index: u256, - // last_schedule_maintenance: u64, - // schedule_maintenance_interval: u64, - // Member Profile Storage - // member_profiles: Map, - // member_profile_count: u32, - // Payment Flexibility Storage - // payment_config: PaymentConfig, - // auto_payment_setups: Map, - // Analytics Storage - // contribution_analytics: ContributionAnalytics, - // member_analytics: Map, - // last_analytics_update: u64, - // System Management Storage agent_permissions: Map<(ContractAddress, felt252), bool>, // (agent, permission) -> granted paused_functions: Map, // function selector -> paused multi_sig_operations: Map, // op_id -> operation data @@ -696,11 +589,32 @@ pub mod StarkRemit { self.auto_schedule.initializer(owner); // Initialize payment flexibility component self.payment_flexibility.initializer(owner); + // Initialize analytics component + self.analytics.initializer(owner); + // Initialize member profile component + self.member_profile.initializer(owner); } // Implementation of the StarkRemit interface with KYC functions #[abi(embed_v0)] impl IStarkRemitImpl of IStarkRemit::IStarkRemit { + // Wrapper: contribute_round -> updates analytics centrally + fn contribute_round(ref self: ContractState, round_id: u256, amount: u256) { + let caller = get_caller_address(); + let now = get_block_timestamp(); + + // Read deadline to compute timeliness using real data + let round_before = self.rounds.read(round_id); + let is_on_time = now <= round_before.deadline; + + // Delegate to component for contribution state (no event emission) + self.contribution_component.contribute_round(round_id, amount); + + // Centralized analytics update + InternalFunctions::_update_analytics_after_contribution( + ref self, caller, round_id, amount, is_on_time, + ); + } // --- Penalty Functions --- fn apply_late_fee(ref self: ContractState, member: ContractAddress, round_id: u256) { self.ownable.assert_only_owner(); @@ -709,104 +623,125 @@ pub mod StarkRemit { fn add_strike(ref self: ContractState, member: ContractAddress, round_id: u256) { self.ownable.assert_only_owner(); - + // Get current penalty record to check if member will be automatically banned let current_record = self.penalty.get_member_penalty_record(member); let penalty_config = self.penalty.get_penalty_config(); let will_be_banned = current_record.strikes + 1 >= penalty_config.max_strikes; - + // Add strike in penalty component self.penalty.add_strike(member, round_id); - + // If member was automatically banned, remove them from main contract's member list if will_be_banned { self._remove_member_from_list(member); - + // Emit main contract event for automatic ban - self.emit(Event::MemberBanned(MemberBanned { - member, - reason: 'max_strikes_reached', - strikes: current_record.strikes + 1, - banned_by: get_caller_address(), - timestamp: get_block_timestamp(), - })); + self + .emit( + Event::MemberBanned( + MemberBanned { + member, + reason: 'max_strikes_reached', + strikes: current_record.strikes + 1, + banned_by: get_caller_address(), + timestamp: get_block_timestamp(), + }, + ), + ); } } fn remove_strike(ref self: ContractState, member: ContractAddress) { self.ownable.assert_only_owner(); - + // Get current penalty record to check if member will be automatically unbanned let current_record = self.penalty.get_member_penalty_record(member); let penalty_config = self.penalty.get_penalty_config(); - let will_be_unbanned = current_record.is_banned && current_record.strikes - 1 < penalty_config.max_strikes; - + let will_be_unbanned = current_record.is_banned && current_record.strikes + - 1 < penalty_config.max_strikes; + // Remove strike in penalty component self.penalty.remove_strike(member); - + // If member was automatically unbanned, re-add them to main contract's member list if will_be_unbanned { self._add_member_to_list(member); - + // Emit main contract event for automatic unban - self.emit(Event::MemberUnbanned(MemberUnbanned { - member, - unbanned_by: get_caller_address(), - timestamp: get_block_timestamp(), - })); + self + .emit( + Event::MemberUnbanned( + MemberUnbanned { + member, + unbanned_by: get_caller_address(), + timestamp: get_block_timestamp(), + }, + ), + ); } } fn ban_member(ref self: ContractState, member: ContractAddress) { self.ownable.assert_only_owner(); - + // First, update penalty state in component self.penalty.ban_member(member); - + // Then, remove member from main contract's member list self._remove_member_from_list(member); - + // Emit main contract event - self.emit(Event::MemberBanned(MemberBanned { - member, - reason: 'admin_ban', - strikes: self.penalty.get_member_penalty_record(member).strikes, - banned_by: get_caller_address(), - timestamp: get_block_timestamp(), - })); + self + .emit( + Event::MemberBanned( + MemberBanned { + member, + reason: 'admin_ban', + strikes: self.penalty.get_member_penalty_record(member).strikes, + banned_by: get_caller_address(), + timestamp: get_block_timestamp(), + }, + ), + ); } fn unban_member(ref self: ContractState, member: ContractAddress) { self.ownable.assert_only_owner(); - + // First, update penalty state in component self.penalty.unban_member(member); - + // Then, re-add member to main contract's member list self._add_member_to_list(member); - + // Emit main contract event - self.emit(Event::MemberUnbanned(MemberUnbanned { - member, - unbanned_by: get_caller_address(), - timestamp: get_block_timestamp(), - })); + self + .emit( + Event::MemberUnbanned( + MemberUnbanned { + member, + unbanned_by: get_caller_address(), + timestamp: get_block_timestamp(), + }, + ), + ); } fn distribute_penalty_pool(ref self: ContractState) { self.ownable.assert_only_owner(); - + // Get distribution data from penalty component let distribution_data = self.penalty.calculate_distribution_data(); - + if distribution_data.total_amount == 0 { return; // No penalty pool to distribute } - + // Execute transfers using main contract's transfer function let mut distributed_count = 0; let mut i = 0; - + while i < distribution_data.member_shares.len() { let member_share = *distribution_data.member_shares.at(i); if member_share.share > 0 { @@ -816,20 +751,25 @@ pub mod StarkRemit { } i += 1; } - + // Reset penalty pool in component after successful distribution self.penalty.reset_penalty_pool(); - + // Emit main contract event - self.emit(Event::PenaltyPoolDistributed(PenaltyPoolDistributed { - total_amount: distribution_data.total_amount, - recipient_count: distributed_count, - distribution_type: 'proportional', - timestamp: get_block_timestamp(), - })); + self + .emit( + Event::PenaltyPoolDistributed( + PenaltyPoolDistributed { + total_amount: distribution_data.total_amount, + recipient_count: distributed_count, + distribution_type: 'proportional', + timestamp: get_block_timestamp(), + }, + ), + ); } - + fn grant_admin_role(ref self: ContractState, admin: ContractAddress) { self.accesscontrol.assert_only_role(ADMIN_ROLE); self.accesscontrol._grant_role(ADMIN_ROLE, admin); @@ -2520,7 +2460,7 @@ pub mod StarkRemit { self.emit(ContractRegistered { name, addr: new_address }); true } - + /// Schedule a parameter update (SuperAdmin only, timelock) fn schedule_parameter_update(ref self: ContractState, key: felt252, value: u256) -> bool { let caller = get_caller_address(); @@ -2669,61 +2609,73 @@ pub mod StarkRemit { // Only pauser can pause let caller = get_caller_address(); assert(self.agent_permissions.read((caller, 'PAUSER')), 'Not authorized pauser'); - + self.emergency._pause_with_metadata(reason); - - self.emit(EmergencyPauseActivated { - function_selector: 0, // Global pause, not function-specific - caller, - expires_at: 0 // No expiry for global pause - }); + + self + .emit( + EmergencyPauseActivated { + function_selector: 0, // Global pause, not function-specific + caller, + expires_at: 0 // No expiry for global pause + }, + ); } /// Emergency unpause the entire contract fn emergency_unpause_contract(ref self: ContractState) { let caller = get_caller_address(); assert(self.agent_permissions.read((caller, 'PAUSER')), 'Not authorized pauser'); - + self.emergency._unpause_with_metadata_clear(); - - self.emit(EmergencyPauseDeactivated { - function_selector: 0, // Global pause, not function-specific - caller - }); + + self + .emit( + EmergencyPauseDeactivated { + function_selector: 0, // Global pause, not function-specific + caller, + }, + ); } /// Emergency pause with metadata fn emergency_pause_with_metadata(ref self: ContractState, reason: felt252) { let caller = get_caller_address(); assert(self.agent_permissions.read((caller, 'PAUSER')), 'Not authorized pauser'); - + self.emergency._pause_with_metadata(reason); - - self.emit(EmergencyPauseActivated { - function_selector: 0, // Global pause, not function-specific - caller, - expires_at: 0 // No expiry for global pause - }); + + self + .emit( + EmergencyPauseActivated { + function_selector: 0, // Global pause, not function-specific + caller, + expires_at: 0 // No expiry for global pause + }, + ); } /// Emergency unpause with metadata clear fn emergency_unpause_with_metadata_clear(ref self: ContractState) { let caller = get_caller_address(); assert(self.agent_permissions.read((caller, 'PAUSER')), 'Not authorized pauser'); - + self.emergency._unpause_with_metadata_clear(); - - self.emit(EmergencyPauseDeactivated { - function_selector: 0, // Global pause, not function-specific - caller - }); + + self + .emit( + EmergencyPauseDeactivated { + function_selector: 0, // Global pause, not function-specific + caller, + }, + ); } /// Emergency set pause metadata fn emergency_set_pause_meta(ref self: ContractState, reason: felt252) { let caller = get_caller_address(); assert(self.agent_permissions.read((caller, 'PAUSER')), 'Not authorized pauser'); - + self.emergency._set_pause_meta(reason); } @@ -2731,23 +2683,31 @@ pub mod StarkRemit { fn emergency_set_ban(ref self: ContractState, member: ContractAddress, banned: bool) { let caller = get_caller_address(); assert(self.agent_permissions.read((caller, 'PAUSER')), 'Not authorized pauser'); - + self.emergency._set_ban(member, banned); - + if banned { - self.emit(Event::MemberBanned(MemberBanned { - member, - reason: 'emergency_ban', - strikes: 0, - banned_by: caller, - timestamp: get_block_timestamp(), - })); + self + .emit( + Event::MemberBanned( + MemberBanned { + member, + reason: 'emergency_ban', + strikes: 0, + banned_by: caller, + timestamp: get_block_timestamp(), + }, + ), + ); } else { - self.emit(Event::MemberUnbanned(MemberUnbanned { - member, - unbanned_by: caller, - timestamp: get_block_timestamp(), - })); + self + .emit( + Event::MemberUnbanned( + MemberUnbanned { + member, unbanned_by: caller, timestamp: get_block_timestamp(), + }, + ), + ); } } @@ -2767,9 +2727,11 @@ pub mod StarkRemit { self.auto_schedule._auto_activate_round(round_id); } - fn auto_complete_expired_rounds(ref self: ContractState) { + fn auto_complete_expired_rounds( + ref self: ContractState, max_iterations: u32, + ) -> (u32, bool) { self.ownable.assert_only_owner(); - self.auto_schedule._auto_complete_expired_rounds(); + self.auto_schedule._auto_complete_expired_rounds(max_iterations) } fn modify_schedule(ref self: ContractState, round_id: u256, new_deadline: u64) { @@ -2794,15 +2756,14 @@ pub mod StarkRemit { member: ContractAddress, round_id: u256, amount: u256, + token: ContractAddress, ) -> (u256, u256) { self.ownable.assert_only_owner(); - self.payment_flexibility._process_early_payment(member, round_id, amount) + self.payment_flexibility._process_early_payment(member, round_id, amount, token) } fn extend_grace_period( - ref self: ContractState, - member: ContractAddress, - extension_hours: u64, + ref self: ContractState, member: ContractAddress, extension_hours: u64, ) { self.ownable.assert_only_owner(); self.payment_flexibility._extend_grace_period(member, extension_hours); @@ -2827,8 +2788,50 @@ pub mod StarkRemit { self.ownable.assert_only_owner(); self.payment_flexibility._process_auto_payments(); } - } + // --- Member Profile Functions --- + fn create_member_profile(ref self: ContractState, member: ContractAddress) { + self.ownable.assert_only_owner(); + self.member_profile.create_member_profile(member); + } + + fn update_reliability_rating( + ref self: ContractState, member: ContractAddress, new_rating: u8, + ) { + self.ownable.assert_only_owner(); + self.member_profile.update_reliability_rating(member, new_rating); + } + + fn get_member_profile(self: @ContractState, member: ContractAddress) -> MemberProfileData { + self.member_profile.get_member_profile(member) + } + + fn add_to_waitlist(ref self: ContractState, member: ContractAddress) { + self.ownable.assert_only_owner(); + self.member_profile.add_to_waitlist(member); + } + + fn remove_from_waitlist(ref self: ContractState, member: ContractAddress) -> bool { + self.ownable.assert_only_owner(); + self.member_profile.remove_from_waitlist(member) + } + + fn get_waitlist_position(self: @ContractState, member: ContractAddress) -> u32 { + self.member_profile.get_waitlist_position(member) + } + + fn update_communication_preferences( + ref self: ContractState, member: ContractAddress, preferences: felt252, + ) { + self.ownable.assert_only_owner(); + self.member_profile.update_communication_preferences(member, preferences); + } + + fn send_member_message(ref self: ContractState, member: ContractAddress, message: felt252) { + self.ownable.assert_only_owner(); + self.member_profile.send_member_message(member, message); + } + } // --- System Management Functions --- @@ -3013,8 +3016,7 @@ pub mod StarkRemit { // --- Emergency Functions --- #[generate_trait] - impl Emergency of EmergencyTrait { - + pub impl Emergency of EmergencyTrait { fn emergency_withdraw_all(ref self: ContractState) { self.ownable.assert_only_owner(); self.emergency.assert_paused(); // Only callable when paused @@ -3121,8 +3123,9 @@ pub mod StarkRemit { // Transfer funds to recipient self.transfer_tokens_to_member(round.recipient, round.total_contributions); - // Update round status + // Update round status and set completion timestamp round.status = RoundStatus::Completed; + round.completed_at = get_block_timestamp(); self.rounds.write(round_id, round); // Update analytics @@ -3234,7 +3237,7 @@ pub mod StarkRemit { let total_contract_balance = self.get_contract_token_balance_specific(token); let total_allocated_tokens = self._calculate_total_allocated_tokens(token); let unallocated_balance = total_contract_balance - total_allocated_tokens; - + // Ensure we only recover unallocated tokens assert(unallocated_balance >= amount, EmergencyErrors::INSUFFICIENT_BALANCE); @@ -3262,14 +3265,18 @@ pub mod StarkRemit { // Business Logic: Migrate all funds to new contract assert(!new_contract.is_zero(), EmergencyErrors::INVALID_CONTRACT_ADDRESS); - assert(new_contract != starknet::get_contract_address(), EmergencyErrors::CANNOT_MIGRATE_TO_SELF); - + assert( + new_contract != starknet::get_contract_address(), + EmergencyErrors::CANNOT_MIGRATE_TO_SELF, + ); + // Verify the target contract is registered in the contract registry // or implements a specific interface let registered_migration_target = self.contract_registry.read('migration_target'); assert( - new_contract == registered_migration_target || registered_migration_target.is_zero(), - EmergencyErrors::INVALID_MIGRATION_TARGET + new_contract == registered_migration_target + || registered_migration_target.is_zero(), + EmergencyErrors::INVALID_MIGRATION_TARGET, ); let total_balance = self.get_contract_token_balance(); @@ -3290,41 +3297,41 @@ pub mod StarkRemit { ), ); } - } // Implementation of IMainContractData trait for penalty component impl MainContractDataImpl of PenaltyMainContractData { - fn get_member_contribution_data(self: @ContractState, round_id: u256, member: ContractAddress) -> MemberContribution { + fn get_member_contribution_data( + self: @ContractState, round_id: u256, member: ContractAddress, + ) -> MemberContribution { let contribution = self.member_contributions.read((round_id, member)); MemberContribution { - member, - amount: contribution.amount, - contributed_at: contribution.contributed_at, + member, amount: contribution.amount, contributed_at: contribution.contributed_at, } } - + fn get_round_data(self: @ContractState, round_id: u256) -> RoundData { let round = self.rounds.read(round_id); RoundData { deadline: round.deadline, + completed_at: round.completed_at, status: round.status, total_contributions: round.total_contributions, } } - + fn get_member_status(self: @ContractState, member: ContractAddress) -> bool { self.members.read(member) } - + fn get_member_count(self: @ContractState) -> u32 { self.member_count.read() } - + fn get_round_ids(self: @ContractState) -> u256 { self.round_ids.read() } - + fn get_member_by_index(self: @ContractState, index: u32) -> ContractAddress { self.member_by_index.read(index) } @@ -3335,22 +3342,25 @@ pub mod StarkRemit { fn get_member_count(self: @ContractState) -> u32 { self.member_count.read() } - + fn get_member_by_index(self: @ContractState, index: u32) -> ContractAddress { self.member_by_index.read(index) } - + fn get_current_round_id(self: @ContractState) -> u256 { self.round_ids.read() } - - fn create_round(ref self: ContractState, recipient: ContractAddress, deadline: u64) -> u256 { + + fn create_round( + ref self: ContractState, recipient: ContractAddress, deadline: u64, + ) -> u256 { // Create a new round using existing round creation logic let round_id = self.round_ids.read() + 1; let round = ContributionRound { round_id, recipient, deadline, + completed_at: 0, status: RoundStatus::Scheduled, total_contributions: 0, }; @@ -3360,1124 +3370,410 @@ pub mod StarkRemit { } } + // Implementation of IMainContractData trait for payment flexibility component + impl PaymentFlexibilityMainContractDataImpl of PaymentFlexibilityMainContractData< + ContractState, + > { + fn get_round_data(self: @ContractState, round_id: u256) -> RoundData { + let round = self.rounds.read(round_id); + RoundData { + deadline: round.deadline, + completed_at: round.completed_at, + status: round.status, + total_contributions: round.total_contributions, + } + } - // Implementation of Member Profile Component Interface - // #[abi(embed_v0)] - // impl MemberProfileImpl of IMemberProfile { - // fn create_member_profile(ref self: ContractState, member: ContractAddress) { - // self.ownable.assert_only_owner(); - - // let profile = MemberProfile { - // join_date: get_block_timestamp(), - // total_contributions: 0, - // missed_contributions: 0, - // credit_score: 100, - // last_recipient_round: 0, - // reliability_rating: 100, - // preferred_payment_method: 'default', - // communication_preferences: 'email', - // }; - - // self.member_profiles.write(member, profile); - // self.member_profile_count.write(self.member_profile_count.read() + 1); - // } - - // fn update_reliability_rating( - // ref self: ContractState, member: ContractAddress, new_rating: u8, - // ) { - // self.ownable.assert_only_owner(); - - // assert(new_rating <= 100, 'Invalid rating: must be 0-100'); - - // let mut profile = self.member_profiles.read(member); - // profile.reliability_rating = new_rating; - // self.member_profiles.write(member, profile); - // } - - // fn get_member_profile(self: @ContractState, member: ContractAddress) -> MemberProfile { - // self.member_profiles.read(member) - // } - // } - - // // Implementation of Payment Flexibility Component Interface - // #[abi(embed_v0)] - // impl PaymentFlexibilityImpl of IPaymentFlexibility { - // fn setup_auto_payment( - // ref self: ContractState, - // token: ContractAddress, - // amount: u256, - // frequency: PaymentFrequency, - // ) { - // self.ownable.assert_only_owner(); - - // // Business Logic: Setup automatic recurring payments for a member - // let caller = get_caller_address(); - // let payment_config = self.payment_config.read(); - - // // Validate token is supported - // assert(self.is_token_supported(token), 'Token not supported'); - // assert(amount > 0, 'Invalid payment amount'); - - // // Check if member already has auto-payment setup - // let existing_setup = self.auto_payment_setups.read(caller); - // assert(!existing_setup.is_active, 'Auto-payment already active'); - - // // Calculate next payment date based on frequency - // let next_payment_date = self.calculate_next_payment_date(frequency); - - // // Create auto-payment setup - // let auto_payment = AutoPaymentSetup { - // member: caller, token, amount, frequency, next_payment_date, is_active: true, - // }; - - // self.auto_payment_setups.write(caller, auto_payment); - - // self - // .emit( - // Event::AutoPaymentSetup( - // AutoPaymentSetup { - // member: caller, - // token, - // amount, - // frequency, - // next_payment_date, - // timestamp: get_block_timestamp(), - // }, - // ), - // ); - // } - - // fn process_early_payment(ref self: ContractState, round_id: u256, amount: u256) { - // self.ownable.assert_only_owner(); - - // // Business Logic: Process early payment with discount - // let caller = get_caller_address(); - // let payment_config = self.payment_config.read(); - // let round = self.rounds.read(round_id); - - // // Validate round is active - // assert(round.status == RoundStatus::Active, 'Round not active'); - // assert(get_block_timestamp() < round.deadline, 'Round deadline passed'); - - // // Calculate early payment discount - // let discount_amount = (amount * payment_config.early_payment_discount_basis_points) - // / 10000; - // let final_amount = amount - discount_amount; - - // // Process the early payment - // self.process_contribution(round_id, caller, final_amount); - - // // Update member profile for early payment bonus - // let mut profile = self.member_profiles.read(caller); - // profile - // .reliability_rating = self - // .calculate_reliability_bonus(profile.reliability_rating, true); - // self.member_profiles.write(caller, profile); - - // self - // .emit( - // Event::EarlyPaymentProcessed( - // EarlyPaymentProcessed { - // member: caller, - // round_id, - // original_amount: amount, - // discount_amount, - // final_amount, - // timestamp: get_block_timestamp(), - // }, - // ), - // ); - // } - - // fn extend_grace_period( - // ref self: ContractState, member: ContractAddress, extension_hours: u64, - // ) { - // self.ownable.assert_only_owner(); - - // // Business Logic: Extend grace period for specific member - // assert(self.members.read(member), 'Member does not exist'); - // assert(extension_hours > 0, 'Invalid extension hours'); - // assert(extension_hours <= 168, 'Extension cannot exceed 1 week'); // Max 7 days - - // // Get current grace period extension - // let current_extension = self.grace_period_extensions.read(member); - // let new_extension = current_extension + extension_hours; - - // // Update grace period extension - // self.grace_period_extensions.write(member, new_extension); - - // self - // .emit( - // Event::GracePeriodExtended( - // GracePeriodExtended { - // member, - // extension_hours, - // total_extension: new_extension, - // extended_by: get_caller_address(), - // timestamp: get_block_timestamp(), - // }, - // ), - // ); - // } - - // fn convert_token_value( - // ref self: ContractState, - // from_token: ContractAddress, - // to_token: ContractAddress, - // amount: u256, - // ) -> u256 { - // // Business Logic: Convert token value using oracle - // let payment_config = self.payment_config.read(); - // let oracle_address = payment_config.usd_oracle_address; - - // assert(!oracle_address.is_zero(), 'Oracle address not set'); - // assert(from_token != to_token, 'Same token conversion not allowed'); - - // // Get token prices from oracle (simplified - would integrate with real oracle) - // let from_price = self.get_token_price_from_oracle(from_token); - // let to_price = self.get_token_price_from_oracle(to_token); - - // assert(from_price > 0 && to_price > 0, 'Invalid token prices'); - - // // Convert amount: (amount * from_price) / to_price - // let converted_amount = (amount * from_price) / to_price; - - // self - // .emit( - // Event::TokenValueConverted( - // TokenValueConverted { - // from_token, - // to_token, - // original_amount: amount, - // converted_amount, - // from_price, - // to_price, - // timestamp: get_block_timestamp(), - // }, - // ), - // ); - - // converted_amount - // } - - // fn get_payment_status( - // ref self: ContractState, member: ContractAddress, round_id: u256, - // ) -> PaymentStatus { - // // Business Logic: Determine payment status based on contribution timing - // let round = self.rounds.read(round_id); - // let member_contribution = self.member_contributions.read((round_id, member)); - // let current_time = get_block_timestamp(); - // let penalty_config = self.penalty_config.read(); - - // if member_contribution.amount == 0 { - // // No payment made - // if current_time > round.deadline + (penalty_config.grace_period_hours * 3600) { - // return PaymentStatus::Missed; - // } else if current_time > round.deadline { - // return PaymentStatus::Late; - // } else { - // return PaymentStatus::Pending; - // } - // } else { - // // Payment made - // if current_time <= round.deadline { - // return PaymentStatus::Paid; - // } else if current_time <= round.deadline - // + (penalty_config.grace_period_hours * 3600) { - // return PaymentStatus::Late; - // } else { - // return PaymentStatus::Overpaid; // Payment made after grace period - // } - // } - // } - // } - - // Implementation of Analytics Component Interface - // #[abi(embed_v0)] - // impl AnalyticsImpl of IAnalytics { - // fn generate_contribution_report(self: @ContractState) -> ContributionAnalytics { - // let mut analytics = self.contribution_analytics.read(); // Reads from cached analytics - - // // Calculate real-time statistics from actual data if cache is stale or not used - // // For a real-time report, you might recompute everything here, or update the cache - // // by calling an internal function. For this example, we assume - // // `cached_contribution_analytics` - // // is updated by other functions. - - // // Example of re-calculating if not using a cache: - // let mut total_rounds_count = 0; - // let mut successful_rounds_count = 0; - // let mut failed_rounds_count = 0; - - // let mut round_id = 1; - // let max_round_id = self.round_ids.read(); // Assuming this tracks total rounds created - // while round_id <= max_round_id { // Iterates through all scheduled rounds - // let round = self.rounds.read(round_id); // Reads round data - // total_rounds_count += 1; - - // match round.status { // Checks the status of the round - // RoundStatus::Completed => successful_rounds_count += 1, - // RoundStatus::Cancelled => failed_rounds_count += 1, - // _ => {} // Ignore Scheduled or Active rounds for "completed" or "failed" counts - // } - // round_id += 1; - // } - - // // Update analytics with real data - // analytics.total_rounds = total_rounds_count; - // analytics.successful_rounds = successful_rounds_count; - // analytics.failed_rounds = failed_rounds_count; - // analytics.total_penalties_collected = self.penalty_pool.read(); // Reads total penalties - // // member_reliability_distribution would need more complex aggregation logic - - // analytics - // } - - // fn get_member_performance( - // self: @ContractState, member: ContractAddress, - // ) -> MemberAnalytics { - // let mut analytics = MemberAnalytics { // Initializes a new MemberAnalytics struct - // total_contributions: 0, - // on_time_payments: 0, - // late_payments: 0, - // missed_payments: 0, - // reliability_score: 0, - // last_updated: 0, - // }; - - // // Calculate from actual contribution data - // let mut round_id = 1; - // let max_round_id = self.round_ids.read(); // Assuming this tracks total rounds created - // while round_id <= max_round_id { // Iterates through all scheduled rounds - // let round = self.rounds.read(round_id); // Reads round data - // // Assuming a mapping for member contributions per round: Map<(u256, - // // ContractAddress), ContributionDetail> - // let contribution = self.member_contributions.read((round_id, member)); - - // if contribution.amount > 0 { - // analytics.total_contributions += contribution.amount; - - // if contribution - // .contributed_at <= round - // .deadline { // Check against round deadline - // analytics.on_time_payments += 1; - // } else { - // analytics.late_payments += 1; - // } - // } else if round.status == RoundStatus::Completed - // || round.status == RoundStatus::Cancelled { - // // Only count as missed if the round has concluded and no contribution was made - // analytics.missed_payments += 1; - // } - - // round_id += 1; - // } - - // // Calculate reliability score - // let total_evaluated_rounds = analytics.on_time_payments - // + analytics.late_payments - // + analytics.missed_payments; - // if total_evaluated_rounds > 0 { - // analytics.reliability_score = (analytics.on_time_payments * 100) - // / total_evaluated_rounds; - // } - - // analytics.last_updated = get_block_timestamp(); - // analytics - // } - - // fn calculate_system_health(self: @ContractState) -> u8 { - // let analytics = self.contribution_analytics.read(); - - // if analytics.total_rounds == 0 { - // return 100; // Perfect health if no rounds yet - // } - - // // Calculate health based on success rate - // let success_rate = (analytics.successful_rounds * 100) / analytics.total_rounds; - // success_rate.try_into().unwrap() - // } - // } - - // Helper functions for enhanced business logic - // impl HelperFunctions of HelperFunctionsTrait { - // fn get_contract_token_balance(self: @ContractState) -> u256 { - // // Get contract's balance of the primary token - // let token_address = self.token_address.read(); - // let erc20_dispatcher = IERC20Dispatcher { contract_address: token_address }; - // erc20_dispatcher.balance_of(starknet::get_contract_address()) - // } - - // fn get_contract_token_balance_specific( - // self: @ContractState, token: ContractAddress, - // ) -> u256 { - // // Get contract's balance of a specific token - // let erc20_dispatcher = IERC20Dispatcher { contract_address: token }; - // erc20_dispatcher.balance_of(starknet::get_contract_address()) - // } - - // fn transfer_tokens_to_member( - // ref self: ContractState, member: ContractAddress, amount: u256, - // ) { - // // Transfer tokens to a member - // let token_address = self.token_address.read(); - // let erc20_dispatcher = IERC20Dispatcher { contract_address: token_address }; - // assert(erc20_dispatcher.transfer(member, amount), 'Transfer failed'); - // } - - // fn transfer_tokens_to_address( - // ref self: ContractState, recipient: ContractAddress, amount: u256, - // ) { - // // Transfer tokens to any address - // let token_address = self.token_address.read(); - // let erc20_dispatcher = IERC20Dispatcher { contract_address: token_address }; - // assert(erc20_dispatcher.transfer(recipient, amount), 'Transfer failed'); - // } - - // fn transfer_specific_tokens_to_address( - // ref self: ContractState, - // token: ContractAddress, - // recipient: ContractAddress, - // amount: u256, - // ) { - // // Transfer specific tokens to an address - // let erc20_dispatcher = IERC20Dispatcher { contract_address: token }; - // assert(erc20_dispatcher.transfer(recipient, amount), 'Transfer failed'); - // } - - // fn calculate_member_total_contribution( - // self: @ContractState, member: ContractAddress, - // ) -> u256 { - // // Calculate total contributions across all rounds for a member - // let mut total = 0; - // let mut round_id = 1; - // while round_id <= self.round_ids.read() { - // let contribution = self.member_contributions.read((round_id, member)); - // total += contribution.amount; - // round_id += 1; - // } - // total - // } - - // fn refund_round_contributions(ref self: ContractState, round_id: u256) { - // // Refund all contributions for a specific round - // let mut member_index = 0; - // while member_index < self.member_count.read() { - // let member = self.member_by_index.read(member_index); - // if self.members.read(member) { - // let contribution = self.member_contributions.read((round_id, member)); - // if contribution.amount > 0 { - // self.transfer_tokens_to_member(member, contribution.amount); - // } - // } - // member_index += 1; - // } - // } - - // fn update_round_analytics(ref self: ContractState, round_id: u256, status: RoundStatus) { - // // Update analytics when round status changes - // let mut analytics = self.contribution_analytics.read(); - - // match status { - // RoundStatus::Completed => { - // analytics.successful_rounds += 1; - // analytics.total_rounds += 1; - // }, - // RoundStatus::Cancelled => { - // analytics.failed_rounds += 1; - // analytics.total_rounds += 1; - // }, - // _ => {}, - // } - - // self.contribution_analytics.write(analytics); - // } - - // fn is_token_supported(self: @ContractState, token: ContractAddress) -> bool { - // // Check if token is supported for payments - // let payment_config = self.payment_config.read(); - // let supported_tokens = payment_config.supported_tokens; - - // let mut i = 0; - // while i < supported_tokens.len() { - // if supported_tokens[i] == token { - // return true; - // } - // i += 1; - // } - // false - // } - - // fn calculate_next_payment_date(self: @ContractState, frequency: PaymentFrequency) -> u64 { - // // Calculate next payment date based on frequency - // let current_time = get_block_timestamp(); - - // match frequency { - // PaymentFrequency::Once => current_time, - // PaymentFrequency::Daily => current_time + 86400, // 24 hours - // PaymentFrequency::Weekly => current_time + 604800, // 7 days - // PaymentFrequency::Monthly => current_time + 2592000 // 30 days - // } - // } - - // fn process_contribution( - // ref self: ContractState, round_id: u256, member: ContractAddress, amount: u256, - // ) { - // // Process a contribution for a round - // let mut round = self.rounds.read(round_id); - // round.total_contributions += amount; - // self.rounds.write(round_id, round); - - // // Update member contribution record - // let contribution = MemberContribution { - // member, amount, contributed_at: get_block_timestamp(), - // }; - // self.member_contributions.write((round_id, member), contribution); - // } - - // fn calculate_reliability_bonus( - // self: @ContractState, current_rating: u8, is_early: bool, - // ) -> u8 { - // // Calculate reliability rating bonus for early payments - // if is_early && current_rating < 100 { - // return current_rating + 5; // +5 points for early payment - // } - // current_rating - // } - - // fn get_token_price_from_oracle(self: @ContractState, token: ContractAddress) -> u256 { - // // Get token price from oracle (simplified implementation) - // // In real implementation, this would call an oracle contract - // // For now, return a default price - // 1000000000000000000 // 1.0 in wei format - // } - - // fn is_round_scheduled(self: @ContractState, round_id: u256) -> bool { - // let scheduled_round = self - // .scheduled_rounds - // .read(round_id); // Reads the scheduled round data - // // Checks if the round_id field of the struct is non-zero, indicating it has been set - // scheduled_round.round_id > 0 - // } - - // // Ensure all components update related state consistently - // fn complete_round(ref self: ContractState, round_id: u256) { - // self.ownable.assert_only_owner(); // Or triggered by an authorized keeper - // self.emergency.assert_not_paused(); // Round completion should happen when not paused - - // let mut round = self.rounds.read(round_id); // Reads the round data - // assert( - // round.status == RoundStatus::Active, 'Round not active', - // ); // Ensures round is in active state - // assert( - // get_block_timestamp() >= round.deadline, 'Round not expired', - // ); // Ensures deadline has passed - - // // Update round status to Completed - // round.status = RoundStatus::Completed; - // self.rounds.write(round_id, round); // Writes the updated round status - - // // Update scheduled round if it exists - // if self.is_round_scheduled(round_id) { - // let mut scheduled_round = self.scheduled_rounds.read(round_id); - // scheduled_round.status = RoundStatus::Completed; - // self.scheduled_rounds.write(round_id, scheduled_round); - // } - - // // Update analytics related to this round - // self - // .update_round_analytics( - // round_id, RoundStatus::Completed, - // ); // Calls internal analytics helper - - // // Transfer funds to the designated recipient - // let total_contributions_for_round = self - // .enhanced_contribution_internal - // ._get_total_contributions_for_round(round_id); // Get total contributions - // self - // .transfer_tokens_to_member( - // round.recipient, total_contributions_for_round, - // ); // Calls internal token transfer helper - - // // Apply penalties for missed contributions in this round - // self - // .penalty_internal - // ._apply_missed_contribution_penalties(round_id); // Calls internal penalty helper - - // self - // .emit( - // Event::RoundCompleted( - // RoundCompleted { - // round_id, - // recipient: round.recipient, - // total_amount: total_contributions_for_round, - // member_count: self.member_count.read(), - // completion_time: get_block_timestamp(), - // }, - // ), - // ); // Emits an event - // } - - // // Add penalty pool distribution logic - // fn distribute_penalty_pool(ref self: ContractState) { - // self.ownable.assert_only_owner(); // Only owner should trigger distribution - // self.emergency.assert_not_paused(); // Distribution should happen when not paused - - // let penalty_pool_amount = self.penalty_pool.read(); // Reads the total penalty pool - // if penalty_pool_amount == 0 { - // return; - // } - - // let mut total_compliant_contributions = 0; - // let mut compliant_members_list = array![]; // Use Array to collect compliant members - - // // Calculate total contributions from compliant members and collect their addresses - // let mut member_index = 0; - // let total_members = self - // .member_count - // .read(); // Assuming member_count tracks total registered members - // while member_index < total_members { - // let member_address = self - // .member_by_index - // .read(member_index); // Get member address by index - // let member_profile = self - // .member_profiles - // .read(member_address); // Read member profile - - // // A member is compliant if not banned and has a good credit score (example) - // if !member_profile.is_banned - // && member_profile.credit_score >= 80 { // Assuming is_banned in MemberProfile - // let member_contribution = self - // .calculate_member_total_contribution( - // member_address, - // ); // Calls internal helper - // total_compliant_contributions += member_contribution; - // compliant_members_list.append(member_address); - // } - // member_index += 1; - // } - - // if total_compliant_contributions > 0 { - // // Distribute penalty pool proportionally - // let mut distributed_count = 0; - // let mut i = 0; - // while i < compliant_members_list.len() { - // let member_address = *compliant_members_list.at(i); - // let member_contribution = self - // .calculate_member_total_contribution( - // member_address, - // ); // Calls internal helper - // let share = (member_contribution * penalty_pool_amount) - // / total_compliant_contributions; - - // if share > 0 { - // self - // .transfer_tokens_to_member( - // member_address, share, - // ); // Calls internal token transfer helper - // distributed_count += 1; - // } - // i += 1; - // } - - // // Reset penalty pool - // self.penalty_pool.write(0); // Resets the penalty pool - - // self - // .emit( - // Event::PenaltyPoolDistributed( - // PenaltyPoolDistributed { - // total_amount: penalty_pool_amount, - // recipient_count: distributed_count, - // distribution_type: 'proportional', - // timestamp: get_block_timestamp(), - // }, - // ), - // ); // Emits an event - // } - // } - // } - - // // Implementation of Internal Traits for Enhanced Components - // impl PenaltyInternalImpl of PenaltyInternalTrait { - // fn _apply_late_fee( - // ref self: ContractState, member: ContractAddress, round_id: u256, amount: u256, - // ) { - // // Call the existing penalty function - // self.penalty.apply_late_fee(member, round_id); - // } - - // fn _add_strike(ref self: ContractState, member: ContractAddress, round_id: u256) { - // // Call the existing penalty function - // self.penalty.add_strike(member, round_id); - // } - - // fn _remove_strike(ref self: ContractState, member: ContractAddress) { - // // Call the existing penalty function - // self.penalty.remove_strike(member); - // } - - // fn _ban_member(ref self: ContractState, member: ContractAddress) { - // // Call the existing penalty function - // self.penalty.ban_member(member); - // } - - // fn _unban_member(ref self: ContractState, member: ContractAddress) { - // // Call the existing penalty function - // self.penalty.unban_member(member); - // } - - // fn _apply_missed_contribution_penalties(ref self: ContractState, round_id: u256) { - // // Apply penalties for missed contributions in a round - // let round = self.rounds.read(round_id); - // let mut member_index = 0; - // let total_members = self.member_count.read(); - - // while member_index < total_members { - // let member_address = self.member_by_index.read(member_index); - // if self.members.read(member_address) { - // let contribution = self.member_contributions.read((round_id, member_address)); - // if contribution.amount == 0 { - // // Member missed contribution - apply penalty - // self.penalty.add_strike(member_address, round_id); - // } - // } - // member_index += 1; - // } - // } - // } - - // impl EnhancedContributionInternalImpl of EnhancedContributionInternalTrait { - // fn _process_contribution( - // ref self: ContractState, round_id: u256, member: ContractAddress, amount: u256, - // ) { - // // Process a contribution for a round - // let mut round = self.rounds.read(round_id); - // round.total_contributions += amount; - // self.rounds.write(round_id, round); - - // // Update member contribution record - // let contribution = MemberContribution { - // member, amount, contributed_at: get_block_timestamp(), - // }; - // self.member_contributions.write((round_id, member), contribution); - // } - - // fn _get_total_contributions_for_round(self: @ContractState, round_id: u256) -> u256 { - // let round = self.rounds.read(round_id); - // round.total_contributions - // } - - // fn _is_contribution_late( - // self: @ContractState, round_id: u256, member: ContractAddress, - // ) -> bool { - // let round = self.rounds.read(round_id); - // let contribution = self.member_contributions.read((round_id, member)); - // if contribution.amount == 0 { - // return false; // No contribution made - // } - - // let current_time = get_block_timestamp(); - // current_time > round.deadline - // } - // } - - // impl MemberProfileInternalImpl of MemberProfileInternalTrait { - // fn _update_profile_after_contribution( - // ref self: ContractState, member: ContractAddress, amount: u256, - // ) { - // let mut profile = self.member_profiles.read(member); - - // // Update contribution statistics - // profile.total_contributions += amount; - - // // Update credit score based on payment timing - // let current_time = get_block_timestamp(); - // let round = self.get_current_active_round(); - // if round > 0 { - // let round_data = self.rounds.read(round); - // if current_time <= round_data.deadline { - // // On-time payment - boost credit score - // if profile.credit_score < 100 { - // profile.credit_score += 2; - // } - // } - // } - - // self.member_profiles.write(member, profile); - // } - - // fn _calculate_member_total_contribution( - // self: @ContractState, member: ContractAddress, - // ) -> u256 { - // self.calculate_member_total_contribution(member) - // } - - // fn _update_reliability_rating( - // ref self: ContractState, member: ContractAddress, new_rating: u8, - // ) { - // let mut profile = self.member_profiles.read(member); - // profile.reliability_rating = new_rating; - // self.member_profiles.write(member, profile); - // } - // } - - // impl AnalyticsInternalImpl of AnalyticsInternalTrait { - // fn _update_round_analytics(ref self: ContractState, round_id: u256, status: RoundStatus) { - // self.update_round_analytics(round_id, status); - // } - - // fn _update_member_performance_for_round(ref self: ContractState, round_id: u256) { - // // Update member performance analytics for a specific round - // let round = self.rounds.read(round_id); - // let mut member_index = 0; - // let total_members = self.member_count.read(); - - // while member_index < total_members { - // let member_address = self.member_by_index.read(member_index); - // if self.members.read(member_address) { - // let contribution = self.member_contributions.read((round_id, member_address)); - // let mut member_analytics = self.member_analytics.read(member_address); - - // if contribution.amount > 0 { - // member_analytics.total_contributions += contribution.amount; - // if contribution.contributed_at <= round.deadline { - // member_analytics.on_time_payments += 1; - // } else { - // member_analytics.late_payments += 1; - // } - // } else if round.status == RoundStatus::Completed - // || round.status == RoundStatus::Cancelled { - // member_analytics.missed_payments += 1; - // } - - // member_analytics.last_updated = get_block_timestamp(); - // self.member_analytics.write(member_address, member_analytics); - // } - // member_index += 1; - // } - // } - - // fn _calculate_system_health(self: @ContractState) -> u8 { - // self.calculate_system_health() - // } - // } - - // impl TokenTransferInternalImpl of TokenTransferInternalTrait { - // fn _transfer_tokens_to_member( - // ref self: ContractState, member: ContractAddress, amount: u256, - // ) { - // self.transfer_tokens_to_member(member, amount); - // } - - // fn _transfer_tokens_to_address( - // ref self: ContractState, recipient: ContractAddress, amount: u256, - // ) { - // self.transfer_tokens_to_address(recipient, amount); - // } - - // fn _transfer_specific_tokens_to_address( - // ref self: ContractState, - // token: ContractAddress, - // recipient: ContractAddress, - // amount: u256, - // ) { - // self.transfer_specific_tokens_to_address(token, recipient, amount); - // } - // } + fn get_member_status(self: @ContractState, member: ContractAddress) -> bool { + self.members.read(member) + } - #[generate_trait] - impl InternalFunctions of InternalFunctionsTrait { - fn _validate_kyc_and_limits(self: @ContractState, user: ContractAddress, amount: u256) { - // Check KYC validity - assert(IStarkRemitImpl::is_kyc_valid(self, user), KYCErrors::INVALID_KYC_STATUS); - - // Check transaction limits - let kyc_data = self.user_kyc_data.read(user); - let level_u8 = self._kyc_level_to_u8(kyc_data.level); - - // Check single transaction limit - let single_limit = self.single_limits.read(level_u8); - assert(amount <= single_limit, KYCErrors::SINGLE_TX_LIMIT_EXCEEDED); - - // Check daily limit - let daily_limit = self.daily_limits.read(level_u8); - let current_usage = self._get_daily_usage(user); - assert(current_usage + amount <= daily_limit, KYCErrors::DAILY_LIMIT_EXCEEDED); - } - - fn _get_daily_usage(self: @ContractState, user: ContractAddress) -> u256 { - let current_time = get_block_timestamp(); - let last_reset = self.last_reset.read(user); - - // Reset if it's a new day (86400 seconds = 24 hours) - if current_time > last_reset + 86400 { - return 0; - } - - self.daily_usage.read(user) + fn get_member_count(self: @ContractState) -> u32 { + self.member_count.read() + } + + fn get_member_by_index(self: @ContractState, index: u32) -> ContractAddress { + self.member_by_index.read(index) + } + } + + // Implementation of IMainContractData trait for analytics component + impl AnalyticsMainContractDataImpl of AnalyticsMainContractData { + fn get_round_data(self: @ContractState, round_id: u256) -> RoundData { + let round = self.rounds.read(round_id); + RoundData { + deadline: round.deadline, + completed_at: round.completed_at, + status: round.status, + total_contributions: round.total_contributions, } - - fn _record_daily_usage(ref self: ContractState, user: ContractAddress, amount: u256) { - let current_time = get_block_timestamp(); - let last_reset = self.last_reset.read(user); - - if current_time > last_reset + 86400 { - // Reset for new day - self.daily_usage.write(user, amount); - self.last_reset.write(user, current_time); - } else { - // Add to current day usage - let current_usage = self.daily_usage.read(user); - self.daily_usage.write(user, current_usage + amount); - } + } + + fn get_member_contribution_data( + self: @ContractState, round_id: u256, member: ContractAddress, + ) -> MemberContribution { + self.member_contributions.read((round_id, member)) + } + + fn get_member_status(self: @ContractState, member: ContractAddress) -> bool { + self.members.read(member) + } + + fn get_member_count(self: @ContractState) -> u32 { + self.member_count.read() + } + + fn get_member_by_index(self: @ContractState, index: u32) -> ContractAddress { + self.member_by_index.read(index) + } + + fn get_round_ids(self: @ContractState) -> u256 { + self.round_ids.read() + } + } + + // Implementation of IMainContractData trait for member profile component + impl MemberProfileMainContractDataImpl of MemberProfileMainContractData { + fn get_member_status(self: @ContractState, member: ContractAddress) -> bool { + self.members.read(member) + } + + fn get_member_count(self: @ContractState) -> u32 { + self.member_count.read() + } + + fn get_member_by_index(self: @ContractState, index: u32) -> ContractAddress { + self.member_by_index.read(index) + } + } + + + #[generate_trait] + impl InternalFunctions of InternalFunctionsTrait { + fn _validate_kyc_and_limits(self: @ContractState, user: ContractAddress, amount: u256) { + // Check KYC validity + assert(IStarkRemitImpl::is_kyc_valid(self, user), KYCErrors::INVALID_KYC_STATUS); + + // Check transaction limits + let kyc_data = self.user_kyc_data.read(user); + let level_u8 = self._kyc_level_to_u8(kyc_data.level); + + // Check single transaction limit + let single_limit = self.single_limits.read(level_u8); + assert(amount <= single_limit, KYCErrors::SINGLE_TX_LIMIT_EXCEEDED); + + // Check daily limit + let daily_limit = self.daily_limits.read(level_u8); + let current_usage = self._get_daily_usage(user); + assert(current_usage + amount <= daily_limit, KYCErrors::DAILY_LIMIT_EXCEEDED); + } + + fn _get_daily_usage(self: @ContractState, user: ContractAddress) -> u256 { + let current_time = get_block_timestamp(); + let last_reset = self.last_reset.read(user); + + // Reset if it's a new day (86400 seconds = 24 hours) + if current_time > last_reset + 86400 { + return 0; } - - fn _kyc_level_to_u8(self: @ContractState, level: KycLevel) -> u8 { - match level { - KycLevel::None => 0, - KycLevel::Basic => 1, - KycLevel::Enhanced => 2, - KycLevel::Premium => 3, - } + + self.daily_usage.read(user) + } + + fn _record_daily_usage(ref self: ContractState, user: ContractAddress, amount: u256) { + let current_time = get_block_timestamp(); + let last_reset = self.last_reset.read(user); + + if current_time > last_reset + 86400 { + // Reset for new day + self.daily_usage.write(user, amount); + self.last_reset.write(user, current_time); + } else { + // Add to current day usage + let current_usage = self.daily_usage.read(user); + self.daily_usage.write(user, current_usage + amount); } - - fn _set_default_transaction_limits(ref self: ContractState) { - // None level - very restricted - self.daily_limits.write(0, 100_000_000_000_000_000); // 0.1 tokens - self.single_limits.write(0, 50_000_000_000_000_000); // 0.05 tokens - - // Basic level - moderate limits - self.daily_limits.write(1, 1000_000_000_000_000_000_000); // 1,000 tokens - self.single_limits.write(1, 500_000_000_000_000_000_000); // 500 tokens - - // Enhanced level - higher limits - self.daily_limits.write(2, 10000_000_000_000_000_000_000); // 10,000 tokens - self.single_limits.write(2, 5000_000_000_000_000_000_000); // 5,000 tokens - - // Premium level - maximum limits - self.daily_limits.write(3, 100000_000_000_000_000_000_000); // 100,000 tokens - self.single_limits.write(3, 50000_000_000_000_000_000_000); // 50,000 tokens + } + + fn _kyc_level_to_u8(self: @ContractState, level: KycLevel) -> u8 { + match level { + KycLevel::None => 0, + KycLevel::Basic => 1, + KycLevel::Enhanced => 2, + KycLevel::Premium => 3, } - - fn _calculate_total_allocated_tokens(self: @ContractState, token: ContractAddress) -> u256 { - // Calculate total allocated tokens = member balances + tokens locked in ongoing rounds - let mut total_allocated = 0_u256; - - // Get the primary token address for comparison - let primary_token = self.token_address.read(); - assert(token == primary_token, 'Only primary token supported'); - - // If this is the primary token, calculate allocated tokens - if token == primary_token { - // Add member balances for the primary token - let member_count = self.member_count.read(); - let mut i = 0_u32; - while i < member_count { - let member = self.member_by_index.read(i); - if self.members.read(member) { - let member_balance = self.balances.read(member); - total_allocated += member_balance; - } - i += 1; + } + + fn _set_default_transaction_limits(ref self: ContractState) { + // None level - very restricted + self.daily_limits.write(0, 100_000_000_000_000_000); // 0.1 tokens + self.single_limits.write(0, 50_000_000_000_000_000); // 0.05 tokens + + // Basic level - moderate limits + self.daily_limits.write(1, 1000_000_000_000_000_000_000); // 1,000 tokens + self.single_limits.write(1, 500_000_000_000_000_000_000); // 500 tokens + + // Enhanced level - higher limits + self.daily_limits.write(2, 10000_000_000_000_000_000_000); // 10,000 tokens + self.single_limits.write(2, 5000_000_000_000_000_000_000); // 5,000 tokens + + // Premium level - maximum limits + self.daily_limits.write(3, 100000_000_000_000_000_000_000); // 100,000 tokens + self.single_limits.write(3, 50000_000_000_000_000_000_000); // 50,000 tokens + } + + fn _calculate_total_allocated_tokens(self: @ContractState, token: ContractAddress) -> u256 { + // Calculate total allocated tokens = member balances + tokens locked in ongoing rounds + let mut total_allocated = 0_u256; + + // Get the primary token address for comparison + let primary_token = self.token_address.read(); + assert(token == primary_token, 'Only primary token supported'); + + // If this is the primary token, calculate allocated tokens + if token == primary_token { + // Add member balances for the primary token + let member_count = self.member_count.read(); + let mut i = 0_u32; + while i < member_count { + let member = self.member_by_index.read(i); + if self.members.read(member) { + let member_balance = self.balances.read(member); + total_allocated += member_balance; } - - // Add tokens locked in ongoing rounds (active rounds with contributions) - let current_round_id = self.round_ids.read(); - if current_round_id > 0 { - let mut round_id = 1_u256; - while round_id <= current_round_id { - let round = self.rounds.read(round_id); - // Only count active rounds that have contributions - if round.status == RoundStatus::Active && round.total_contributions > 0 { - total_allocated += round.total_contributions; - } - round_id += 1; + i += 1; + } + + // Add tokens locked in ongoing rounds (active rounds with contributions) + let current_round_id = self.round_ids.read(); + if current_round_id > 0 { + let mut round_id = 1_u256; + while round_id <= current_round_id { + let round = self.rounds.read(round_id); + // Only count active rounds that have contributions + if round.status == RoundStatus::Active && round.total_contributions > 0 { + total_allocated += round.total_contributions; } + round_id += 1; } } - total_allocated - } - - fn _record_transfer_history( - ref self: ContractState, - transfer_id: u256, - action: felt252, - actor: ContractAddress, - previous_status: TransferStatus, - new_status: TransferStatus, - details: felt252, - ) { - let current_time = get_block_timestamp(); - - // Create history entry - let history = TransferHistory { - transfer_id, - action, - actor, - timestamp: current_time, - previous_status, - new_status, - details, - }; - - // Store in transfer history - let history_count = self.transfer_history_count.read(transfer_id); - self.transfer_history.write((transfer_id, history_count), history); - self.transfer_history_count.write(transfer_id, history_count + 1); - - // Store in actor history - let actor_count = self.actor_history_count.read(actor); - self.actor_history.write((actor, actor_count), (transfer_id, history_count)); - self.actor_history_count.write(actor, actor_count + 1); - - // Store in action history - let action_count = self.action_history_count.read(action); - self.action_history.write((action, action_count), (transfer_id, history_count)); - self.action_history_count.write(action, action_count + 1); - - // Emit event - self - .emit( - TransferHistoryRecorded { transfer_id, action, actor, timestamp: current_time }, - ); - } - - // Generates and stores a new unique group ID for a savings group - // Returns the newly generated group ID - fn _new_group_id(ref self: ContractState) -> u64 { - let group_id = self.group_count.read(); - - self.group_count.write(group_id + 1); - - group_id - } - - // Helper functions for emergency operations - fn get_contract_token_balance(self: @ContractState) -> u256 { - // Get contract's balance of the primary token - let token_address = self.token_address.read(); - let erc20_dispatcher = IERC20Dispatcher { contract_address: token_address }; - erc20_dispatcher.balance_of(starknet::get_contract_address()) - } - - fn get_contract_token_balance_specific( - self: @ContractState, token: ContractAddress, - ) -> u256 { - // Get contract's balance of a specific token - let erc20_dispatcher = IERC20Dispatcher { contract_address: token }; - erc20_dispatcher.balance_of(starknet::get_contract_address()) - } - - fn transfer_tokens_to_member( - ref self: ContractState, member: ContractAddress, amount: u256, - ) { - // Transfer tokens to a member - let token_address = self.token_address.read(); - let erc20_dispatcher = IERC20Dispatcher { contract_address: token_address }; - assert(erc20_dispatcher.transfer(member, amount), 'Transfer failed'); } - - fn transfer_tokens_to_address( - ref self: ContractState, recipient: ContractAddress, amount: u256, - ) { - // Transfer tokens to any address - let token_address = self.token_address.read(); - let erc20_dispatcher = IERC20Dispatcher { contract_address: token_address }; - assert(erc20_dispatcher.transfer(recipient, amount), 'Transfer failed'); - } - - fn transfer_specific_tokens_to_address( - ref self: ContractState, - token: ContractAddress, - recipient: ContractAddress, - amount: u256, - ) { - // Transfer specific tokens to an address - let erc20_dispatcher = IERC20Dispatcher { contract_address: token }; - assert(erc20_dispatcher.transfer(recipient, amount), 'Transfer failed'); + total_allocated + } + + fn _record_transfer_history( + ref self: ContractState, + transfer_id: u256, + action: felt252, + actor: ContractAddress, + previous_status: TransferStatus, + new_status: TransferStatus, + details: felt252, + ) { + let current_time = get_block_timestamp(); + + // Create history entry + let history = TransferHistory { + transfer_id, + action, + actor, + timestamp: current_time, + previous_status, + new_status, + details, + }; + + // Store in transfer history + let history_count = self.transfer_history_count.read(transfer_id); + self.transfer_history.write((transfer_id, history_count), history); + self.transfer_history_count.write(transfer_id, history_count + 1); + + // Store in actor history + let actor_count = self.actor_history_count.read(actor); + self.actor_history.write((actor, actor_count), (transfer_id, history_count)); + self.actor_history_count.write(actor, actor_count + 1); + + // Store in action history + let action_count = self.action_history_count.read(action); + self.action_history.write((action, action_count), (transfer_id, history_count)); + self.action_history_count.write(action, action_count + 1); + + // Emit event + self + .emit( + TransferHistoryRecorded { transfer_id, action, actor, timestamp: current_time }, + ); + } + + // Generates and stores a new unique group ID for a savings group + // Returns the newly generated group ID + fn _new_group_id(ref self: ContractState) -> u64 { + let group_id = self.group_count.read(); + + self.group_count.write(group_id + 1); + + group_id + } + + // Helper functions for emergency operations + fn get_contract_token_balance(self: @ContractState) -> u256 { + // Get contract's balance of the primary token + let token_address = self.token_address.read(); + let erc20_dispatcher = IERC20Dispatcher { contract_address: token_address }; + erc20_dispatcher.balance_of(starknet::get_contract_address()) + } + + fn get_contract_token_balance_specific( + self: @ContractState, token: ContractAddress, + ) -> u256 { + // Get contract's balance of a specific token + let erc20_dispatcher = IERC20Dispatcher { contract_address: token }; + erc20_dispatcher.balance_of(starknet::get_contract_address()) + } + + fn transfer_tokens_to_member( + ref self: ContractState, member: ContractAddress, amount: u256, + ) { + // Transfer tokens to a member + let token_address = self.token_address.read(); + let erc20_dispatcher = IERC20Dispatcher { contract_address: token_address }; + assert(erc20_dispatcher.transfer(member, amount), 'Transfer failed'); + } + + fn transfer_tokens_to_address( + ref self: ContractState, recipient: ContractAddress, amount: u256, + ) { + // Transfer tokens to any address + let token_address = self.token_address.read(); + let erc20_dispatcher = IERC20Dispatcher { contract_address: token_address }; + assert(erc20_dispatcher.transfer(recipient, amount), 'Transfer failed'); + } + + fn transfer_specific_tokens_to_address( + ref self: ContractState, + token: ContractAddress, + recipient: ContractAddress, + amount: u256, + ) { + // Transfer specific tokens to an address + let erc20_dispatcher = IERC20Dispatcher { contract_address: token }; + assert(erc20_dispatcher.transfer(recipient, amount), 'Transfer failed'); + } + + fn calculate_member_total_contribution( + self: @ContractState, member: ContractAddress, + ) -> u256 { + // Calculate total contributions across all rounds for a member + let mut total = 0; + let mut round_id = 1; + while round_id <= self.round_ids.read() { + let contribution = self.member_contributions.read((round_id, member)); + total += contribution.amount; + round_id += 1; } - - fn calculate_member_total_contribution( - self: @ContractState, member: ContractAddress, - ) -> u256 { - // Calculate total contributions across all rounds for a member - let mut total = 0; - let mut round_id = 1; - while round_id <= self.round_ids.read() { + total + } + + fn refund_round_contributions(ref self: ContractState, round_id: u256) { + // Refund all contributions for a specific round + let mut member_index = 0; + while member_index < self.member_count.read() { + let member = self.member_by_index.read(member_index); + if self.members.read(member) { let contribution = self.member_contributions.read((round_id, member)); - total += contribution.amount; - round_id += 1; - } - total - } - - fn refund_round_contributions(ref self: ContractState, round_id: u256) { - // Refund all contributions for a specific round - let mut member_index = 0; - while member_index < self.member_count.read() { - let member = self.member_by_index.read(member_index); - if self.members.read(member) { - let contribution = self.member_contributions.read((round_id, member)); - if contribution.amount > 0 { - self.transfer_tokens_to_member(member, contribution.amount); - } + if contribution.amount > 0 { + self.transfer_tokens_to_member(member, contribution.amount); } - member_index += 1; } + member_index += 1; } - - fn update_round_analytics(ref self: ContractState, round_id: u256, status: RoundStatus) { - // Update analytics when round status changes - // This is a simplified implementation since analytics component is commented out - // In a full implementation, this would update the analytics storage + } + + fn update_round_analytics(ref self: ContractState, round_id: u256, status: RoundStatus) { + // Update analytics when round status changes + let round = self.rounds.read(round_id); + let participant_count = self.member_count.read(); // Simplified participant count + self + .analytics + ._update_round_metrics( + round_id, status, round.total_contributions, participant_count, + ); + self.analytics._update_contribution_analytics(); + self.analytics._update_system_health(); + } + + // Private helper function to remove member from member list + fn _remove_member_from_list(ref self: ContractState, member: ContractAddress) { + // Check if member is currently active + if !self.members.read(member) { + return; // Already removed } - // Private helper function to remove member from member list - fn _remove_member_from_list(ref self: ContractState, member: ContractAddress) { - // Check if member is currently active - if !self.members.read(member) { - return; // Already removed - } - - // Mark member as inactive - self.members.write(member, false); - - // Decrease member count - let current_count = self.member_count.read(); - self.member_count.write(current_count - 1); - - // Find and remove member from member_by_index - let mut i = 0; - let total_members = current_count; - - while i < total_members { - let member_at_index = self.member_by_index.read(i); - if member_at_index == member { - // Found the member, remove by setting to zero address - self.member_by_index.write(i, 0.try_into().unwrap()); - break; - } - i += 1; + // Mark member as inactive + self.members.write(member, false); + + // Decrease member count + let current_count = self.member_count.read(); + self.member_count.write(current_count - 1); + + // Find and remove member from member_by_index + let mut i = 0; + let total_members = current_count; + + while i < total_members { + let member_at_index = self.member_by_index.read(i); + if member_at_index == member { + // Found the member, remove by setting to zero address + self.member_by_index.write(i, 0.try_into().unwrap()); + break; } + i += 1; } + } - // Private helper function to add member back to member list - fn _add_member_to_list(ref self: ContractState, member: ContractAddress) { - // Check if member is already active - if self.members.read(member) { - return; // Already active - } - - // Mark member as active - self.members.write(member, true); - - // Increase member count - let current_count = self.member_count.read(); - self.member_count.write(current_count + 1); - - // Add member to member_by_index at the end - self.member_by_index.write(current_count, member); + // Internal function to update analytics after contribution + fn _update_analytics_after_contribution( + ref self: ContractState, + member: ContractAddress, + round_id: u256, + amount: u256, + is_on_time: bool, + ) { + // Update member analytics + self.analytics._update_member_analytics(member, is_on_time, amount, round_id); + + // Update round performance metrics + let round = self.rounds.read(round_id); + let participant_count = 1; // Simplified - would need proper participant count logic + self + .analytics + ._update_round_metrics( + round_id, round.status, round.total_contributions, participant_count, + ); + + // Update contribution analytics + self.analytics._update_contribution_analytics(); + + // Update system health + self.analytics._update_system_health(); + } + + // Private helper function to add member back to member list + fn _add_member_to_list(ref self: ContractState, member: ContractAddress) { + // Check if member is already active + if self.members.read(member) { + return; // Already active } - } + + // Mark member as active + self.members.write(member, true); + + // Increase member count + let current_count = self.member_count.read(); + self.member_count.write(current_count + 1); + + // Add member to member_by_index at the end + self.member_by_index.write(current_count, member); + } + } } diff --git a/tests/test_analytics_component.cairo b/tests/test_analytics_component.cairo index 64a5bda..0c7c68f 100644 --- a/tests/test_analytics_component.cairo +++ b/tests/test_analytics_component.cairo @@ -1,197 +1,191 @@ -// use starknet::ContractAddress; -// use starknet::testing::{set_caller_address, set_block_timestamp}; -// use starknet::contract_address_const; -// use starkremit_contract::component::analytics::{ -// analytics_component, IAnalyticsDispatcher, IAnalyticsDispatcherTrait, -// ContributionAnalytics, MemberAnalytics, RoundPerformanceMetrics, FinancialReport, SystemHealthMetrics -// }; - -// const ADMIN: felt252 = 0x123; -// const MEMBER1: felt252 = 0x789; -// const MEMBER2: felt252 = 0xABC; - -// fn setup() -> ContractAddress { -// let admin_address = contract_address_const::(); -// let contract_address = contract_address_const::<0x1>(); - -// set_caller_address(admin_address); -// set_block_timestamp(1000); - -// contract_address -// } - -// #[test] -// fn test_generate_contribution_report() { -// let contract_address = setup(); -// let analytics = IAnalyticsDispatcher { contract_address }; - -// let report = analytics.generate_contribution_report(); - -// // Initial report should have default values -// assert!(report.total_rounds == 0, "Initial total rounds should be 0"); -// assert!(report.successful_rounds == 0, "Initial successful rounds should be 0"); -// assert!(report.failed_rounds == 0, "Initial failed rounds should be 0"); -// } - -// #[test] -// fn test_get_member_performance_new_member() { -// let contract_address = setup(); -// let analytics = IAnalyticsDispatcher { contract_address }; -// let member_address = contract_address_const::(); - -// let performance = analytics.get_member_performance(member_address); - -// // New member should have default values -// assert!(performance.total_contributions == 0, "New member should have 0 contributions"); -// assert!(performance.on_time_payments == 0, "New member should have 0 on-time payments"); -// assert!(performance.late_payments == 0, "New member should have 0 late payments"); -// assert!(performance.missed_payments == 0, "New member should have 0 missed payments"); -// assert!(performance.reliability_score == 50, "New member should have neutral reliability score"); -// } - -// #[test] -// fn test_calculate_system_health() { -// let contract_address = setup(); -// let analytics = IAnalyticsDispatcher { contract_address }; - -// let health_score = analytics.calculate_system_health(); - -// // Health score should be between 0 and 100 -// assert!(health_score <= 100, "Health score should not exceed 100"); -// assert!(health_score >= 0, "Health score should not be negative"); -// } - -// #[test] -// fn test_member_performance_tracking() { -// let contract_address = setup(); -// let analytics = IAnalyticsDispatcher { contract_address }; -// let member_address = contract_address_const::(); - -// // Get initial performance -// let initial_performance = analytics.get_member_performance(member_address); -// assert!(initial_performance.reliability_score == 50, "Should start with neutral score"); - -// // Performance should remain consistent on multiple calls -// let second_call = analytics.get_member_performance(member_address); -// assert!(second_call.reliability_score == initial_performance.reliability_score, "Performance should be consistent"); -// } - -// #[test] -// fn test_multiple_members_analytics() { -// let contract_address = setup(); -// let analytics = IAnalyticsDispatcher { contract_address }; -// let member1_address = contract_address_const::(); -// let member2_address = contract_address_const::(); - -// let performance1 = analytics.get_member_performance(member1_address); -// let performance2 = analytics.get_member_performance(member2_address); - -// // Both members should have independent analytics -// assert!(performance1.total_contributions == 0, "Member 1 should start with 0 contributions"); -// assert!(performance2.total_contributions == 0, "Member 2 should start with 0 contributions"); -// assert!(performance1.reliability_score == 50, "Member 1 should have neutral score"); -// assert!(performance2.reliability_score == 50, "Member 2 should have neutral score"); -// } - -// #[test] -// fn test_system_health_consistency() { -// let contract_address = setup(); -// let analytics = IAnalyticsDispatcher { contract_address }; - -// let health1 = analytics.calculate_system_health(); -// let health2 = analytics.calculate_system_health(); - -// // Health should be consistent (assuming no state changes) -// assert!(health1 == health2, "System health should be consistent"); -// } - -// #[test] -// fn test_contribution_report_structure() { -// let contract_address = setup(); -// let analytics = IAnalyticsDispatcher { contract_address }; - -// let report = analytics.generate_contribution_report(); - -// // Verify report structure -// assert!(report.average_completion_time >= 0, "Completion time should be non-negative"); -// assert!(report.total_penalties_collected >= 0, "Penalties should be non-negative"); - -// // Total rounds should equal successful + failed -// assert!( -// report.total_rounds == report.successful_rounds + report.failed_rounds, -// "Total rounds should equal successful plus failed rounds" -// ); -// } - -// #[test] -// fn test_member_analytics_initialization() { -// let contract_address = setup(); -// let analytics = IAnalyticsDispatcher { contract_address }; -// let member_address = contract_address_const::(); - -// let performance = analytics.get_member_performance(member_address); - -// // Verify all fields are properly initialized -// assert!(performance.total_contributions == 0, "Contributions should start at 0"); -// assert!(performance.on_time_payments == 0, "On-time payments should start at 0"); -// assert!(performance.late_payments == 0, "Late payments should start at 0"); -// assert!(performance.missed_payments == 0, "Missed payments should start at 0"); -// assert!(performance.reliability_score == 50, "Reliability should start at neutral"); -// assert!(performance.last_updated > 0, "Last updated should be set"); -// } - -// #[test] -// fn test_system_health_bounds() { -// let contract_address = setup(); -// let analytics = IAnalyticsDispatcher { contract_address }; - -// // Test multiple calls to ensure bounds are maintained -// for _i in 0..10 { -// let health = analytics.calculate_system_health(); -// assert!(health >= 0 && health <= 100, "Health score must be between 0 and 100"); -// } -// } - -// #[test] -// fn test_analytics_timestamp_tracking() { -// let contract_address = setup(); -// let analytics = IAnalyticsDispatcher { contract_address }; -// let member_address = contract_address_const::(); - -// let initial_timestamp = 1000_u64; -// set_block_timestamp(initial_timestamp); - -// let performance = analytics.get_member_performance(member_address); -// assert!(performance.last_updated >= initial_timestamp, "Should have current or later timestamp"); - -// // Move time forward -// set_block_timestamp(2000); - -// let updated_performance = analytics.get_member_performance(member_address); -// // For a new member, the timestamp should be updated -// assert!(updated_performance.last_updated >= 2000, "Should reflect new timestamp"); -// } - -// #[test] -// fn test_contribution_report_reliability_distribution() { -// let contract_address = setup(); -// let analytics = IAnalyticsDispatcher { contract_address }; - -// let report = analytics.generate_contribution_report(); - -// // Reliability distribution array should be valid (though may be empty initially) -// assert!(report.member_reliability_distribution.len() >= 0, "Distribution should be valid array"); -// } - -// #[test] -// fn test_empty_state_analytics() { -// let contract_address = setup(); -// let analytics = IAnalyticsDispatcher { contract_address }; - -// // Test that analytics work correctly with no data -// let report = analytics.generate_contribution_report(); -// let health = analytics.calculate_system_health(); - -// assert!(report.total_rounds == 0, "No rounds should exist initially"); -// assert!(health >= 0, "Health should still be calculable"); -// } \ No newline at end of file +use starknet::ContractAddress; +use starknet::testing::{set_block_timestamp, set_caller_address}; +use starkremit_contract::base::types::{MemberContribution, RoundData, RoundStatus}; +use starkremit_contract::component::analytics::analytics_component::{AnalyticsComponent, Storage}; +use starkremit_contract::component::analytics::{IAnalytics, IMainContractData}; + +// Mock implementation of IMainContractData for testing +#[starknet::interface] +trait IMockMainContract { + fn get_round_data(self: @TContractState, round_id: u256) -> RoundData; + fn get_member_contribution_data( + self: @TContractState, round_id: u256, member: ContractAddress, + ) -> MemberContribution; + fn get_member_status(self: @TContractState, member: ContractAddress) -> bool; + fn get_member_count(self: @TContractState) -> u32; + fn get_member_by_index(self: @TContractState, index: u32) -> ContractAddress; + fn get_round_ids(self: @TContractState) -> u256; +} + +#[starknet::contract] +mod MockMainContract { + use starknet::storage::Map; + use super::*; + + #[storage] + struct Storage { + rounds: Map, + member_contributions: Map<(u256, ContractAddress), MemberContribution>, + members: Map, + member_count: u32, + member_by_index: Map, + round_ids: u256, + } + + #[constructor] + fn constructor(ref self: ContractState) { + self.member_count.write(2); + self.member_by_index.write(0, 0x123.try_into().unwrap()); + self.member_by_index.write(1, 0x456.try_into().unwrap()); + self.round_ids.write(1); + + // Setup test data + let round_data = RoundData { + deadline: 1000, completed_at: 0, status: RoundStatus::Active, total_contributions: 1000, + }; + self.rounds.write(1, round_data); + + let member_contribution = MemberContribution { + member: 0x123.try_into().unwrap(), amount: 500, contributed_at: 500, + }; + self.member_contributions.write((1, 0x123.try_into().unwrap()), member_contribution); + + self.members.write(0x123.try_into().unwrap(), true); + self.members.write(0x456.try_into().unwrap(), true); + } + + impl MockMainContractImpl of IMockMainContract { + fn get_round_data(self: @ContractState, round_id: u256) -> RoundData { + self.rounds.read(round_id) + } + + fn get_member_contribution_data( + self: @ContractState, round_id: u256, member: ContractAddress, + ) -> MemberContribution { + self.member_contributions.read((round_id, member)) + } + + fn get_member_status(self: @ContractState, member: ContractAddress) -> bool { + self.members.read(member) + } + + fn get_member_count(self: @ContractState) -> u32 { + self.member_count.read() + } + + fn get_member_by_index(self: @ContractState, index: u32) -> ContractAddress { + self.member_by_index.read(index) + } + + fn get_round_ids(self: @ContractState) -> u256 { + self.round_ids.read() + } + } +} + +#[test] +fn test_analytics_initialization() { + let mut state = AnalyticsComponent::contract_state_for_testing(); + let admin: ContractAddress = 0x123.try_into().unwrap(); + + set_caller_address(admin); + set_block_timestamp(1000); + + AnalyticsComponent::InternalImpl::initializer(ref state, admin); + + // Test that analytics is properly initialized + let analytics = IAnalytics::get_contribution_analytics(@state); + assert(analytics.total_rounds == 0, 'Should start with 0 rounds'); + assert(analytics.successful_rounds == 0, 'Should start with 0 successful rounds'); + assert(analytics.failed_rounds == 0, 'Should start with 0 failed rounds'); +} + +#[test] +fn test_member_analytics_update() { + let mut state = AnalyticsComponent::contract_state_for_testing(); + let admin: ContractAddress = 0x123.try_into().unwrap(); + let member: ContractAddress = 0x456.try_into().unwrap(); + + set_caller_address(admin); + set_block_timestamp(1000); + + AnalyticsComponent::InternalImpl::initializer(ref state, admin); + + // Update member analytics + AnalyticsComponent::InternalImpl::_update_member_analytics( + ref state, member, true, // payment_made + 1000, // amount + 1 // round_id + ); + + // Check member analytics + let member_analytics = IAnalytics::get_member_analytics(@state, member); + assert(member_analytics.total_contributions == 1000, 'Total contributions should be 1000'); + assert(member_analytics.on_time_payments == 1, 'On time payments should be 1'); + assert(member_analytics.missed_payments == 0, 'Missed payments should be 0'); + assert(member_analytics.reliability_score == 100, 'Reliability score should be 100'); +} + +#[test] +fn test_round_performance_update() { + let mut state = AnalyticsComponent::contract_state_for_testing(); + let admin: ContractAddress = 0x123.try_into().unwrap(); + + set_caller_address(admin); + set_block_timestamp(1000); + + AnalyticsComponent::InternalImpl::initializer(ref state, admin); + + // Update round metrics + AnalyticsComponent::InternalImpl::_update_round_metrics( + ref state, + 1, // round_id + RoundStatus::Completed, + 1000, // total_contributions + 2 // participant_count + ); + + // Check round performance + let round_performance = IAnalytics::get_round_performance(@state, 1); + assert(round_performance.round_id == 1, 'Round ID should be 1'); + assert(round_performance.total_contributions == 1000, 'Total contributions should be 1000'); + assert(round_performance.participant_count == 2, 'Participant count should be 2'); +} + +#[test] +fn test_system_health_update() { + let mut state = AnalyticsComponent::contract_state_for_testing(); + let admin: ContractAddress = 0x123.try_into().unwrap(); + + set_caller_address(admin); + set_block_timestamp(1000); + + AnalyticsComponent::InternalImpl::initializer(ref state, admin); + + // Update system health + AnalyticsComponent::InternalImpl::_update_system_health(ref state); + + // Check system health + let system_health = IAnalytics::get_system_health(@state); + assert(system_health.last_health_check == 1000, 'Last health check should be 1000'); + assert(system_health.security_score > 0, 'Security score should be positive'); +} + +#[test] +fn test_financial_report_generation() { + let mut state = AnalyticsComponent::contract_state_for_testing(); + let admin: ContractAddress = 0x123.try_into().unwrap(); + + set_caller_address(admin); + set_block_timestamp(1000); + + AnalyticsComponent::InternalImpl::initializer(ref state, admin); + + // Generate financial report + let report = IAnalytics::generate_financial_report(@state, 0, 2000); + + // Check report + assert(report.period_start == 0, 'Period start should be 0'); + assert(report.period_end == 2000, 'Period end should be 2000'); + assert(report.active_members == 0, 'Active members should be 0 initially'); +} diff --git a/tests/test_auto_schedule_component.cairo b/tests/test_auto_schedule_component.cairo index 7376685..a035498 100644 --- a/tests/test_auto_schedule_component.cairo +++ b/tests/test_auto_schedule_component.cairo @@ -15,10 +15,10 @@ // fn setup() -> ContractAddress { // let admin_address = contract_address_const::(); // let contract_address = contract_address_const::<0x1>(); - + // set_caller_address(admin_address); // set_block_timestamp(1000); - + // contract_address // } @@ -36,10 +36,10 @@ // fn test_setup_auto_schedule() { // let contract_address = setup(); // let auto_schedule = IAutoScheduleDispatcher { contract_address }; - + // let config = get_default_auto_schedule_config(); // auto_schedule.setup_auto_schedule(config); - + // let retrieved_config = auto_schedule.get_auto_schedule_config(); // assert!(retrieved_config.round_duration_days == 30, "Round duration should match"); // assert!(retrieved_config.auto_activation_enabled, "Auto activation should be enabled"); @@ -50,10 +50,10 @@ // fn test_get_current_active_round() { // let contract_address = setup(); // let auto_schedule = IAutoScheduleDispatcher { contract_address }; - + // let config = get_default_auto_schedule_config(); // auto_schedule.setup_auto_schedule(config); - + // // Initially no active round // let active_round_id = auto_schedule.get_current_active_round(); // assert!(active_round_id == 0, "No active round should exist initially"); @@ -63,10 +63,10 @@ // fn test_get_next_scheduled_rounds() { // let contract_address = setup(); // let auto_schedule = IAutoScheduleDispatcher { contract_address }; - + // let config = get_default_auto_schedule_config(); // auto_schedule.setup_auto_schedule(config); - + // // Get next scheduled rounds // let scheduled_rounds = auto_schedule.get_next_scheduled_rounds(5); // // Initially should be empty or have few rounds @@ -77,13 +77,13 @@ // fn test_maintain_rolling_schedule() { // let contract_address = setup(); // let auto_schedule = IAutoScheduleDispatcher { contract_address }; - + // let config = get_default_auto_schedule_config(); // auto_schedule.setup_auto_schedule(config); - + // // Maintain rolling schedule // auto_schedule.maintain_rolling_schedule(); - + // // Should create future rounds based on rolling_schedule_count // let scheduled_rounds = auto_schedule.get_next_scheduled_rounds(5); // assert!(scheduled_rounds.len() >= 0, "Should have created scheduled rounds"); @@ -93,13 +93,13 @@ // fn test_modify_schedule() { // let contract_address = setup(); // let auto_schedule = IAutoScheduleDispatcher { contract_address }; - + // let config = get_default_auto_schedule_config(); // auto_schedule.setup_auto_schedule(config); - + // let round_id = 1_u256; // let new_deadline = 2000_u64; - + // // This should work even if round doesn't exist yet (implementation handles it) // auto_schedule.modify_schedule(round_id, new_deadline); // } @@ -108,16 +108,16 @@ // fn test_auto_complete_expired_rounds() { // let contract_address = setup(); // let auto_schedule = IAutoScheduleDispatcher { contract_address }; - + // let config = get_default_auto_schedule_config(); // auto_schedule.setup_auto_schedule(config); - + // // Move time forward to simulate expired rounds // set_block_timestamp(5000); - + // // Try to complete expired rounds // auto_schedule.auto_complete_expired_rounds(); - + // // This should complete any rounds that have expired // // The function should not fail even if no rounds exist // } @@ -128,9 +128,9 @@ // let contract_address = setup(); // let auto_schedule = IAutoScheduleDispatcher { contract_address }; // let non_admin_address = contract_address_const::(); - + // set_caller_address(non_admin_address); - + // let config = get_default_auto_schedule_config(); // auto_schedule.setup_auto_schedule(config); // } @@ -140,7 +140,7 @@ // fn test_setup_invalid_config() { // let contract_address = setup(); // let auto_schedule = IAutoScheduleDispatcher { contract_address }; - + // let invalid_config = AutoScheduleConfig { // round_duration_days: 0, // Invalid: zero duration // start_date: 1000, @@ -148,7 +148,7 @@ // auto_completion_enabled: true, // rolling_schedule_count: 3, // }; - + // auto_schedule.setup_auto_schedule(invalid_config); // } @@ -157,7 +157,7 @@ // fn test_setup_invalid_rolling_count() { // let contract_address = setup(); // let auto_schedule = IAutoScheduleDispatcher { contract_address }; - + // let invalid_config = AutoScheduleConfig { // round_duration_days: 30, // start_date: 1000, @@ -165,7 +165,7 @@ // auto_completion_enabled: true, // rolling_schedule_count: 0, // Invalid: zero rolling count // }; - + // auto_schedule.setup_auto_schedule(invalid_config); // } @@ -174,7 +174,7 @@ // fn test_setup_excessive_rolling_count() { // let contract_address = setup(); // let auto_schedule = IAutoScheduleDispatcher { contract_address }; - + // let invalid_config = AutoScheduleConfig { // round_duration_days: 30, // start_date: 1000, @@ -182,7 +182,7 @@ // auto_completion_enabled: true, // rolling_schedule_count: 10, // Invalid: too high // }; - + // auto_schedule.setup_auto_schedule(invalid_config); // } @@ -192,11 +192,11 @@ // let contract_address = setup(); // let auto_schedule = IAutoScheduleDispatcher { contract_address }; // let non_admin_address = contract_address_const::(); - + // // Setup as admin first // let config = get_default_auto_schedule_config(); // auto_schedule.setup_auto_schedule(config); - + // // Try to modify as non-admin // set_caller_address(non_admin_address); // auto_schedule.modify_schedule(1_u256, 2000_u64); @@ -206,7 +206,7 @@ // fn test_schedule_with_disabled_features() { // let contract_address = setup(); // let auto_schedule = IAutoScheduleDispatcher { contract_address }; - + // let config = AutoScheduleConfig { // round_duration_days: 30, // start_date: 1000, @@ -214,12 +214,12 @@ // auto_completion_enabled: false, // Disabled // rolling_schedule_count: 2, // }; - + // auto_schedule.setup_auto_schedule(config); - + // // Auto completion should do nothing when disabled // auto_schedule.auto_complete_expired_rounds(); - + // let retrieved_config = auto_schedule.get_auto_schedule_config(); // assert!(!retrieved_config.auto_activation_enabled, "Auto activation should be disabled"); // assert!(!retrieved_config.auto_completion_enabled, "Auto completion should be disabled"); @@ -229,13 +229,13 @@ // fn test_future_round_limit() { // let contract_address = setup(); // let auto_schedule = IAutoScheduleDispatcher { contract_address }; - + // let config = get_default_auto_schedule_config(); // auto_schedule.setup_auto_schedule(config); - + // // Request more rounds than available // let scheduled_rounds = auto_schedule.get_next_scheduled_rounds(100); - + // // Should not return more than reasonable number of future rounds // assert!(scheduled_rounds.len() <= 100, "Should handle large requests gracefully"); -// } \ No newline at end of file +// } diff --git a/tests/test_emergency_component.cairo b/tests/test_emergency_component.cairo index 5303839..6395d9a 100644 --- a/tests/test_emergency_component.cairo +++ b/tests/test_emergency_component.cairo @@ -1,16 +1,16 @@ +use core::array::ArrayTrait; // Needed for array! macro and Serde +// Required snforge_std imports for deployment and cheatcodes +use snforge_std::{ + ContractClassTrait, DeclareResultTrait, declare, start_cheat_block_timestamp, + start_cheat_caller_address, stop_cheat_block_timestamp, stop_cheat_caller_address, +}; use starknet::ContractAddress; // Replaced starknet::testing imports with snforge_std cheatcodes use starknet::contract_address_const; use starkremit_contract::component::emergency::{ - emergency_component, IEmergency, IEmergencyDispatcher, IEmergencyDispatcherTrait, - EmergencyConfig -}; -// Required snforge_std imports for deployment and cheatcodes -use snforge_std::{ - ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address, - stop_cheat_caller_address, start_cheat_block_timestamp, stop_cheat_block_timestamp + EmergencyConfig, IEmergency, IEmergencyDispatcher, IEmergencyDispatcherTrait, + emergency_component, }; -use core::array::ArrayTrait; // Needed for array! macro and Serde const ADMIN: felt252 = 0x123; const NON_ADMIN: felt252 = 0x456; @@ -19,26 +19,26 @@ const MEMBER: felt252 = 0x789; #[starknet::contract] mod TestContract { use super::*; - + component!(path: emergency_component, storage: emergency, event: EmergencyEvent); - + #[storage] struct Storage { #[substorage(v0)] emergency: emergency_component::Storage, } - + #[event] #[derive(Drop, starknet::Event)] enum Event { EmergencyEvent: emergency_component::Event, } - + #[constructor] fn constructor(ref self: ContractState, admin: ContractAddress) { self.emergency.initializer(admin); } - + #[abi(embed_v0)] impl EmergencyInternalImpl = emergency_component::InternalImpl; } @@ -46,14 +46,14 @@ mod TestContract { // Fixed setup function to correctly deploy the contract and set initial cheats fn setup() -> ContractAddress { let admin_address = contract_address_const::(); - + let contract_class = declare("TestContract").unwrap().contract_class(); - + let mut constructor_calldata = array![]; Serde::serialize(@admin_address, ref constructor_calldata); - + let (contract_address, _) = contract_class.deploy(@constructor_calldata).unwrap(); - + start_cheat_caller_address(contract_address, admin_address); start_cheat_block_timestamp(contract_address, 1000); @@ -64,13 +64,13 @@ fn setup() -> ContractAddress { fn test_emergency_initialization() { let contract_address = setup(); let emergency = IEmergencyDispatcher { contract_address }; - + // Test that component is initialized correctly assert!(!emergency.is_paused(), "Contract should start unpaused"); assert!(emergency.get_pause_reason() == 0, "Initial pause reason should be 0"); // Assuming initializer does not set a timestamp for pause, so it should be 0 initially - assert!(emergency.get_pause_timestamp() == 0, "Initial pause timestamp should be 0"); - + assert!(emergency.get_pause_timestamp() == 0, "Initial pause timestamp should be 0"); + stop_cheat_caller_address(contract_address); stop_cheat_block_timestamp(contract_address); } @@ -79,11 +79,11 @@ fn test_emergency_initialization() { fn test_emergency_pause_unpause() { let contract_address = setup(); let emergency = IEmergencyDispatcher { contract_address }; - + // Test pause emergency.pause(); assert!(emergency.is_paused(), "Contract should be paused"); - + // Test unpause emergency.unpause(); assert!(!emergency.is_paused(), "Contract should be unpaused"); @@ -96,18 +96,18 @@ fn test_emergency_pause_unpause() { fn test_emergency_pause_with_metadata() { let contract_address = setup(); let emergency = IEmergencyDispatcher { contract_address }; - + // First pause the contract emergency.pause(); - + // Set pause metadata let reason = 'emergency_maintenance'; emergency.set_pause_meta(reason); - + assert!(emergency.get_pause_reason() == reason, "Pause reason should match"); // The timestamp should be the cheated block timestamp from setup (1000) or a later value - assert!(emergency.get_pause_timestamp() > 0, "Pause timestamp should be set"); - + assert!(emergency.get_pause_timestamp() > 0, "Pause timestamp should be set"); + stop_cheat_caller_address(contract_address); stop_cheat_block_timestamp(contract_address); } @@ -116,14 +116,14 @@ fn test_emergency_pause_with_metadata() { fn test_emergency_unpause_with_metadata_clear() { let contract_address = setup(); let emergency = IEmergencyDispatcher { contract_address }; - + // First pause the contract with metadata emergency.pause(); emergency.set_pause_meta('test_reason'); - + // Unpause and clear metadata emergency.unpause_with_metadata_clear(); - + assert!(!emergency.is_paused(), "Contract should be unpaused"); assert!(emergency.get_pause_reason() == 0, "Pause reason should be cleared"); assert!(emergency.get_pause_timestamp() == 0, "Pause timestamp should be cleared"); @@ -132,20 +132,20 @@ fn test_emergency_unpause_with_metadata_clear() { stop_cheat_block_timestamp(contract_address); } -#[test] +#[test] fn test_emergency_ban_unban_member() { let contract_address = setup(); let emergency = IEmergencyDispatcher { contract_address }; let member_address = contract_address_const::(); - + // Test ban member emergency.set_ban(member_address, true); assert!(emergency.is_banned(member_address), "Member should be banned"); - + // Test unban member emergency.set_ban(member_address, false); assert!(!emergency.is_banned(member_address), "Member should be unbanned"); - + stop_cheat_caller_address(contract_address); stop_cheat_block_timestamp(contract_address); } @@ -154,18 +154,15 @@ fn test_emergency_ban_unban_member() { fn test_emergency_config() { let contract_address = setup(); let emergency = IEmergencyDispatcher { contract_address }; - - let config = EmergencyConfig { - emergency_cooldown: 86400, - required_approvals: 3, - }; - + + let config = EmergencyConfig { emergency_cooldown: 86400, required_approvals: 3 }; + emergency.set_config(config); let retrieved_config = emergency.get_config(); - + assert!(retrieved_config.emergency_cooldown == 86400, "Cooldown should match"); assert!(retrieved_config.required_approvals == 3, "Required approvals should match"); - + stop_cheat_caller_address(contract_address); stop_cheat_block_timestamp(contract_address); } @@ -174,10 +171,10 @@ fn test_emergency_config() { fn test_emergency_assert_not_paused_success() { let contract_address = setup(); let emergency = IEmergencyDispatcher { contract_address }; - + // Should not panic when contract is not paused emergency.assert_not_paused(); - + stop_cheat_caller_address(contract_address); stop_cheat_block_timestamp(contract_address); } @@ -187,13 +184,13 @@ fn test_emergency_assert_not_paused_success() { fn test_emergency_assert_not_paused_panic() { let contract_address = setup(); let emergency = IEmergencyDispatcher { contract_address }; - + // Pause the contract emergency.pause(); - + // Should panic when contract is paused emergency.assert_not_paused(); - + stop_cheat_caller_address(contract_address); stop_cheat_block_timestamp(contract_address); } @@ -204,10 +201,9 @@ fn test_emergency_pause_unauthorized() { let contract_address = setup(); let emergency = IEmergencyDispatcher { contract_address }; let non_admin_address = contract_address_const::(); - + start_cheat_caller_address(contract_address, non_admin_address); emergency.pause(); // This should panic - stop_cheat_caller_address(contract_address); stop_cheat_block_timestamp(contract_address); @@ -218,10 +214,9 @@ fn test_emergency_pause_unauthorized() { fn test_emergency_double_pause() { let contract_address = setup(); let emergency = IEmergencyDispatcher { contract_address }; - + emergency.pause(); emergency.pause(); // Should panic - stop_cheat_caller_address(contract_address); stop_cheat_block_timestamp(contract_address); @@ -232,10 +227,9 @@ fn test_emergency_double_pause() { fn test_emergency_set_pause_meta_when_not_paused() { let contract_address = setup(); let emergency = IEmergencyDispatcher { contract_address }; - + // Try to set pause metadata without pausing first emergency.set_pause_meta('test_reason'); // Should panic - stop_cheat_caller_address(contract_address); stop_cheat_block_timestamp(contract_address); @@ -246,9 +240,8 @@ fn test_emergency_set_pause_meta_when_not_paused() { fn test_emergency_unpause_when_not_paused() { let contract_address = setup(); let emergency = IEmergencyDispatcher { contract_address }; - + emergency.unpause(); // Should panic as contract is not paused - stop_cheat_caller_address(contract_address); stop_cheat_block_timestamp(contract_address); @@ -258,20 +251,19 @@ fn test_emergency_unpause_when_not_paused() { fn test_emergency_component_integration() { let contract_address = setup(); let emergency = IEmergencyDispatcher { contract_address }; - + assert!(!emergency.is_paused(), "Should start unpaused"); - + // Pause with reason emergency.pause(); emergency.set_pause_meta('emergency_stop'); assert!(emergency.is_paused(), "Should be paused"); assert!(emergency.get_pause_reason() == 'emergency_stop', "Reason should match"); - + emergency.unpause_with_metadata_clear(); assert!(!emergency.is_paused(), "Should be unpaused"); assert!(emergency.get_pause_reason() == 0, "Reason should be cleared"); - stop_cheat_caller_address(contract_address); stop_cheat_block_timestamp(contract_address); -} \ No newline at end of file +} diff --git a/tests/test_integration.cairo b/tests/test_integration.cairo index 439c99e..f273cd3 100644 --- a/tests/test_integration.cairo +++ b/tests/test_integration.cairo @@ -2,7 +2,8 @@ // use starknet::testing::{set_caller_address, set_block_timestamp}; // use starknet::contract_address_const; // use starkremit_contract::starkremit::StarkRemit; -// use starkremit_contract::interfaces::IStarkRemit::{IStarkRemitDispatcher, IStarkRemitDispatcherTrait}; +// use starkremit_contract::interfaces::IStarkRemit::{IStarkRemitDispatcher, +// IStarkRemitDispatcherTrait}; // use starkremit_contract::base::types::{UserProfile, KYCLevel, RegistrationRequest}; // const ADMIN: felt252 = 0x123; @@ -12,10 +13,10 @@ // fn setup() -> ContractAddress { // let admin_address = contract_address_const::(); // let contract_address = contract_address_const::<0x1>(); - + // set_caller_address(admin_address); // set_block_timestamp(1000); - + // contract_address // } @@ -24,13 +25,13 @@ // let contract_address = setup(); // let stark_remit = IStarkRemitDispatcher { contract_address }; // let member_address = contract_address_const::(); - + // // Test emergency pause // stark_remit.emergency_pause_contract('SECURITY_BREACH'); - + // // Test member ban through emergency functions // stark_remit.ban_member(member_address); - + // // Test penalty application (should work with emergency system) // stark_remit.apply_late_fee(member_address, 1_u256); // } @@ -40,10 +41,10 @@ // let contract_address = setup(); // let stark_remit = IStarkRemitDispatcher { contract_address }; // let member_address = contract_address_const::(); - + // // Apply penalty // stark_remit.add_strike(member_address, 1_u256); - + // // Check that analytics are updated (if integrated properly) // // This tests that penalty actions trigger analytics updates // } @@ -53,7 +54,7 @@ // let contract_address = setup(); // let stark_remit = IStarkRemitDispatcher { contract_address }; // let member_address = contract_address_const::(); - + // // Register member (user management) // let registration_data = RegistrationRequest { // email_hash: 'test@email.com', @@ -61,17 +62,17 @@ // full_name: 'Test User', // country_code: 'US', // }; - + // set_caller_address(member_address); // stark_remit.register_user(registration_data); - + // // Check if member is registered // assert!(stark_remit.is_user_registered(member_address), "Member should be registered"); - + // // Apply penalties as admin // set_caller_address(contract_address_const::()); // stark_remit.add_strike(member_address, 1_u256); - + // // Test emergency functions // stark_remit.emergency_reset_member_strikes(member_address); // } @@ -81,12 +82,12 @@ // let contract_address = setup(); // let stark_remit = IStarkRemitDispatcher { contract_address }; // let member_address = contract_address_const::(); - + // // Setup automated scheduling would be integrated here // // Apply penalties that could affect scheduling // stark_remit.add_strike(member_address, 1_u256); // stark_remit.add_strike(member_address, 2_u256); - + // // Member should be affected in future scheduling decisions // // (Implementation specific - depends on integration) // } @@ -96,7 +97,7 @@ // let contract_address = setup(); // let stark_remit = IStarkRemitDispatcher { contract_address }; // let member_address = contract_address_const::(); - + // // Test that payment flexibility features integrate with analytics // // This would involve setting up auto-payments, early payments, etc. // // and verifying that analytics track these properly @@ -108,17 +109,17 @@ // let stark_remit = IStarkRemitDispatcher { contract_address }; // let member1_address = contract_address_const::(); // let member2_address = contract_address_const::(); - + // // Simulate emergency scenario // stark_remit.emergency_pause_contract('SYSTEM_COMPROMISE'); - + // // Emergency actions should still work // stark_remit.ban_member(member1_address); // stark_remit.emergency_withdraw_member(member2_address); - + // // Resume operations // stark_remit.emergency_unpause_contract(); - + // // Verify member states // stark_remit.unban_member(member1_address); // } @@ -128,12 +129,12 @@ // let contract_address = setup(); // let stark_remit = IStarkRemitDispatcher { contract_address }; // let member_address = contract_address_const::(); - + // // Apply multiple penalties // stark_remit.add_strike(member_address, 1_u256); // stark_remit.apply_late_fee(member_address, 2_u256); // stark_remit.add_strike(member_address, 3_u256); - + // // System health should be affected but still calculable // // (This depends on the integration between analytics and penalty systems) // } @@ -142,16 +143,16 @@ // fn test_round_completion_with_all_systems() { // let contract_address = setup(); // let stark_remit = IStarkRemitDispatcher { contract_address }; - + // // Create group and round // stark_remit.create_group(5); - + // // Test that when a round completes, all systems are updated: // // - Analytics track the completion // // - Penalties are applied to late members // // - Next round is automatically scheduled // // - Member profiles are updated - + // // This integration test verifies cross-component communication // } @@ -160,14 +161,14 @@ // let contract_address = setup(); // let stark_remit = IStarkRemitDispatcher { contract_address }; // let token_address = contract_address_const::<0x999>(); - + // // Test emergency token recovery // stark_remit.emergency_recover_tokens(token_address, 1000_u256); - + // // Test emergency fund migration // let new_contract = contract_address_const::<0x888>(); // stark_remit.emergency_migrate_funds(new_contract); - + // // Verify that analytics are updated to reflect emergency actions // } @@ -177,13 +178,13 @@ // let stark_remit = IStarkRemitDispatcher { contract_address }; // let member1_address = contract_address_const::(); // let member2_address = contract_address_const::(); - + // // Apply different penalties to different members // stark_remit.add_strike(member1_address, 1_u256); // stark_remit.apply_late_fee(member2_address, 1_u256); // stark_remit.add_strike(member1_address, 2_u256); // stark_remit.add_strike(member1_address, 3_u256); // Should trigger ban - + // // Test that analytics properly track different member states // // Test that emergency functions can handle banned members // stark_remit.emergency_reset_member_strikes(member1_address); @@ -194,14 +195,14 @@ // let contract_address = setup(); // let stark_remit = IStarkRemitDispatcher { contract_address }; // let member_address = contract_address_const::(); - + // // Perform actions across multiple components // stark_remit.add_strike(member_address, 1_u256); // Penalty system // stark_remit.ban_member(member_address); // Emergency system - + // // Verify that all systems reflect consistent state // // Member should be banned in all relevant systems - + // // Reset and verify consistency // stark_remit.unban_member(member_address); // Should reset penalties too // stark_remit.emergency_reset_member_strikes(member_address); // Double-check cleanup @@ -211,16 +212,16 @@ // fn test_automated_system_maintenance() { // let contract_address = setup(); // let stark_remit = IStarkRemitDispatcher { contract_address }; - + // // Test that automated functions work together: // // - Schedule maintenance creates future rounds // // - Auto-completion processes expired rounds // // - Penalties are applied to late members // // - Analytics track all activities - + // // Move time forward to trigger automated actions // set_block_timestamp(10000); - + // // Trigger various automated processes // // (Implementation depends on how components are integrated) -// } \ No newline at end of file +// } diff --git a/tests/test_penalty_component.cairo b/tests/test_penalty_component.cairo index f2936d6..f4cfd13 100644 --- a/tests/test_penalty_component.cairo +++ b/tests/test_penalty_component.cairo @@ -14,10 +14,10 @@ // fn setup() -> ContractAddress { // let admin_address = contract_address_const::(); // let contract_address = contract_address_const::<0x1>(); - + // set_caller_address(admin_address); // set_block_timestamp(1000); - + // contract_address // } @@ -35,9 +35,9 @@ // fn test_penalty_config() { // let contract_address = setup(); // let penalty = IPenaltyDispatcher { contract_address }; - + // let config = get_default_penalty_config(); - + // // Get initial config (should have default values) // let retrieved_config = penalty.get_penalty_config(); // assert!(retrieved_config.late_fee_percentage >= 0, "Initial config should be valid"); @@ -49,10 +49,10 @@ // let penalty = IPenaltyDispatcher { contract_address }; // let member_address = contract_address_const::(); // let round_id = 1_u256; - + // // Apply late fee // penalty.apply_late_fee(member_address, round_id); - + // // Check member penalty record // let record = penalty.get_member_penalty_record(member_address); // assert!(record.total_penalties_paid > 0, "Penalty should be applied"); @@ -64,24 +64,24 @@ // let penalty = IPenaltyDispatcher { contract_address }; // let member_address = contract_address_const::(); // let round_id = 1_u256; - + // // Add first strike // penalty.add_strike(member_address, round_id); // let record = penalty.get_member_penalty_record(member_address); // assert!(record.strikes == 1, "Member should have 1 strike"); // assert!(!record.is_banned, "Member should not be banned yet"); - + // // Add second strike // penalty.add_strike(member_address, round_id + 1); // let record = penalty.get_member_penalty_record(member_address); // assert!(record.strikes == 2, "Member should have 2 strikes"); // assert!(!record.is_banned, "Member should not be banned yet"); - + // // Add third strike (should result in ban if max_strikes is 3) // penalty.add_strike(member_address, round_id + 2); // let record = penalty.get_member_penalty_record(member_address); // assert!(record.strikes == 3, "Member should have 3 strikes"); - + // // Remove a strike // penalty.remove_strike(member_address); // let record = penalty.get_member_penalty_record(member_address); @@ -93,12 +93,12 @@ // let contract_address = setup(); // let penalty = IPenaltyDispatcher { contract_address }; // let member_address = contract_address_const::(); - + // // Ban member // penalty.ban_member(member_address); // let record = penalty.get_member_penalty_record(member_address); // assert!(record.is_banned, "Member should be banned"); - + // // Unban member // penalty.unban_member(member_address); // let record = penalty.get_member_penalty_record(member_address); @@ -114,7 +114,7 @@ // let non_admin_address = contract_address_const::(); // let member_address = contract_address_const::(); // let round_id = 1_u256; - + // set_caller_address(non_admin_address); // penalty.apply_late_fee(member_address, round_id); // } @@ -125,7 +125,7 @@ // let contract_address = setup(); // let penalty = IPenaltyDispatcher { contract_address }; // let member_address = contract_address_const::(); - + // penalty.ban_member(member_address); // penalty.ban_member(member_address); // Should panic // } @@ -136,7 +136,7 @@ // let contract_address = setup(); // let penalty = IPenaltyDispatcher { contract_address }; // let member_address = contract_address_const::(); - + // penalty.unban_member(member_address); // Should panic as member is not banned // } @@ -145,7 +145,7 @@ // let contract_address = setup(); // let penalty = IPenaltyDispatcher { contract_address }; // let member_address = contract_address_const::(); - + // let record = penalty.get_member_penalty_record(member_address); // assert!(record.strikes == 0, "New member should have 0 strikes"); // assert!(record.total_penalties_paid == 0, "New member should have paid 0 penalties"); @@ -160,15 +160,15 @@ // let member1_address = contract_address_const::(); // let member2_address = contract_address_const::(); // let round_id = 1_u256; - + // // Apply penalties to both members // penalty.add_strike(member1_address, round_id); // penalty.apply_late_fee(member2_address, round_id); - + // // Check that penalties are tracked separately // let record1 = penalty.get_member_penalty_record(member1_address); // let record2 = penalty.get_member_penalty_record(member2_address); - + // assert!(record1.strikes == 1, "Member 1 should have 1 strike"); // assert!(record2.strikes == 0, "Member 2 should have 0 strikes"); // assert!(record1.total_penalties_paid == 0, "Member 1 should have no late fees"); @@ -181,12 +181,13 @@ // let penalty = IPenaltyDispatcher { contract_address }; // let member_address = contract_address_const::(); // let round_id = 1_u256; - + // // Apply penalty and check credit score change // let initial_record = penalty.get_member_penalty_record(member_address); // penalty.apply_late_fee(member_address, round_id); // let updated_record = penalty.get_member_penalty_record(member_address); - + // // Credit score should be updated (implementation may vary) -// assert!(updated_record.last_penalty_date > initial_record.last_penalty_date, "Last penalty date should be updated"); -// } \ No newline at end of file +// assert!(updated_record.last_penalty_date > initial_record.last_penalty_date, "Last penalty +// date should be updated"); +// }