use serde::{Deserialize, Serialize}; use crate::contract::{InheritanceContract, ContractStatus}; /// Progressive unlock tier /// This is OnionCoin's INNOVATIVE feature: gradual unlock gives owner time to recover! #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UnlockTier { /// Tier number (1, 2, 3...) pub tier: u32, /// Days after grace period expiry pub days_after_expiry: i64, /// Percentage of total to unlock at this tier pub unlock_percentage: u8, /// Has this tier been unlocked? pub unlocked: bool, /// Timestamp when unlocked pub unlock_time: Option, } impl UnlockTier { pub fn new(tier: u32, days_after_expiry: i64, unlock_percentage: u8) -> Self { Self { tier, days_after_expiry, unlock_percentage, unlocked: false, unlock_time: None, } } /// Check if this tier should be unlocked pub fn should_unlock(&self, time_since_expiry: i64) -> bool { !self.unlocked && time_since_expiry >= self.days_after_expiry * 24 * 3600 } /// Mark tier as unlocked pub fn mark_unlocked(&mut self, current_time: i64) { self.unlocked = true; self.unlock_time = Some(current_time); } } /// Complete unlock schedule with multiple tiers #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UnlockSchedule { /// All unlock tiers pub tiers: Vec, /// Grace period expiry time pub grace_expiry_time: i64, /// Total unlocked so far (percentage) pub total_unlocked_percentage: u8, } impl UnlockSchedule { /// Create standard 4-tier progressive unlock /// Tier 1 (30 days): 10% /// Tier 2 (60 days): 25% (35% total) /// Tier 3 (90 days): 35% (70% total) /// Tier 4 (120 days): 30% (100% total) pub fn standard(grace_expiry_time: i64) -> Self { let tiers = vec![ UnlockTier::new(1, 30, 10), // 10% after 30 days UnlockTier::new(2, 60, 25), // 25% after 60 days (35% total) UnlockTier::new(3, 90, 35), // 35% after 90 days (70% total) UnlockTier::new(4, 120, 30), // 30% after 120 days (100% total) ]; Self { tiers, grace_expiry_time, total_unlocked_percentage: 0, } } /// Create aggressive 3-tier unlock (faster) /// Tier 1 (14 days): 25% /// Tier 2 (30 days): 35% (60% total) /// Tier 3 (60 days): 40% (100% total) pub fn aggressive(grace_expiry_time: i64) -> Self { let tiers = vec![ UnlockTier::new(1, 14, 25), // 25% after 14 days UnlockTier::new(2, 30, 35), // 35% after 30 days UnlockTier::new(3, 60, 40), // 40% after 60 days ]; Self { tiers, grace_expiry_time, total_unlocked_percentage: 0, } } /// Create conservative 5-tier unlock (slower, more chances to recover) /// Tier 1 (60 days): 5% /// Tier 2 (90 days): 10% (15% total) /// Tier 3 (120 days): 20% (35% total) /// Tier 4 (180 days): 30% (65% total) /// Tier 5 (365 days): 35% (100% total) pub fn conservative(grace_expiry_time: i64) -> Self { let tiers = vec![ UnlockTier::new(1, 60, 5), // 5% after 60 days UnlockTier::new(2, 90, 10), // 10% after 90 days UnlockTier::new(3, 120, 20), // 20% after 120 days UnlockTier::new(4, 180, 30), // 30% after 180 days UnlockTier::new(5, 365, 35), // 35% after 365 days ]; Self { tiers, grace_expiry_time, total_unlocked_percentage: 0, } } /// Custom unlock schedule pub fn custom(grace_expiry_time: i64, tiers: Vec) -> Result { // Validate total percentage = 100% let total: u32 = tiers.iter().map(|t| t.unlock_percentage as u32).sum(); if total != 100 { return Err(UnlockError::InvalidTotalPercentage(total)); } // Validate tiers are in order for i in 1..tiers.len() { if tiers[i].days_after_expiry <= tiers[i - 1].days_after_expiry { return Err(UnlockError::InvalidTierOrder); } } Ok(Self { tiers, grace_expiry_time, total_unlocked_percentage: 0, }) } /// Process unlocks for current time pub fn process_unlocks(&mut self, current_time: i64) -> Vec { let time_since_expiry = current_time - self.grace_expiry_time; let mut events = Vec::new(); for tier in &mut self.tiers { if tier.should_unlock(time_since_expiry) { tier.mark_unlocked(current_time); self.total_unlocked_percentage += tier.unlock_percentage; events.push(UnlockEvent { tier: tier.tier, percentage: tier.unlock_percentage, timestamp: current_time, }); } } events } /// Get next unlock tier pub fn next_unlock(&self) -> Option<&UnlockTier> { self.tiers.iter().find(|t| !t.unlocked) } /// Get time until next unlock pub fn time_until_next_unlock(&self, current_time: i64) -> Option { self.next_unlock().map(|tier| { let target_time = self.grace_expiry_time + tier.days_after_expiry * 24 * 3600; (target_time - current_time).max(0) }) } /// Check if fully unlocked pub fn is_fully_unlocked(&self) -> bool { self.total_unlocked_percentage >= 100 } /// Get summary of unlock progress pub fn progress_summary(&self, current_time: i64) -> String { let mut summary = format!("Unlocked: {}%\n", self.total_unlocked_percentage); for tier in &self.tiers { let status = if tier.unlocked { format!("✓ Unlocked at {}", chrono::DateTime::from_timestamp(tier.unlock_time.unwrap(), 0) .map(|dt| dt.format("%Y-%m-%d").to_string()) .unwrap_or_else(|| "unknown".to_string())) } else { let time_until = self.grace_expiry_time + tier.days_after_expiry * 24 * 3600 - current_time; if time_until > 0 { format!("⏳ In {} days", time_until / (24 * 3600)) } else { "⏳ Ready to unlock".to_string() } }; summary.push_str(&format!( "Tier {}: {}% - {}\n", tier.tier, tier.unlock_percentage, status )); } summary } } /// Event when a tier is unlocked #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UnlockEvent { pub tier: u32, pub percentage: u8, pub timestamp: i64, } /// Manager for progressive unlock pub struct ProgressiveUnlock; impl ProgressiveUnlock { /// Initialize progressive unlock for a contract pub fn initialize( contract: &mut InheritanceContract, schedule_type: UnlockScheduleType, ) -> Result<(), UnlockError> { if !contract.config.progressive_unlock { return Err(UnlockError::ProgressiveUnlockDisabled); } if contract.status != ContractStatus::Unlocking { return Err(UnlockError::ContractNotUnlocking); } let grace_expiry = contract.last_heartbeat + contract.config.total_timeout(); let schedule = match schedule_type { UnlockScheduleType::Standard => UnlockSchedule::standard(grace_expiry), UnlockScheduleType::Aggressive => UnlockSchedule::aggressive(grace_expiry), UnlockScheduleType::Conservative => UnlockSchedule::conservative(grace_expiry), UnlockScheduleType::Custom(tiers) => { UnlockSchedule::custom(grace_expiry, tiers)? } }; contract.unlock_schedule = Some(schedule); Ok(()) } /// Process unlocks and distribute to beneficiaries pub fn process( contract: &mut InheritanceContract, current_time: i64, ) -> Result, UnlockError> { let schedule = contract .unlock_schedule .as_mut() .ok_or(UnlockError::NoSchedule)?; let events = schedule.process_unlocks(current_time); if events.is_empty() { return Ok(Vec::new()); } let mut distributions = Vec::new(); for event in events { // Calculate amount to unlock for this tier let tier_amount = (contract.locked_amount as f64 * (event.percentage as f64 / 100.0)) as u64; // Distribute to beneficiaries for beneficiary in &mut contract.beneficiaries { let beneficiary_amount = beneficiary.calculate_amount(tier_amount); beneficiary.unlocked_amount += beneficiary_amount; distributions.push(Distribution { beneficiary: beneficiary.pubkey, amount: beneficiary_amount, tier: event.tier, timestamp: event.timestamp, }); } } // Mark as completed if fully unlocked if schedule.is_fully_unlocked() { contract.status = ContractStatus::Completed; } Ok(distributions) } /// Get unlock status pub fn status(contract: &InheritanceContract, current_time: i64) -> String { if let Some(schedule) = &contract.unlock_schedule { schedule.progress_summary(current_time) } else { "No unlock schedule".to_string() } } } /// Type of unlock schedule #[derive(Debug, Clone)] pub enum UnlockScheduleType { Standard, Aggressive, Conservative, Custom(Vec), } /// Distribution to a beneficiary #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Distribution { pub beneficiary: [u8; 32], pub amount: u64, pub tier: u32, pub timestamp: i64, } #[derive(Debug, thiserror::Error)] pub enum UnlockError { #[error("Invalid total percentage: {0}% (must be 100%)")] InvalidTotalPercentage(u32), #[error("Unlock tiers must be in chronological order")] InvalidTierOrder, #[error("Progressive unlock is disabled for this contract")] ProgressiveUnlockDisabled, #[error("Contract is not in unlocking state")] ContractNotUnlocking, #[error("No unlock schedule configured")] NoSchedule, } #[cfg(test)] mod tests { use super::*; use crate::contract::InheritanceConfig; #[test] fn test_unlock_tier() { let mut tier = UnlockTier::new(1, 30, 10); assert!(!tier.unlocked); assert!(tier.should_unlock(31 * 24 * 3600)); assert!(!tier.should_unlock(29 * 24 * 3600)); tier.mark_unlocked(1000); assert!(tier.unlocked); assert_eq!(tier.unlock_time, Some(1000)); } #[test] fn test_standard_schedule() { let schedule = UnlockSchedule::standard(0); assert_eq!(schedule.tiers.len(), 4); assert_eq!(schedule.total_unlocked_percentage, 0); let total: u32 = schedule.tiers.iter().map(|t| t.unlock_percentage as u32).sum(); assert_eq!(total, 100); } #[test] fn test_schedule_processing() { let mut schedule = UnlockSchedule::standard(0); // 31 days after expiry - should unlock tier 1 (10%) let events = schedule.process_unlocks(31 * 24 * 3600); assert_eq!(events.len(), 1); assert_eq!(events[0].tier, 1); assert_eq!(events[0].percentage, 10); assert_eq!(schedule.total_unlocked_percentage, 10); } #[test] fn test_multiple_tier_unlock() { let mut schedule = UnlockSchedule::standard(0); // 91 days after expiry - should unlock tiers 1, 2, and 3 let events = schedule.process_unlocks(91 * 24 * 3600); assert_eq!(events.len(), 3); assert_eq!(schedule.total_unlocked_percentage, 70); // 10 + 25 + 35 } #[test] fn test_full_unlock() { let mut schedule = UnlockSchedule::standard(0); schedule.process_unlocks(121 * 24 * 3600); assert!(schedule.is_fully_unlocked()); assert_eq!(schedule.total_unlocked_percentage, 100); } #[test] fn test_custom_schedule() { let tiers = vec![ UnlockTier::new(1, 10, 50), UnlockTier::new(2, 20, 50), ]; let schedule = UnlockSchedule::custom(0, tiers); assert!(schedule.is_ok()); } #[test] fn test_invalid_custom_schedule() { // Invalid total percentage let tiers = vec![ UnlockTier::new(1, 10, 50), UnlockTier::new(2, 20, 40), // Total = 90% ]; let schedule = UnlockSchedule::custom(0, tiers); assert!(matches!( schedule.unwrap_err(), UnlockError::InvalidTotalPercentage(90) )); } #[test] fn test_progressive_unlock_with_contract() { use crate::contract::Beneficiary; let beneficiaries = vec![ Beneficiary::new([1u8; 32], 60).unwrap(), Beneficiary::new([2u8; 32], 40).unwrap(), ]; let mut contract = InheritanceContract::new( [0u8; 32], beneficiaries, InheritanceConfig::default(), 10000, 0, ) .unwrap(); // Simulate unlocking state contract.status = ContractStatus::Unlocking; // Initialize progressive unlock ProgressiveUnlock::initialize(&mut contract, UnlockScheduleType::Standard).unwrap(); assert!(contract.unlock_schedule.is_some()); // Process unlocks after 31 days let grace_expiry = contract.config.total_timeout(); let current_time = grace_expiry + 31 * 24 * 3600; let distributions = ProgressiveUnlock::process(&mut contract, current_time).unwrap(); // Tier 1 unlocks 10% = 1000 ONC // Beneficiary 1: 60% of 1000 = 600 // Beneficiary 2: 40% of 1000 = 400 assert_eq!(distributions.len(), 2); assert_eq!(distributions[0].amount, 600); assert_eq!(distributions[1].amount, 400); } }