use std::collections::BTreeMap;
use crate::account::state::{Abstraction, PerpState, SpotRisk, SpotState};
use crate::client::USDC_INDEX;
use crate::utils::{Address, Dec};
pub const USDC: u32 = USDC_INDEX;
pub const MOST_USERS: usize = 10;
pub const LTV: [(&str, i128); 2] = [("HYPE", 65), ("UBTC", 50)];
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Collateral(BTreeMap<String, u32>);
impl Collateral {
#[must_use]
pub fn new() -> Collateral {
Collateral::default()
}
#[must_use]
pub fn with(mut self, dex: &str, token: u32) -> Collateral {
self.0.insert(dex.to_owned(), token);
self
}
#[must_use]
pub fn of(&self, dex: &str) -> u32 {
self.0.get(dex).copied().unwrap_or(USDC)
}
#[must_use]
pub fn tokens(&self) -> Vec<u32> {
let mut tokens = vec![USDC];
for token in self.0.values() {
if !tokens.contains(token) {
tokens.push(*token);
}
}
tokens[1..].sort_unstable();
tokens
}
}
impl FromIterator<(String, u32)> for Collateral {
fn from_iter<I: IntoIterator<Item = (String, u32)>>(iter: I) -> Collateral {
Collateral(iter.into_iter().collect())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Surface<'a> {
Perps { dex: &'a str },
Spot { quote: u32 },
Outcomes,
}
impl Surface<'_> {
#[must_use]
pub fn token(&self, collateral: &Collateral) -> u32 {
match self {
Surface::Perps { dex } => collateral.of(dex),
Surface::Spot { quote } => *quote,
Surface::Outcomes => USDC,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Keeping {
Apart,
Native,
Unified,
Portfolio,
}
impl Keeping {
#[must_use]
pub fn of(mode: Option<&Abstraction>, spot: Option<&SpotState>) -> Keeping {
match mode {
Some(Abstraction::UnifiedAccount) => Keeping::Unified,
Some(Abstraction::PortfolioMargin) => Keeping::Portfolio,
Some(Abstraction::DexAbstraction) => Keeping::Native,
Some(_) => Keeping::Apart,
None if spot.is_some_and(|spot| spot.portfolio_margin_enabled == Some(true)) => Keeping::Portfolio,
None => Keeping::Apart,
}
}
#[must_use]
pub const fn pools(self) -> bool {
matches!(self, Keeping::Unified | Keeping::Portfolio)
}
#[must_use]
pub const fn draws(self, movement: Move) -> Draws {
if self.pools() {
return Draws::Pooled;
}
match movement {
Move::ClassTransfer { to_perp: true } => Draws::Spot,
Move::ClassTransfer { to_perp: false } | Move::Send | Move::Withdraw => Draws::Perp,
}
}
#[must_use]
pub fn refuses(self, movement: Move) -> Option<String> {
let account = match (self, movement) {
(Keeping::Unified, Move::ClassTransfer { .. }) => "a unified account",
(Keeping::Portfolio, Move::ClassTransfer { .. }) => "a portfolio margin account",
_ => return None,
};
Some(format!("{account} holds USDC in one balance, so there is nothing to move between perps and spot"))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Move {
ClassTransfer { to_perp: bool },
Send,
Withdraw,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Draws {
Spot,
Perp,
Pooled,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Pays {
Perps { dex: String },
UsdcPerps,
Spot,
Any,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Balance {
pub token: u32,
pub total: Dec,
pub available: Dec,
pub pays: Pays,
}
#[must_use]
pub fn payer<'a>(balances: &'a [Balance], surface: Surface<'_>, collateral: &Collateral) -> Option<&'a Balance> {
let token = surface.token(collateral);
balances.iter().find(|balance| {
balance.token == token
&& match (&balance.pays, surface) {
(Pays::Any, _) => true,
(Pays::Spot, Surface::Spot { .. } | Surface::Outcomes) => true,
(Pays::Perps { dex }, Surface::Perps { dex: wanted }) => dex == wanted,
(Pays::UsdcPerps, Surface::Perps { .. }) => token == USDC,
_ => false,
}
})
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Source {
Perps {
dex: String,
},
NativePerps,
Spot,
Unified,
PortfolioMargin,
}
impl Source {
#[must_use]
pub fn words(&self) -> String {
match self {
Source::Perps { dex } if dex.is_empty() => "perps".to_owned(),
Source::Perps { dex } => format!("perps on {dex}"),
Source::NativePerps => "perps, the main dex's USDC".to_owned(),
Source::Spot => "spot".to_owned(),
Source::Unified => "unified, in spot".to_owned(),
Source::PortfolioMargin => "portfolio margin".to_owned(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Usable {
pub amount: Dec,
pub token: u32,
pub from: Source,
pub borrowable: Dec,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DexState {
pub dex: String,
pub state: PerpState,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Standing {
pub mode: Option<Abstraction>,
pub dexes: Vec<DexState>,
pub spot: Option<SpotState>,
pub heard: Option<u64>,
}
impl Standing {
#[must_use]
pub fn dex(&self, dex: &str) -> Option<&PerpState> {
self.dexes.iter().find(|state| state.dex == dex).map(|state| &state.state)
}
#[must_use]
pub fn keeping(&self) -> Keeping {
Keeping::of(self.mode.as_ref(), self.spot.as_ref())
}
#[must_use]
pub fn pools(&self) -> bool {
self.keeping().pools()
}
#[must_use]
pub fn free(&self, token: u32) -> Dec {
self.spot.as_ref().map_or(Dec::ZERO, |spot| spot.free(token))
}
#[must_use]
pub fn balances(&self, collateral: &Collateral) -> Vec<Balance> {
let keeping = self.keeping();
let mut out = Vec::new();
if !keeping.pools() {
for dex in &self.dexes {
let token = collateral.of(&dex.dex);
let pays = match (keeping, dex.dex.is_empty(), token == USDC) {
(Keeping::Native, true, _) => Pays::UsdcPerps,
(Keeping::Native, false, true) => continue,
_ => Pays::Perps { dex: dex.dex.clone() },
};
let summary = &dex.state.margin_summary;
out.push(Balance { token, total: summary.account_value, available: dex.state.withdrawable, pays });
}
}
let pays = match keeping.pools() {
true => Pays::Any,
false => Pays::Spot,
};
let counted = collateral.tokens();
let rows = self.spot.iter().flat_map(|spot| &spot.balances).filter(|row| !row.is_outcome());
let mut usdc = false;
for row in rows {
let Some(token) = row.token.filter(|token| counted.contains(token)) else { continue };
let available = row.available().unwrap_or(Dec::ZERO);
if token != USDC && row.total.is_zero() && available.is_zero() {
continue;
}
usdc |= token == USDC;
out.push(Balance { token, total: row.total, available, pays: pays.clone() });
}
if !usdc {
let none = Balance { token: USDC, total: Dec::ZERO, available: Dec::ZERO, pays };
let at = out.iter().position(|balance| balance.pays == Pays::Spot || balance.pays == Pays::Any);
out.insert(at.unwrap_or(out.len()), none);
}
out
}
#[must_use]
pub fn usable(&self, surface: Surface<'_>, collateral: &Collateral, price: impl Fn(&str) -> Option<Dec>) -> Usable {
let keeping = self.keeping();
let token = surface.token(collateral);
let balances = self.balances(collateral);
let paying = payer(&balances, surface, collateral);
let from = match (keeping, paying.map(|balance| &balance.pays), surface) {
(Keeping::Unified, _, _) => Source::Unified,
(Keeping::Portfolio, _, _) => Source::PortfolioMargin,
(_, Some(Pays::UsdcPerps), Surface::Perps { dex }) if !dex.is_empty() => Source::NativePerps,
(_, _, Surface::Perps { dex }) => Source::Perps { dex: dex.to_owned() },
(_, _, Surface::Spot { .. } | Surface::Outcomes) => Source::Spot,
};
let free = paying.map_or(Dec::ZERO, |balance| balance.available);
let borrowable = match (keeping, token == USDC) {
(Keeping::Portfolio, true) => self.borrowable(&price),
_ => Dec::ZERO,
};
let amount = free.checked_add(borrowable).unwrap_or(free);
Usable { amount, token, from, borrowable }
}
#[must_use]
pub fn borrowable(&self, price: &impl Fn(&str) -> Option<Dec>) -> Dec {
let Some(spot) = self.spot.as_ref().filter(|_| self.keeping() == Keeping::Portfolio) else {
return Dec::ZERO;
};
if spot.risk() == crate::account::state::Risk::Liquidatable {
return Dec::ZERO;
}
let rows = || spot.balances.iter().filter(|row| !row.is_outcome());
let room = rows()
.filter(|row| row.token != Some(USDC))
.filter_map(|row| {
let ltv = row.ltv.or_else(|| {
LTV.iter().find(|(coin, _)| *coin == row.coin).map(|(_, hundredths)| Dec::lit::<2>(*hundredths))
})?;
row.total.checked_mul(price(&row.coin)?)?.checked_mul(ltv).filter(|lent| lent.is_positive())
})
.fold(Dec::ZERO, |sum, lent| sum.checked_add(lent).unwrap_or(sum));
let owed =
rows().filter_map(|row| row.borrowed).fold(Dec::ZERO, |sum, owed| sum.checked_add(owed).unwrap_or(sum));
let mut out = room.checked_sub(owed).unwrap_or(Dec::ZERO).max(Dec::ZERO);
if let Some(after) = spot.available_after_maintenance(USDC) {
out = out.min(after.max(Dec::ZERO));
}
out
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Standings {
by: BTreeMap<Address, Standing>,
followed: Vec<Address>,
}
impl Standings {
#[must_use]
pub fn of(&self, address: Address) -> Option<&Standing> {
self.by.get(&address)
}
#[must_use]
pub fn followed(&self) -> &[Address] {
&self.followed
}
pub fn follow(&mut self, users: &[Address]) -> bool {
let mut wanted: Vec<Address> = Vec::new();
for user in users {
if !wanted.contains(user) && wanted.len() < MOST_USERS {
wanted.push(*user);
}
}
if wanted == self.followed {
return false;
}
self.by.retain(|address, _| wanted.contains(address));
self.followed = wanted;
true
}
pub fn accept(&mut self, user: Address, now: u64, change: impl FnOnce(&mut Standing)) -> bool {
if !self.followed.contains(&user) {
return false;
}
let standing = self.by.entry(user).or_default();
change(standing);
standing.heard = Some(now);
true
}
pub fn accept_mode(&mut self, user: Address, mode: Option<Abstraction>, now: u64) {
if let Some(mode) = mode {
self.accept(user, now, |standing| standing.mode = Some(mode));
}
}
}
#[cfg(test)]
#[path = "tests/usable.rs"]
mod tests;