summaryrefslogtreecommitdiffstats
path: root/inheritance/src/heartbeat.rs
blob: ef1977659f6241aae818b92d0c6ee46af4d2483e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::contract::InheritanceContract;

/// Manages heartbeat system for inheritance contracts
/// Owner must periodically "check in" to prove they're alive
pub struct HeartbeatManager {
    /// Contract heartbeat tracking
    contracts: HashMap<[u8; 32], HeartbeatRecord>,

    /// Notification settings
    notification_settings: NotificationSettings,
}

/// Record of heartbeat activity for a contract
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeartbeatRecord {
    /// Contract ID
    pub contract_id: [u8; 32],

    /// Last heartbeat timestamp
    pub last_heartbeat: i64,

    /// Total heartbeats sent
    pub heartbeat_count: u64,

    /// Average interval between heartbeats
    pub avg_interval_days: f64,

    /// Warnings sent to owner
    pub warnings_sent: u32,

    /// Notifications sent to beneficiaries
    pub beneficiary_notifications_sent: u32,
}

/// Notification settings for heartbeat warnings
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotificationSettings {
    /// Send warning when X% of interval remaining
    pub warning_threshold_percentage: u8,

    /// Send critical warning when in grace period
    pub grace_period_warning: bool,

    /// Notify beneficiaries when grace period starts
    pub notify_beneficiaries_grace: bool,

    /// Notify beneficiaries on each unlock tier
    pub notify_beneficiaries_unlock: bool,
}

impl Default for NotificationSettings {
    fn default() -> Self {
        Self {
            warning_threshold_percentage: 80, // Warn at 80% of interval
            grace_period_warning: true,
            notify_beneficiaries_grace: true,
            notify_beneficiaries_unlock: true,
        }
    }
}

impl HeartbeatManager {
    pub fn new() -> Self {
        Self {
            contracts: HashMap::new(),
            notification_settings: NotificationSettings::default(),
        }
    }

    /// Register a contract for heartbeat monitoring
    pub fn register_contract(&mut self, contract_id: [u8; 32], current_time: i64) {
        let record = HeartbeatRecord {
            contract_id,
            last_heartbeat: current_time,
            heartbeat_count: 0,
            avg_interval_days: 0.0,
            warnings_sent: 0,
            beneficiary_notifications_sent: 0,
        };

        self.contracts.insert(contract_id, record);
    }

    /// Process a heartbeat
    pub fn process_heartbeat(
        &mut self,
        contract: &mut InheritanceContract,
        current_time: i64,
    ) -> Result<HeartbeatStatus, HeartbeatError> {
        // Record heartbeat in contract
        contract.heartbeat(current_time)
            .map_err(|_| HeartbeatError::ContractError)?;

        // Update heartbeat record
        if let Some(record) = self.contracts.get_mut(&contract.contract_id) {
            let interval = current_time - record.last_heartbeat;
            let interval_days = interval as f64 / (24.0 * 3600.0);

            // Update average interval
            let total = record.heartbeat_count as f64;
            record.avg_interval_days =
                (record.avg_interval_days * total + interval_days) / (total + 1.0);

            record.last_heartbeat = current_time;
            record.heartbeat_count += 1;
            record.warnings_sent = 0; // Reset warnings

            Ok(HeartbeatStatus::Success {
                next_required: current_time + contract.config.heartbeat_interval,
                avg_interval_days: record.avg_interval_days,
            })
        } else {
            Err(HeartbeatError::ContractNotRegistered)
        }
    }

