use crate::error::{Result, TokenError};
use crate::treasury::FeeDistributionConfig;
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tenzro_types::asset::AssetId;
use tenzro_types::primitives::Timestamp;
use tracing::{debug, info};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum FeeSource {
Transaction,
Settlement,
Bridge,
ModelInference,
Storage,
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeeRecord {
pub fee_id: String,
pub asset_id: AssetId,
pub amount: u128,
pub source: FeeSource,
pub treasury_share: u128,
pub burn_share: u128,
pub staker_share: u128,
pub timestamp: Timestamp,
}
impl FeeRecord {
pub fn new(
asset_id: AssetId,
amount: u128,
source: FeeSource,
treasury_share: u128,
burn_share: u128,
staker_share: u128,
) -> Self {
Self {
fee_id: uuid::Uuid::new_v4().to_string(),
asset_id,
amount,
source,
treasury_share,
burn_share,
staker_share,
timestamp: Timestamp::now(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Default)]
pub struct FeeStats {
pub total_collected: HashMap<AssetId, u128>,
pub by_source: HashMap<FeeSource, u128>,
pub total_to_treasury: u128,
pub total_burned: u128,
pub total_to_stakers: u128,
pub fee_count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DistributionHistory {
pub period_start: Timestamp,
pub period_end: Timestamp,
pub total_fees: u128,
pub fees_by_asset: HashMap<AssetId, u128>,
pub treasury_share: u128,
pub burn_share: u128,
pub staker_share: u128,
}
impl DistributionHistory {
pub fn new(period_start: Timestamp, period_end: Timestamp) -> Self {
Self {
period_start,
period_end,
total_fees: 0,
fees_by_asset: HashMap::new(),
treasury_share: 0,
burn_share: 0,
staker_share: 0,
}
}
pub fn add_fee(&mut self, record: &FeeRecord) {
self.total_fees = self.total_fees.saturating_add(record.amount);
let asset_total = self.fees_by_asset.get(&record.asset_id).copied().unwrap_or(0);
self.fees_by_asset.insert(
record.asset_id.clone(),
asset_total.saturating_add(record.amount),
);
self.treasury_share = self.treasury_share.saturating_add(record.treasury_share);
self.burn_share = self.burn_share.saturating_add(record.burn_share);
self.staker_share = self.staker_share.saturating_add(record.staker_share);
}
}
#[derive(Debug)]
pub struct FeeProcessor {
config: parking_lot::RwLock<FeeDistributionConfig>,
fee_records: DashMap<String, FeeRecord>,
stats: parking_lot::RwLock<FeeStats>,
history: DashMap<String, DistributionHistory>,
current_period: parking_lot::RwLock<Option<String>>,
}
impl FeeProcessor {
pub fn new() -> Self {
Self {
config: parking_lot::RwLock::new(FeeDistributionConfig::default()),
fee_records: DashMap::new(),
stats: parking_lot::RwLock::new(FeeStats::default()),
history: DashMap::new(),
current_period: parking_lot::RwLock::new(None),
}
}
pub fn process_fee(&self, asset_id: AssetId, amount: u128, source: FeeSource) -> Result<FeeRecord> {
if amount == 0 {
return Err(TokenError::InvalidAmount("Fee amount must be greater than zero".to_string()));
}
let config = self.config.read();
let distribution = config.calculate_distribution(amount);
drop(config);
let record = FeeRecord::new(
asset_id.clone(),
amount,
source,
distribution.treasury_amount,
distribution.burn_amount,
distribution.staker_amount,
);
let fee_id = record.fee_id.clone();
let mut stats = self.stats.write();
let asset_total = stats.total_collected.get(&asset_id).copied().unwrap_or(0);
stats.total_collected.insert(
asset_id.clone(),
asset_total.checked_add(amount)
.ok_or_else(|| TokenError::ArithmeticOverflow {
operation: "fee total collected".to_string(),
})?,
);
let source_total = stats.by_source.get(&source).copied().unwrap_or(0);
stats.by_source.insert(
source,
source_total.checked_add(amount)
.ok_or_else(|| TokenError::ArithmeticOverflow {
operation: "fee by source".to_string(),
})?,
);
stats.total_to_treasury = stats.total_to_treasury.checked_add(distribution.treasury_amount)
.ok_or_else(|| TokenError::ArithmeticOverflow {
operation: "fee total to treasury".to_string(),
})?;
stats.total_burned = stats.total_burned.checked_add(distribution.burn_amount)
.ok_or_else(|| TokenError::ArithmeticOverflow {
operation: "fee total burned".to_string(),
})?;
stats.total_to_stakers = stats.total_to_stakers.checked_add(distribution.staker_amount)
.ok_or_else(|| TokenError::ArithmeticOverflow {
operation: "fee total to stakers".to_string(),
})?;
stats.fee_count += 1;
drop(stats);
self.add_to_history(&record);
self.fee_records.insert(fee_id.clone(), record.clone());
debug!(
"Processed fee: {} of {} (treasury: {}, burn: {}, stakers: {})",
amount, asset_id.as_str(),
distribution.treasury_amount,
distribution.burn_amount,
distribution.staker_amount
);
Ok(record)
}
pub fn get_fee_stats(&self) -> FeeStats {
self.stats.read().clone()
}
pub fn get_distribution_history(&self, period: &str) -> Option<DistributionHistory> {
self.history.get(period).map(|h| h.clone())
}
pub fn get_all_history(&self) -> Vec<(String, DistributionHistory)> {
self.history
.iter()
.map(|entry| (entry.key().clone(), entry.value().clone()))
.collect()
}
pub fn start_new_period(&self, period_id: String, start_time: Timestamp) {
if let Some(current) = self.current_period.read().as_ref()
&& let Some(mut history) = self.history.get_mut(current)
{
history.period_end = Timestamp::now();
}
let history = DistributionHistory::new(start_time, Timestamp::now());
self.history.insert(period_id.clone(), history);
*self.current_period.write() = Some(period_id.clone());
info!("Started new distribution period: {}", period_id);
}
pub fn update_config(&self, config: FeeDistributionConfig) -> Result<()> {
config.validate()?;
*self.config.write() = config;
info!("Updated fee distribution configuration");
Ok(())
}
pub fn get_config(&self) -> FeeDistributionConfig {
self.config.read().clone()
}
fn add_to_history(&self, record: &FeeRecord) {
let current_period = self.current_period.read().clone();
if let Some(period_id) = current_period {
if let Some(mut history) = self.history.get_mut(&period_id) {
history.add_fee(record);
}
} else {
drop(current_period);
let period_id = format!("period_{}", Timestamp::now().as_millis());
self.start_new_period(period_id.clone(), Timestamp::now());
if let Some(mut history) = self.history.get_mut(&period_id) {
history.add_fee(record);
}
}
}
pub fn get_fee_record(&self, fee_id: &str) -> Option<FeeRecord> {
self.fee_records.get(fee_id).map(|r| r.clone())
}
pub fn get_all_fee_records(&self) -> Vec<FeeRecord> {
self.fee_records
.iter()
.map(|entry| entry.value().clone())
.collect()
}
}
impl Default for FeeProcessor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process_fee() {
let processor = FeeProcessor::new();
let asset_id = AssetId::tnzo();
let record = processor.process_fee(asset_id.clone(), 10000, FeeSource::Transaction).unwrap();
assert_eq!(record.amount, 10000);
assert_eq!(record.treasury_share, 4000); assert_eq!(record.burn_share, 3000); assert_eq!(record.staker_share, 3000);
let stats = processor.get_fee_stats();
assert_eq!(stats.fee_count, 1);
assert_eq!(stats.total_to_treasury, 4000);
}
#[test]
fn test_fee_stats() {
let processor = FeeProcessor::new();
let asset_id = AssetId::tnzo();
processor.process_fee(asset_id.clone(), 10000, FeeSource::Transaction).unwrap();
processor.process_fee(asset_id.clone(), 5000, FeeSource::Settlement).unwrap();
let stats = processor.get_fee_stats();
assert_eq!(stats.fee_count, 2);
assert_eq!(*stats.total_collected.get(&asset_id).unwrap(), 15000);
}
#[test]
fn test_history_periods() {
let processor = FeeProcessor::new();
let asset_id = AssetId::tnzo();
processor.start_new_period("period1".to_string(), Timestamp::now());
processor.process_fee(asset_id.clone(), 10000, FeeSource::Transaction).unwrap();
let history = processor.get_distribution_history("period1").unwrap();
assert_eq!(history.total_fees, 10000);
}
}