use std::collections::HashMap;
use std::sync::Mutex;
use serde::{Deserialize, Serialize};
use crate::error::{Result, ScemaDexError};
use crate::mesh::ExperienceBatch;
use crate::primitives::Usdc;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LineageEntry {
pub batch_digest: String,
pub seller_id: String,
pub weight: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RoyaltySplit {
pub student_keeps: Usdc,
pub payouts: Vec<(String, Usdc)>,
}
#[derive(Default)]
pub struct LineageLedger {
lineages: Mutex<HashMap<String, Vec<LineageEntry>>>,
earned: Mutex<HashMap<String, u64>>,
}
impl LineageLedger {
pub fn new() -> Self {
Self::default()
}
pub fn record_training(&self, student_id: impl Into<String>, batch: &ExperienceBatch) -> Result<()> {
let entry = LineageEntry {
batch_digest: batch.digest(),
seller_id: batch.peer_id.clone(),
weight: batch.transitions as u64,
};
self.lineages
.lock()
.map_err(|_| ScemaDexError::Mesh("lineage lock poisoned".into()))?
.entry(student_id.into())
.or_default()
.push(entry);
Ok(())
}
pub fn lineage(&self, student_id: &str) -> Vec<LineageEntry> {
self.lineages
.lock()
.ok()
.and_then(|l| l.get(student_id).cloned())
.unwrap_or_default()
}
pub fn lineage_root(&self, student_id: &str) -> String {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for entry in self.lineage(student_id) {
for b in entry.batch_digest.bytes() {
h ^= b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
}
format!("{h:016x}")
}
pub fn distribute(
&self,
student_id: &str,
fee: Usdc,
royalty_bps: u32,
) -> Result<RoyaltySplit> {
let lineage = self.lineage(student_id);
let pool = fee.0.saturating_mul(royalty_bps.min(10_000) as u64) / 10_000;
let total_weight: u64 = lineage
.iter()
.filter(|e| e.seller_id != student_id)
.map(|e| e.weight)
.sum();
let mut payouts: Vec<(String, Usdc)> = Vec::new();
let mut paid = 0u64;
if total_weight > 0 {
let mut by_seller: HashMap<String, u64> = HashMap::new();
for e in lineage.iter().filter(|e| e.seller_id != student_id) {
*by_seller.entry(e.seller_id.clone()).or_default() += e.weight;
}
let mut sellers: Vec<_> = by_seller.into_iter().collect();
sellers.sort(); for (seller, weight) in sellers {
let royalty = pool
.saturating_mul(weight)
.checked_div(total_weight)
.unwrap_or(0);
if royalty > 0 {
paid += royalty;
payouts.push((seller, Usdc(royalty)));
}
}
let mut earned = self
.earned
.lock()
.map_err(|_| ScemaDexError::Mesh("royalty ledger lock poisoned".into()))?;
for (seller, royalty) in &payouts {
*earned.entry(seller.clone()).or_default() += royalty.0;
}
}
Ok(RoyaltySplit {
student_keeps: Usdc(fee.0 - paid),
payouts,
})
}
pub fn royalties_earned(&self, seller_id: &str) -> Usdc {
Usdc(
self.earned
.lock()
.ok()
.and_then(|e| e.get(seller_id).copied())
.unwrap_or(0),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn batch(seller: &str, transitions: u32) -> ExperienceBatch {
ExperienceBatch {
peer_id: seller.into(),
transitions,
price: Usdc(1_000),
payload: vec![transitions as u8],
}
}
#[test]
fn batch_digest_is_stable_and_content_sensitive() {
let a = batch("alpha", 100);
assert_eq!(a.digest(), batch("alpha", 100).digest());
assert_ne!(a.digest(), batch("alpha", 101).digest());
}
#[test]
fn no_lineage_means_student_keeps_everything() {
let ledger = LineageLedger::new();
let split = ledger.distribute("solo", Usdc(1_000_000), 1_000).unwrap();
assert_eq!(split.student_keeps, Usdc(1_000_000));
assert!(split.payouts.is_empty());
}
#[test]
fn royalties_split_prorata_by_transitions_and_conserve_value() {
let ledger = LineageLedger::new();
ledger.record_training("student", &batch("alpha", 7_500)).unwrap();
ledger.record_training("student", &batch("beta", 2_500)).unwrap();
let split = ledger.distribute("student", Usdc(1_000_000), 1_000).unwrap();
assert_eq!(split.payouts.len(), 2);
assert_eq!(split.payouts[0], ("alpha".into(), Usdc(75_000)));
assert_eq!(split.payouts[1], ("beta".into(), Usdc(25_000)));
let total: u64 = split.payouts.iter().map(|(_, p)| p.0).sum();
assert_eq!(split.student_keeps.0 + total, 1_000_000);
ledger.distribute("student", Usdc(1_000_000), 1_000).unwrap();
assert_eq!(ledger.royalties_earned("alpha"), Usdc(150_000));
}
#[test]
fn self_sold_batches_earn_no_royalties() {
let ledger = LineageLedger::new();
ledger.record_training("student", &batch("student", 10_000)).unwrap();
let split = ledger.distribute("student", Usdc(1_000_000), 1_000).unwrap();
assert!(split.payouts.is_empty());
assert_eq!(split.student_keeps, Usdc(1_000_000));
}
#[test]
fn same_seller_batches_aggregate_into_one_payout() {
let ledger = LineageLedger::new();
ledger.record_training("student", &batch("alpha", 1_000)).unwrap();
ledger.record_training("student", &batch("alpha", 3_000)).unwrap();
let split = ledger.distribute("student", Usdc(1_000_000), 1_000).unwrap();
assert_eq!(split.payouts, vec![("alpha".into(), Usdc(100_000))]);
}
#[test]
fn lineage_root_reflects_training_history() {
let ledger = LineageLedger::new();
let untrained_root = ledger.lineage_root("student");
ledger.record_training("student", &batch("alpha", 100)).unwrap();
assert_ne!(ledger.lineage_root("student"), untrained_root);
}
}