use chrono::{DateTime, Duration, Utc};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use std::collections::{HashMap, VecDeque};
use std::fmt;
use uuid::Uuid;
use crate::error::{CoreError, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmergencyPause {
pub is_paused: bool,
pub pause_reason: Option<String>,
pub paused_by: Option<Uuid>,
pub paused_at: Option<DateTime<Utc>>,
pub authorized_pausers: Vec<Uuid>,
}
impl EmergencyPause {
pub fn new(authorized_pausers: Vec<Uuid>) -> Self {
Self {
is_paused: false,
pause_reason: None,
paused_by: None,
paused_at: None,
authorized_pausers,
}
}
pub fn pause(&mut self, admin_id: Uuid, reason: String) -> Result<()> {
if !self.authorized_pausers.contains(&admin_id) {
return Err(CoreError::Validation(
"Unauthorized to pause system".to_string(),
));
}
self.is_paused = true;
self.pause_reason = Some(reason);
self.paused_by = Some(admin_id);
self.paused_at = Some(Utc::now());
Ok(())
}
pub fn resume(&mut self, admin_id: Uuid) -> Result<()> {
if !self.authorized_pausers.contains(&admin_id) {
return Err(CoreError::Validation(
"Unauthorized to resume system".to_string(),
));
}
self.is_paused = false;
self.pause_reason = None;
self.paused_by = None;
self.paused_at = None;
Ok(())
}
pub fn check_allowed(&self) -> Result<()> {
if self.is_paused {
Err(CoreError::Validation(format!(
"System is paused: {}",
self.pause_reason
.as_ref()
.unwrap_or(&"Unknown reason".to_string())
)))
} else {
Ok(())
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct MultiSigOperation {
pub operation_id: Uuid,
pub operation_type: OperationType,
pub parameters: String,
pub required_signatures: i32,
pub current_signatures: i32,
pub signers: Vec<Uuid>,
pub authorized_signers: Vec<Uuid>,
pub status: MultiSigStatus,
pub expires_at: DateTime<Utc>,
pub initiated_by: Uuid,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, PartialEq, Eq)]
#[sqlx(type_name = "varchar", rename_all = "lowercase")]
#[derive(Default)]
pub enum OperationType {
Withdraw,
#[default]
Transfer,
Pause,
ConfigChange,
}
impl fmt::Display for OperationType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OperationType::Withdraw => write!(f, "withdraw"),
OperationType::Transfer => write!(f, "transfer"),
OperationType::Pause => write!(f, "pause"),
OperationType::ConfigChange => write!(f, "config_change"),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, PartialEq, Eq)]
#[sqlx(type_name = "varchar", rename_all = "lowercase")]
#[derive(Default)]
pub enum MultiSigStatus {
#[default]
Pending,
Approved,
Rejected,
Expired,
Executed,
}
impl fmt::Display for MultiSigStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MultiSigStatus::Pending => write!(f, "pending"),
MultiSigStatus::Approved => write!(f, "approved"),
MultiSigStatus::Rejected => write!(f, "rejected"),
MultiSigStatus::Expired => write!(f, "expired"),
MultiSigStatus::Executed => write!(f, "executed"),
}
}
}
impl MultiSigOperation {
pub fn new(
operation_type: OperationType,
parameters: String,
required_signatures: i32,
authorized_signers: Vec<Uuid>,
initiator_id: Uuid,
expiration_hours: i64,
) -> Result<Self> {
if required_signatures <= 0 {
return Err(CoreError::Validation(
"Required signatures must be positive".to_string(),
));
}
if required_signatures > authorized_signers.len() as i32 {
return Err(CoreError::Validation(
"Required signatures exceeds authorized signers".to_string(),
));
}
let now = Utc::now();
Ok(Self {
operation_id: Uuid::new_v4(),
operation_type,
parameters,
required_signatures,
current_signatures: 0,
signers: Vec::new(),
authorized_signers,
status: MultiSigStatus::Pending,
expires_at: now + Duration::hours(expiration_hours),
initiated_by: initiator_id,
created_at: now,
updated_at: now,
})
}
pub fn sign(&mut self, signer_id: Uuid) -> Result<()> {
if Utc::now() > self.expires_at {
self.status = MultiSigStatus::Expired;
return Err(CoreError::Validation("Operation expired".to_string()));
}
if self.status != MultiSigStatus::Pending {
return Err(CoreError::Validation(format!(
"Operation is not pending: {}",
self.status
)));
}
if !self.authorized_signers.contains(&signer_id) {
return Err(CoreError::Validation("Signer not authorized".to_string()));
}
if self.signers.contains(&signer_id) {
return Err(CoreError::Validation("Already signed".to_string()));
}
self.signers.push(signer_id);
self.current_signatures += 1;
self.updated_at = Utc::now();
if self.current_signatures >= self.required_signatures {
self.status = MultiSigStatus::Approved;
}
Ok(())
}
pub fn is_approved(&self) -> bool {
self.status == MultiSigStatus::Approved
}
pub fn mark_executed(&mut self) {
self.status = MultiSigStatus::Executed;
self.updated_at = Utc::now();
}
}
#[derive(Debug, Clone)]
pub struct WithdrawalRateLimiter {
pub max_per_hour: Decimal,
pub max_per_day: Decimal,
recent_withdrawals: HashMap<Uuid, VecDeque<WithdrawalRecord>>,
}
#[derive(Debug, Clone)]
struct WithdrawalRecord {
amount: Decimal,
timestamp: DateTime<Utc>,
}
impl WithdrawalRateLimiter {
pub fn new(max_per_hour: Decimal, max_per_day: Decimal) -> Self {
Self {
max_per_hour,
max_per_day,
recent_withdrawals: HashMap::new(),
}
}
pub fn check_withdrawal(&mut self, user_id: Uuid, amount: Decimal) -> Result<()> {
let now = Utc::now();
let hour_ago = now - Duration::hours(1);
let day_ago = now - Duration::days(1);
let user_withdrawals = self.recent_withdrawals.entry(user_id).or_default();
user_withdrawals.retain(|w| w.timestamp > day_ago);
let hour_total: Decimal = user_withdrawals
.iter()
.filter(|w| w.timestamp > hour_ago)
.map(|w| w.amount)
.sum();
let day_total: Decimal = user_withdrawals.iter().map(|w| w.amount).sum();
if hour_total + amount > self.max_per_hour {
return Err(CoreError::Validation(format!(
"Hourly withdrawal limit exceeded. Current: {}, Limit: {}",
hour_total + amount,
self.max_per_hour
)));
}
if day_total + amount > self.max_per_day {
return Err(CoreError::Validation(format!(
"Daily withdrawal limit exceeded. Current: {}, Limit: {}",
day_total + amount,
self.max_per_day
)));
}
Ok(())
}
pub fn record_withdrawal(&mut self, user_id: Uuid, amount: Decimal) {
let user_withdrawals = self.recent_withdrawals.entry(user_id).or_default();
user_withdrawals.push_back(WithdrawalRecord {
amount,
timestamp: Utc::now(),
});
}
}
#[derive(Debug, Clone)]
pub struct AnomalyDetector {
pub large_trade_threshold: Decimal,
pub rapid_trade_threshold: i32,
recent_trades: HashMap<Uuid, VecDeque<DateTime<Utc>>>,
}
impl AnomalyDetector {
pub fn new(large_trade_threshold: Decimal, rapid_trade_threshold: i32) -> Self {
Self {
large_trade_threshold,
rapid_trade_threshold,
recent_trades: HashMap::new(),
}
}
pub fn check_large_trade(
&self,
trade_amount: Decimal,
pool_liquidity: Decimal,
) -> Option<AnomalyAlert> {
if pool_liquidity == dec!(0) {
return None;
}
let trade_percentage = trade_amount / pool_liquidity;
if trade_percentage > self.large_trade_threshold {
Some(AnomalyAlert {
alert_type: AnomalyType::LargeTrade,
severity: AlertSeverity::High,
description: format!(
"Large trade detected: {} ({:.2}% of pool liquidity)",
trade_amount,
trade_percentage * dec!(100)
),
timestamp: Utc::now(),
})
} else {
None
}
}
pub fn check_rapid_trading(&mut self, user_id: Uuid) -> Option<AnomalyAlert> {
let now = Utc::now();
let minute_ago = now - Duration::minutes(1);
let user_trades = self.recent_trades.entry(user_id).or_default();
user_trades.retain(|t| *t > minute_ago);
user_trades.push_back(now);
if user_trades.len() as i32 > self.rapid_trade_threshold {
Some(AnomalyAlert {
alert_type: AnomalyType::RapidTrading,
severity: AlertSeverity::Medium,
description: format!(
"Rapid trading detected: {} trades in 1 minute (threshold: {})",
user_trades.len(),
self.rapid_trade_threshold
),
timestamp: now,
})
} else {
None
}
}
pub fn check_price_manipulation(
&self,
price_change: Decimal,
max_allowed_change: Decimal,
) -> Option<AnomalyAlert> {
if price_change.abs() > max_allowed_change {
Some(AnomalyAlert {
alert_type: AnomalyType::PriceManipulation,
severity: AlertSeverity::Critical,
description: format!(
"Suspicious price movement: {:.2}% (max allowed: {:.2}%)",
price_change * dec!(100),
max_allowed_change * dec!(100)
),
timestamp: Utc::now(),
})
} else {
None
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct AnomalyAlert {
pub alert_type: AnomalyType,
pub severity: AlertSeverity,
pub description: String,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
pub enum AnomalyType {
LargeTrade,
RapidTrading,
PriceManipulation,
SuspiciousPattern,
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
pub enum AlertSeverity {
Low,
Medium,
High,
Critical,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_emergency_pause() {
let admin1 = Uuid::new_v4();
let admin2 = Uuid::new_v4();
let unauthorized = Uuid::new_v4();
let mut pause_system = EmergencyPause::new(vec![admin1, admin2]);
assert!(!pause_system.is_paused);
assert!(pause_system.check_allowed().is_ok());
assert!(
pause_system
.pause(admin1, "Security issue".to_string())
.is_ok()
);
assert!(pause_system.is_paused);
assert!(pause_system.check_allowed().is_err());
assert!(pause_system.resume(unauthorized).is_err());
assert!(pause_system.resume(admin2).is_ok());
assert!(!pause_system.is_paused);
assert!(pause_system.check_allowed().is_ok());
}
#[test]
fn test_multisig_operation() {
let signer1 = Uuid::new_v4();
let signer2 = Uuid::new_v4();
let signer3 = Uuid::new_v4();
let unauthorized = Uuid::new_v4();
let mut operation = MultiSigOperation::new(
OperationType::Withdraw,
"{\"amount\": 1000}".to_string(),
2, vec![signer1, signer2, signer3],
signer1,
24, )
.unwrap();
assert_eq!(operation.status, MultiSigStatus::Pending);
assert_eq!(operation.current_signatures, 0);
assert!(operation.sign(signer1).is_ok());
assert_eq!(operation.current_signatures, 1);
assert!(!operation.is_approved());
assert!(operation.sign(unauthorized).is_err());
assert!(operation.sign(signer2).is_ok());
assert_eq!(operation.current_signatures, 2);
assert!(operation.is_approved());
assert!(operation.sign(signer2).is_err());
}
#[test]
fn test_withdrawal_rate_limiter() {
let mut limiter = WithdrawalRateLimiter::new(
dec!(1000), dec!(5000), );
let user = Uuid::new_v4();
assert!(limiter.check_withdrawal(user, dec!(500)).is_ok());
limiter.record_withdrawal(user, dec!(500));
assert!(limiter.check_withdrawal(user, dec!(400)).is_ok());
limiter.record_withdrawal(user, dec!(400));
assert!(limiter.check_withdrawal(user, dec!(200)).is_err());
assert!(limiter.check_withdrawal(user, dec!(100)).is_ok());
}
#[test]
fn test_anomaly_detector() {
let mut detector = AnomalyDetector::new(
dec!(0.1), 5, );
let alert = detector.check_large_trade(dec!(150), dec!(1000));
assert!(alert.is_some());
let alert = alert.unwrap();
assert_eq!(alert.alert_type, AnomalyType::LargeTrade);
assert_eq!(alert.severity, AlertSeverity::High);
let alert = detector.check_large_trade(dec!(50), dec!(1000));
assert!(alert.is_none());
let user = Uuid::new_v4();
for _ in 0..6 {
let alert = detector.check_rapid_trading(user);
if let Some(a) = alert {
assert_eq!(a.alert_type, AnomalyType::RapidTrading);
break;
}
}
let alert = detector.check_price_manipulation(dec!(0.15), dec!(0.1));
assert!(alert.is_some());
let alert = alert.unwrap();
assert_eq!(alert.alert_type, AnomalyType::PriceManipulation);
assert_eq!(alert.severity, AlertSeverity::Critical);
}
}