//! Networking profit tracking. //! //! Aggregates earnings from content sales (ecash) and Lightning routing fees. use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::path::Path; use tokio::fs; use tracing::debug; use super::ecash; const PROFITS_FILE: &str = "wallet/profits.json"; /// Earnings breakdown by source. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ProfitsSummary { /// Total earnings in sats from all sources. pub total_sats: u64, /// Earnings from ecash content sales. pub content_sales_sats: u64, /// Earnings from Lightning routing fees. pub routing_fees_sats: u64, /// Recent earning entries (newest first). pub recent: Vec, } /// A single profit event. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProfitEntry { pub source: ProfitSource, pub amount_sats: u64, pub timestamp: String, #[serde(default)] pub description: String, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ProfitSource { ContentSale, RoutingFee, } /// Load profits summary from disk. pub async fn load_profits(data_dir: &Path) -> Result { let path = data_dir.join(PROFITS_FILE); if !path.exists() { return Ok(ProfitsSummary::default()); } let content = fs::read_to_string(&path) .await .context("Failed to read profits file")?; let summary: ProfitsSummary = serde_json::from_str(&content).unwrap_or_default(); Ok(summary) } /// Save profits summary to disk. pub async fn save_profits(data_dir: &Path, summary: &ProfitsSummary) -> Result<()> { let dir = data_dir.join("wallet"); fs::create_dir_all(&dir) .await .context("Failed to create wallet dir")?; let path = data_dir.join(PROFITS_FILE); let content = serde_json::to_string_pretty(summary).context("Failed to serialize profits")?; fs::write(&path, content) .await .context("Failed to write profits file")?; Ok(()) } /// Record a content sale profit. pub async fn record_content_sale(data_dir: &Path, amount_sats: u64, description: &str) -> Result<()> { let mut summary = load_profits(data_dir).await?; summary.total_sats += amount_sats; summary.content_sales_sats += amount_sats; summary.recent.insert( 0, ProfitEntry { source: ProfitSource::ContentSale, amount_sats, timestamp: chrono::Utc::now().to_rfc3339(), description: description.to_string(), }, ); // Keep only the last 100 entries summary.recent.truncate(100); save_profits(data_dir, &summary).await?; debug!("Recorded content sale: {} sats", amount_sats); Ok(()) } /// Compute a full profits summary including ecash receive transactions. pub async fn get_networking_profits(data_dir: &Path) -> Result { let mut summary = load_profits(data_dir).await?; // Also count ecash "receive" transactions as content sales revenue let wallet = ecash::load_wallet(data_dir).await?; let ecash_received: u64 = wallet .transactions .iter() .filter(|tx| matches!(tx.tx_type, ecash::TransactionType::Receive)) .map(|tx| tx.amount_sats) .sum(); // Use the higher of tracked profits or ecash receives as content sales if ecash_received > summary.content_sales_sats { summary.content_sales_sats = ecash_received; } summary.total_sats = summary.content_sales_sats + summary.routing_fees_sats; Ok(summary) }