    /// Check all contracts and generate notifications
    pub fn check_contracts(
        &mut self,
        contracts: &[&InheritanceContract],
        current_time: i64,
    ) -> Vec<Notification> {
        let mut notifications = Vec::new();

        for contract in contracts {
            if let Some(record) = self.contracts.get_mut(&contract.contract_id) {
                let time_since_heartbeat = current_time - contract.last_heartbeat;
                let interval = contract.config.heartbeat_interval;

                // Calculate percentage of interval elapsed
                let percentage_elapsed = (time_since_heartbeat as f64 / interval as f64 * 100.0) as u8;

                // Check for warnings
                if percentage_elapsed >= self.notification_settings.warning_threshold_percentage
                    && record.warnings_sent == 0
                {
                    notifications.push(Notification {
                        contract_id: contract.contract_id,
                        notification_type: NotificationType::WarningOwner,
                        message: format!(
                            "{}% of heartbeat interval elapsed. Please send heartbeat soon!",
                            percentage_elapsed
                        ),
                        urgency: Urgency::Medium,
                        timestamp: current_time,
                    });
                    record.warnings_sent += 1;
                }

                // Check if grace period started
                if contract.is_timeout(current_time)
                    && !contract.is_grace_expired(current_time)
                    && self.notification_settings.grace_period_warning
                    && record.warnings_sent < 2
                {
                    let grace_remaining = contract.config.grace_period
                        - (time_since_heartbeat - interval);

                    notifications.push(Notification {
                        contract_id: contract.contract_id,
                        notification_type: NotificationType::CriticalOwner,
                        message: format!(
                            "CRITICAL: Grace period active! {} days remaining before unlock!",
                            grace_remaining / (24 * 3600)
                        ),
                        urgency: Urgency::Critical,
                        timestamp: current_time,
                    });

                    // Notify beneficiaries
                    if self.notification_settings.notify_beneficiaries_grace
                        && record.beneficiary_notifications_sent == 0
                    {
                        notifications.push(Notification {
                            contract_id: contract.contract_id,
                            notification_type: NotificationType::InfoBeneficiaries,
                            message: "Inheritance contract has entered grace period. Unlock will begin if owner does not respond.".to_string(),
                            urgency: Urgency::Low,
                            timestamp: current_time,
                        });
                        record.beneficiary_notifications_sent += 1;
                    }

                    record.warnings_sent += 1;
                }

                // Check if unlocking started
                if contract.is_grace_expired(current_time)
                    && record.beneficiary_notifications_sent < 2
                    && self.notification_settings.notify_beneficiaries_unlock
                {
                    notifications.push(Notification {
                        contract_id: contract.contract_id,
                        notification_type: NotificationType::UnlockStarted,
                        message: "Inheritance unlock has begun. Funds will be distributed according to the schedule.".to_string(),
                        urgency: Urgency::High,
                        timestamp: current_time,
                    });
                    record.beneficiary_notifications_sent += 1;
                }
            }
        }

        notifications
    }

    /// Get heartbeat statistics for a contract
    pub fn get_stats(&self, contract_id: &[u8; 32]) -> Option<&HeartbeatRecord> {
        self.contracts.get(contract_id)
    }

    /// Suggest optimal heartbeat frequency for user
    pub fn suggest_frequency(&self, contract_id: &[u8; 32]) -> Option<String> {
        self.contracts.get(contract_id).map(|record| {
            if record.heartbeat_count < 3 {
                "Not enough data yet. Continue sending regular heartbeats.".to_string()
            } else if record.avg_interval_days < 30.0 {
                format!(
                    "You're checking in every {:.0} days. Consider spacing out to 30-60 days.",
                    record.avg_interval_days
                )
            } else if record.avg_interval_days > 60.0 {
                format!(
                    "You're checking in every {:.0} days. Consider more frequent heartbeats (30-45 days) for safety.",
                    record.avg_interval_days
                )
            } else {
                format!(
                    "Good frequency! Checking in every {:.0} days.",
                    record.avg_interval_days
                )
            }
        })
    }

    /// Create simple heartbeat transaction
    /// Just send tiny amount to yourself to prove you're alive
    pub fn create_heartbeat_tx(owner: [u8; 32]) -> HeartbeatTransaction {
        HeartbeatTransaction {
            from: owner,
            to: owner, // Send to yourself
            amount: 1, // Minimum amount (0.00000001 ONC)
            timestamp: chrono::Utc::now().timestamp(),
            heartbeat_marker: true,
        }
    }
}

impl Default for HeartbeatManager {
    fn default() -> Self {
        Self::new()
    }
}

/// Result of heartbeat processing
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum HeartbeatStatus {
    Success {
        next_required: i64,
        avg_interval_days: f64,
    },
    Warning {
        message: String,
    },
    Error {
        message: String,
    },
}

