use std::sync::Mutex;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::bond::{Bond, BondOutcome};
use crate::error::{Result, ScemaDexError};
use crate::primitives::Usdc;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ScarRecord {
pub peer_id: String,
pub intent_digest: String,
pub slashed_collateral: Usdc,
pub transitions: u32,
pub payload: Vec<u8>,
pub price: Usdc,
pub slashed_unix: u64,
}
impl ScarRecord {
pub fn collateral_per_price(&self) -> f64 {
if self.price.0 == 0 {
f64::INFINITY
} else {
self.slashed_collateral.0 as f64 / self.price.0 as f64
}
}
}
pub fn certify_scar(
bond: &Bond,
outcome: BondOutcome,
peer_id: impl Into<String>,
transitions: u32,
payload: Vec<u8>,
price: Usdc,
) -> Result<ScarRecord> {
if outcome != BondOutcome::Slashed {
return Err(ScemaDexError::Bond(
"scar certification requires a slashed bond".into(),
));
}
if bond.amount.0 == 0 {
return Err(ScemaDexError::Bond(
"zero-collateral bonds cannot certify a scar".into(),
));
}
Ok(ScarRecord {
peer_id: peer_id.into(),
intent_digest: bond.intent_digest.clone(),
slashed_collateral: bond.amount,
transitions,
payload,
price,
slashed_unix: 0,
})
}
#[async_trait]
pub trait ScarMarket: Send + Sync {
async fn sell_scar(&self, scar: ScarRecord) -> Result<()>;
async fn buy_scar(&self, max_price: Usdc) -> Result<ScarRecord>;
}
#[derive(Default)]
pub struct LocalScarMarket {
scars: Mutex<Vec<ScarRecord>>,
}
impl LocalScarMarket {
pub fn new() -> Self {
Self::default()
}
pub fn scar_count(&self) -> usize {
self.scars.lock().map(|v| v.len()).unwrap_or(0)
}
}
#[async_trait]
impl ScarMarket for LocalScarMarket {
async fn sell_scar(&self, scar: ScarRecord) -> Result<()> {
self.scars
.lock()
.map_err(|_| ScemaDexError::Mesh("scar book lock poisoned".into()))?
.push(scar);
Ok(())
}
async fn buy_scar(&self, max_price: Usdc) -> Result<ScarRecord> {
let mut scars = self
.scars
.lock()
.map_err(|_| ScemaDexError::Mesh("scar book lock poisoned".into()))?;
let idx = scars
.iter()
.enumerate()
.filter(|(_, s)| s.price <= max_price)
.max_by(|(_, a), (_, b)| {
a.collateral_per_price()
.partial_cmp(&b.collateral_per_price())
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i);
match idx {
Some(i) => Ok(scars.remove(i)),
None => Err(ScemaDexError::Mesh("no scar under max_price".into())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn slashed_bond(digest: &str, amount: u64) -> Bond {
Bond {
intent_digest: digest.into(),
amount: Usdc(amount),
min_out_raw: 1_000_000,
deadline_unix: 0,
}
}
#[test]
fn honored_bonds_cannot_certify() {
let bond = slashed_bond("d1", 1_000);
assert!(certify_scar(&bond, BondOutcome::Honored, "p", 10, vec![], Usdc(100)).is_err());
}
#[test]
fn zero_collateral_bonds_cannot_certify() {
let bond = slashed_bond("d1", 0);
assert!(certify_scar(&bond, BondOutcome::Slashed, "p", 10, vec![], Usdc(100)).is_err());
}
#[test]
fn slash_certifies_and_carries_collateral() {
let bond = slashed_bond("d1", 5_000);
let scar =
certify_scar(&bond, BondOutcome::Slashed, "p", 10, vec![1, 2], Usdc(100)).unwrap();
assert_eq!(scar.slashed_collateral, Usdc(5_000));
assert_eq!(scar.collateral_per_price(), 50.0);
}
#[tokio::test]
async fn buys_most_collateral_per_dollar_under_cap() {
let market = LocalScarMarket::new();
let cheap_shallow = certify_scar(
&slashed_bond("d1", 1_000),
BondOutcome::Slashed,
"shallow",
5,
vec![],
Usdc(100), )
.unwrap();
let cheap_deep = certify_scar(
&slashed_bond("d2", 50_000),
BondOutcome::Slashed,
"deep",
5,
vec![],
Usdc(500), )
.unwrap();
let over_cap = certify_scar(
&slashed_bond("d3", 1_000_000),
BondOutcome::Slashed,
"pricey",
5,
vec![],
Usdc(10_000),
)
.unwrap();
market.sell_scar(cheap_shallow).await.unwrap();
market.sell_scar(cheap_deep).await.unwrap();
market.sell_scar(over_cap).await.unwrap();
let bought = market.buy_scar(Usdc(1_000)).await.unwrap();
assert_eq!(bought.peer_id, "deep");
assert_eq!(market.scar_count(), 2);
assert!(market.buy_scar(Usdc(50)).await.is_err());
}
}