use std::fmt;
use alloy::primitives::{Address, U256};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PositionIdentity {
chain_id: u64,
market: Address,
account: Address,
}
impl PositionIdentity {
#[must_use]
pub const fn new(chain_id: u64, market: Address, account: Address) -> Self {
Self {
chain_id,
market,
account,
}
}
#[must_use]
pub const fn chain_id(&self) -> u64 {
self.chain_id
}
#[must_use]
pub const fn market(&self) -> Address {
self.market
}
#[must_use]
pub const fn account(&self) -> Address {
self.account
}
}
impl fmt::Display for PositionIdentity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"position of {} in market {} on chain {}",
self.account, self.market, self.chain_id
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Freshness {
Any,
AtOrAfter {
block: u64,
},
AtMost {
max_age: u64,
head: u64,
},
}
impl Freshness {
#[must_use]
pub fn accepts(self, observed_block: Option<u64>) -> bool {
match self {
Self::Any => true,
Self::AtOrAfter { block } => observed_block.is_some_and(|observed| observed >= block),
Self::AtMost { max_age, head } => {
observed_block.is_some_and(|observed| observed >= head.saturating_sub(max_age))
}
}
}
}
impl fmt::Display for Freshness {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Any => f.write_str("any age"),
Self::AtOrAfter { block } => write!(f, "at or after block {block}"),
Self::AtMost { max_age, head } => {
write!(f, "at most {max_age} blocks behind head {head}")
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HealthFactor {
NoDebt,
Ratio(U256),
}
impl HealthFactor {
#[must_use]
pub fn is_liquidatable(self) -> bool {
match self {
Self::NoDebt => false,
Self::Ratio(ratio) => ratio < U256::from(1_000_000_000_000_000_000u64),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PositionReading {
identity: PositionIdentity,
observed_block: u64,
health_factor: HealthFactor,
}
impl PositionReading {
#[must_use]
pub const fn new(
identity: PositionIdentity,
observed_block: u64,
health_factor: HealthFactor,
) -> Self {
Self {
identity,
observed_block,
health_factor,
}
}
#[must_use]
pub const fn identity(&self) -> &PositionIdentity {
&self.identity
}
#[must_use]
pub const fn observed_block(&self) -> u64 {
self.observed_block
}
#[must_use]
pub const fn health_factor(&self) -> HealthFactor {
self.health_factor
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum PositionRefusal {
#[error("no position observer is installed on this session")]
NoPositionOwner,
#[error("{identity} is not on this session's chain {session_chain_id}")]
ChainScopeMismatch {
identity: PositionIdentity,
session_chain_id: u64,
},
#[error("no position observer serves {identity}")]
MarketNotServed {
identity: PositionIdentity,
},
#[error("{identity} holds no position in a served market")]
UnknownPosition {
identity: PositionIdentity,
},
#[error("reading {identity} failed and may be retried: {reason}")]
TransientRead {
identity: PositionIdentity,
reason: String,
},
#[error("reading {identity} produced rows that cannot be interpreted: {reason}")]
UnreadablePosition {
identity: PositionIdentity,
reason: String,
},
#[error("the reading for {identity} is too old ({observed_block:?}, required {required})")]
StaleObservation {
identity: PositionIdentity,
observed_block: Option<u64>,
required: Freshness,
},
}
impl PositionRefusal {
#[must_use]
pub const fn is_retryable(&self) -> bool {
matches!(self, Self::TransientRead { .. })
}
}
pub trait PositionObserver: Send + Sync {
fn read_position(
&self,
identity: &PositionIdentity,
freshness: &Freshness,
) -> Result<PositionReading, PositionRefusal>;
}