/// Notification to send to owner or beneficiaries
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Notification {
    pub contract_id: [u8; 32],
    pub notification_type: NotificationType,
    pub message: String,
    pub urgency: Urgency,
    pub timestamp: i64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NotificationType {
    WarningOwner,
    CriticalOwner,
    InfoBeneficiaries,
    UnlockStarted,
    TierUnlocked,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Urgency {
    Low,
    Medium,
    High,
    Critical,
}

/// Simple heartbeat transaction
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeartbeatTransaction {
    pub from: [u8; 32],
    pub to: [u8; 32],
    pub amount: u64,
    pub timestamp: i64,
    pub heartbeat_marker: bool,
}

#[derive(Debug, thiserror::Error)]
pub enum HeartbeatError {
    #[error("Contract not registered with heartbeat manager")]
    ContractNotRegistered,

    #[error("Contract error occurred")]
    ContractError,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::contract::{InheritanceContract, Beneficiary, InheritanceConfig};

    fn create_test_contract() -> InheritanceContract {
        let beneficiaries = vec![Beneficiary::new([1u8; 32], 100).unwrap()];

        InheritanceContract::new(
            [0u8; 32],
            beneficiaries,
            InheritanceConfig::default(),
            10000,
            0,
        )
        .unwrap()
    }

    #[test]
    fn test_heartbeat_manager_registration() {
        let mut manager = HeartbeatManager::new();
        let contract_id = [1u8; 32];

        manager.register_contract(contract_id, 0);

        assert!(manager.contracts.contains_key(&contract_id));
    }

    #[test]
    fn test_heartbeat_processing() {
        let mut manager = HeartbeatManager::new();
        let mut contract = create_test_contract();

        manager.register_contract(contract.contract_id, 0);

        let result = manager.process_heartbeat(&mut contract, 1000);

        assert!(result.is_ok());
        assert_eq!(contract.last_heartbeat, 1000);

        let record = manager.get_stats(&contract.contract_id).unwrap();
        assert_eq!(record.heartbeat_count, 1);
    }

    #[test]
    fn test_average_interval_calculation() {
        let mut manager = HeartbeatManager::new();
        let mut contract = create_test_contract();

        manager.register_contract(contract.contract_id, 0);

        // First heartbeat at day 30
        manager.process_heartbeat(&mut contract, 30 * 24 * 3600).unwrap();

        // Second heartbeat at day 60
        manager.process_heartbeat(&mut contract, 60 * 24 * 3600).unwrap();

        let record = manager.get_stats(&contract.contract_id).unwrap();
        assert!((record.avg_interval_days - 30.0).abs() < 1.0);
    }

    #[test]
    fn test_notification_generation() {
        let mut manager = HeartbeatManager::new();
        let mut contract = create_test_contract();

        // Set short interval for testing
        contract.config.heartbeat_interval = 100;
        contract.config.grace_period = 50;

        manager.register_contract(contract.contract_id, 0);

        // Check at 85% of interval (should generate warning)
        let notifications = manager.check_contracts(&[&contract], 85);

        assert!(!notifications.is_empty());
        assert_eq!(notifications[0].notification_type, NotificationType::WarningOwner);
        assert_eq!(notifications[0].urgency, Urgency::Medium);
    }

    #[test]
    fn test_grace_period_notifications() {
        let mut manager = HeartbeatManager::new();
        let mut contract = create_test_contract();

        contract.config.heartbeat_interval = 100;
        contract.config.grace_period = 50;

        manager.register_contract(contract.contract_id, 0);

        // Enter grace period
        let notifications = manager.check_contracts(&[&contract], 101);

        // Should have critical warning for owner and info for beneficiaries
        assert!(notifications.len() >= 2);
        assert!(notifications.iter().any(|n| n.notification_type == NotificationType::CriticalOwner));
        assert!(notifications.iter().any(|n| n.notification_type == NotificationType::InfoBeneficiaries));
    }

    #[test]
    fn test_heartbeat_transaction() {
        let owner = [42u8; 32];
        let tx = HeartbeatManager::create_heartbeat_tx(owner);

        assert_eq!(tx.from, owner);
        assert_eq!(tx.to, owner); // Sent to self
        assert_eq!(tx.amount, 1); // Minimal amount
        assert!(tx.heartbeat_marker);
    }

    #[test]
    fn test_frequency_suggestion() {
        let mut manager = HeartbeatManager::new();
        let contract_id = [1u8; 32];

        manager.register_contract(contract_id, 0);

        // Not enough data initially
        let suggestion = manager.suggest_frequency(&contract_id).unwrap();
        assert!(suggestion.contains("Not enough data"));

        // Simulate multiple heartbeats
        let mut record = manager.contracts.get_mut(&contract_id).unwrap();
        record.heartbeat_count = 5;
        record.avg_interval_days = 45.0;

        let suggestion = manager.suggest_frequency(&contract_id).unwrap();
        assert!(suggestion.contains("Good frequency"));
    }
}