kaccy_bitcoin/
tx_limits.rs

1//! Transaction Limits Module
2//!
3//! This module provides transaction limit enforcement including per-user
4//! daily/monthly limits and platform-wide limits for security.
5
6use chrono::{DateTime, Duration, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::Arc;
10use tokio::sync::RwLock;
11
12use crate::error::{BitcoinError, Result};
13
14/// Limit period
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub enum LimitPeriod {
17    /// Hourly limit
18    Hourly,
19    /// Daily limit
20    Daily,
21    /// Weekly limit
22    Weekly,
23    /// Monthly limit
24    Monthly,
25}
26
27impl LimitPeriod {
28    /// Get duration for this period
29    pub fn duration(&self) -> Duration {
30        match self {
31            LimitPeriod::Hourly => Duration::hours(1),
32            LimitPeriod::Daily => Duration::days(1),
33            LimitPeriod::Weekly => Duration::weeks(1),
34            LimitPeriod::Monthly => Duration::days(30),
35        }
36    }
37
38    /// Get period start time
39    pub fn period_start(&self, now: DateTime<Utc>) -> DateTime<Utc> {
40        match self {
41            LimitPeriod::Hourly => now - Duration::hours(1),
42            LimitPeriod::Daily => now - Duration::days(1),
43            LimitPeriod::Weekly => now - Duration::weeks(1),
44            LimitPeriod::Monthly => now - Duration::days(30),
45        }
46    }
47}
48
49/// Transaction limit configuration
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct TransactionLimit {
52    /// Maximum amount in satoshis
53    pub max_amount_sats: u64,
54    /// Maximum number of transactions
55    pub max_count: u32,
56    /// Limit period
57    pub period: LimitPeriod,
58}
59
60/// Limit configuration
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct LimitConfig {
63    /// Per-user limits
64    pub user_limits: Vec<TransactionLimit>,
65    /// Platform-wide limits
66    pub platform_limits: Vec<TransactionLimit>,
67    /// Single transaction maximum (instant rejection)
68    pub single_tx_max_sats: u64,
69}
70
71impl Default for LimitConfig {
72    fn default() -> Self {
73        Self {
74            user_limits: vec![
75                TransactionLimit {
76                    max_amount_sats: 10_000_000, // 0.1 BTC per day
77                    max_count: 10,
78                    period: LimitPeriod::Daily,
79                },
80                TransactionLimit {
81                    max_amount_sats: 50_000_000, // 0.5 BTC per month
82                    max_count: 100,
83                    period: LimitPeriod::Monthly,
84                },
85            ],
86            platform_limits: vec![
87                TransactionLimit {
88                    max_amount_sats: 100_000_000, // 1 BTC per hour platform-wide
89                    max_count: 100,
90                    period: LimitPeriod::Hourly,
91                },
92                TransactionLimit {
93                    max_amount_sats: 1_000_000_000, // 10 BTC per day platform-wide
94                    max_count: 1000,
95                    period: LimitPeriod::Daily,
96                },
97            ],
98            single_tx_max_sats: 50_000_000, // 0.5 BTC max per transaction
99        }
100    }
101}
102
103/// Usage record for tracking
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct UsageRecord {
106    /// User ID
107    pub user_id: String,
108    /// Amount in satoshis
109    pub amount_sats: u64,
110    /// Timestamp
111    pub timestamp: DateTime<Utc>,
112    /// Transaction ID (if available)
113    pub txid: Option<String>,
114}
115
116/// Limit violation information
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct LimitViolation {
119    /// Type of limit violated
120    pub limit_type: String,
121    /// Period
122    pub period: LimitPeriod,
123    /// Current usage
124    pub current_usage_sats: u64,
125    /// Limit amount
126    pub limit_sats: u64,
127    /// Current transaction count
128    pub current_count: u32,
129    /// Max count
130    pub max_count: u32,
131}
132
133/// Limit enforcer for transaction limits
134pub struct LimitEnforcer {
135    config: LimitConfig,
136    /// User usage records
137    user_usage: Arc<RwLock<HashMap<String, Vec<UsageRecord>>>>,
138    /// Platform usage records
139    platform_usage: Arc<RwLock<Vec<UsageRecord>>>,
140}
141
142impl LimitEnforcer {
143    /// Create a new limit enforcer
144    pub fn new(config: LimitConfig) -> Self {
145        Self {
146            config,
147            user_usage: Arc::new(RwLock::new(HashMap::new())),
148            platform_usage: Arc::new(RwLock::new(Vec::new())),
149        }
150    }
151
152    /// Check if a transaction is allowed
153    pub async fn check_transaction(&self, user_id: &str, amount_sats: u64) -> Result<()> {
154        // Check single transaction maximum
155        if amount_sats > self.config.single_tx_max_sats {
156            return Err(BitcoinError::LimitExceeded(format!(
157                "Transaction amount {} sats exceeds single transaction limit of {} sats",
158                amount_sats, self.config.single_tx_max_sats
159            )));
160        }
161
162        // Check user limits
163        self.check_user_limits(user_id, amount_sats).await?;
164
165        // Check platform limits
166        self.check_platform_limits(amount_sats).await?;
167
168        Ok(())
169    }
170
171    /// Record a transaction
172    pub async fn record_transaction(&self, user_id: &str, amount_sats: u64, txid: Option<String>) {
173        let record = UsageRecord {
174            user_id: user_id.to_string(),
175            amount_sats,
176            timestamp: Utc::now(),
177            txid,
178        };
179
180        // Record user usage
181        let mut user_usage = self.user_usage.write().await;
182        user_usage
183            .entry(user_id.to_string())
184            .or_insert_with(Vec::new)
185            .push(record.clone());
186
187        // Record platform usage
188        let mut platform_usage = self.platform_usage.write().await;
189        platform_usage.push(record);
190
191        // Cleanup old records
192        drop(user_usage);
193        drop(platform_usage);
194        self.cleanup_old_records().await;
195    }
196
197    /// Get user usage for a period
198    pub async fn get_user_usage(&self, user_id: &str, period: LimitPeriod) -> (u64, u32) {
199        let user_usage = self.user_usage.read().await;
200        let records = user_usage.get(user_id);
201
202        if records.is_none() {
203            return (0, 0);
204        }
205
206        let now = Utc::now();
207        let period_start = period.period_start(now);
208
209        let filtered: Vec<_> = records
210            .unwrap()
211            .iter()
212            .filter(|r| r.timestamp >= period_start)
213            .collect();
214
215        let total_amount: u64 = filtered.iter().map(|r| r.amount_sats).sum();
216        let count = filtered.len() as u32;
217
218        (total_amount, count)
219    }
220
221    /// Get platform usage for a period
222    pub async fn get_platform_usage(&self, period: LimitPeriod) -> (u64, u32) {
223        let platform_usage = self.platform_usage.read().await;
224
225        let now = Utc::now();
226        let period_start = period.period_start(now);
227
228        let filtered: Vec<_> = platform_usage
229            .iter()
230            .filter(|r| r.timestamp >= period_start)
231            .collect();
232
233        let total_amount: u64 = filtered.iter().map(|r| r.amount_sats).sum();
234        let count = filtered.len() as u32;
235
236        (total_amount, count)
237    }
238
239    /// Check user limits
240    async fn check_user_limits(&self, user_id: &str, amount_sats: u64) -> Result<()> {
241        for limit in &self.config.user_limits {
242            let (current_usage, current_count) = self.get_user_usage(user_id, limit.period).await;
243
244            // Check amount limit
245            if current_usage + amount_sats > limit.max_amount_sats {
246                return Err(BitcoinError::LimitExceeded(format!(
247                    "User {:?} limit exceeded for amount: {} + {} > {} sats",
248                    limit.period, current_usage, amount_sats, limit.max_amount_sats
249                )));
250            }
251
252            // Check count limit
253            if current_count + 1 > limit.max_count {
254                return Err(BitcoinError::LimitExceeded(format!(
255                    "User {:?} limit exceeded for count: {} + 1 > {}",
256                    limit.period, current_count, limit.max_count
257                )));
258            }
259        }
260
261        Ok(())
262    }
263
264    /// Check platform limits
265    async fn check_platform_limits(&self, amount_sats: u64) -> Result<()> {
266        for limit in &self.config.platform_limits {
267            let (current_usage, current_count) = self.get_platform_usage(limit.period).await;
268
269            // Check amount limit
270            if current_usage + amount_sats > limit.max_amount_sats {
271                return Err(BitcoinError::LimitExceeded(format!(
272                    "Platform {:?} limit exceeded for amount: {} + {} > {} sats",
273                    limit.period, current_usage, amount_sats, limit.max_amount_sats
274                )));
275            }
276
277            // Check count limit
278            if current_count + 1 > limit.max_count {
279                return Err(BitcoinError::LimitExceeded(format!(
280                    "Platform {:?} limit exceeded for count: {} + 1 > {}",
281                    limit.period, current_count, limit.max_count
282                )));
283            }
284        }
285
286        Ok(())
287    }
288
289    /// Cleanup old records (older than the longest period)
290    async fn cleanup_old_records(&self) {
291        let now = Utc::now();
292        let max_period = Duration::days(30); // Monthly is the longest
293        let cutoff = now - max_period;
294
295        // Cleanup user records
296        let mut user_usage = self.user_usage.write().await;
297        for records in user_usage.values_mut() {
298            records.retain(|r| r.timestamp >= cutoff);
299        }
300
301        // Cleanup platform records
302        let mut platform_usage = self.platform_usage.write().await;
303        platform_usage.retain(|r| r.timestamp >= cutoff);
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    #[test]
312    fn test_limit_period_duration() {
313        assert_eq!(LimitPeriod::Hourly.duration(), Duration::hours(1));
314        assert_eq!(LimitPeriod::Daily.duration(), Duration::days(1));
315        assert_eq!(LimitPeriod::Weekly.duration(), Duration::weeks(1));
316        assert_eq!(LimitPeriod::Monthly.duration(), Duration::days(30));
317    }
318
319    #[test]
320    fn test_limit_config_defaults() {
321        let config = LimitConfig::default();
322        assert_eq!(config.user_limits.len(), 2);
323        assert_eq!(config.platform_limits.len(), 2);
324        assert_eq!(config.single_tx_max_sats, 50_000_000);
325    }
326
327    #[tokio::test]
328    async fn test_single_tx_limit() {
329        let enforcer = LimitEnforcer::new(LimitConfig::default());
330
331        // Should fail - exceeds single tx limit
332        let result = enforcer.check_transaction("user1", 100_000_000).await;
333        assert!(result.is_err());
334
335        // Should succeed
336        let result = enforcer.check_transaction("user1", 1_000_000).await;
337        assert!(result.is_ok());
338    }
339
340    #[tokio::test]
341    async fn test_usage_tracking() {
342        let enforcer = LimitEnforcer::new(LimitConfig::default());
343
344        // Record some transactions
345        enforcer.record_transaction("user1", 1_000_000, None).await;
346        enforcer.record_transaction("user1", 2_000_000, None).await;
347
348        // Check usage
349        let (usage, count) = enforcer.get_user_usage("user1", LimitPeriod::Daily).await;
350        assert_eq!(usage, 3_000_000);
351        assert_eq!(count, 2);
352    }
353}