1use std::collections::BTreeMap;
6
7use crate::account::state::{Abstraction, PerpState, SpotRisk, SpotState};
8use crate::client::USDC_INDEX;
9use crate::utils::{Address, Dec};
10
11pub const USDC: u32 = USDC_INDEX;
13
14pub const MOST_USERS: usize = 10;
16
17pub const LTV: [(&str, i128); 2] = [("HYPE", 65), ("UBTC", 50)];
21
22#[derive(Debug, Clone, Default, PartialEq, Eq)]
25pub struct Collateral(BTreeMap<String, u32>);
26
27impl Collateral {
28 #[must_use]
29 pub fn new() -> Collateral {
30 Collateral::default()
31 }
32
33 #[must_use]
34 pub fn with(mut self, dex: &str, token: u32) -> Collateral {
35 self.0.insert(dex.to_owned(), token);
36 self
37 }
38
39 #[must_use]
41 pub fn of(&self, dex: &str) -> u32 {
42 self.0.get(dex).copied().unwrap_or(USDC)
43 }
44
45 #[must_use]
47 pub fn tokens(&self) -> Vec<u32> {
48 let mut tokens = vec![USDC];
49 for token in self.0.values() {
50 if !tokens.contains(token) {
51 tokens.push(*token);
52 }
53 }
54 tokens[1..].sort_unstable();
55 tokens
56 }
57}
58
59impl FromIterator<(String, u32)> for Collateral {
60 fn from_iter<I: IntoIterator<Item = (String, u32)>>(iter: I) -> Collateral {
61 Collateral(iter.into_iter().collect())
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
67pub enum Surface<'a> {
68 Perps { dex: &'a str },
70 Spot { quote: u32 },
72 Outcomes,
74}
75
76impl Surface<'_> {
77 #[must_use]
79 pub fn token(&self, collateral: &Collateral) -> u32 {
80 match self {
81 Surface::Perps { dex } => collateral.of(dex),
82 Surface::Spot { quote } => *quote,
83 Surface::Outcomes => USDC,
84 }
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90pub enum Keeping {
91 Apart,
94 Native,
96 Unified,
98 Portfolio,
100}
101
102impl Keeping {
103 #[must_use]
106 pub fn of(mode: Option<&Abstraction>, spot: Option<&SpotState>) -> Keeping {
107 match mode {
108 Some(Abstraction::UnifiedAccount) => Keeping::Unified,
109 Some(Abstraction::PortfolioMargin) => Keeping::Portfolio,
110 Some(Abstraction::DexAbstraction) => Keeping::Native,
111 Some(_) => Keeping::Apart,
112 None if spot.is_some_and(|spot| spot.portfolio_margin_enabled == Some(true)) => Keeping::Portfolio,
113 None => Keeping::Apart,
114 }
115 }
116
117 #[must_use]
119 pub const fn pools(self) -> bool {
120 matches!(self, Keeping::Unified | Keeping::Portfolio)
121 }
122
123 #[must_use]
125 pub const fn draws(self, movement: Move) -> Draws {
126 if self.pools() {
127 return Draws::Pooled;
128 }
129 match movement {
130 Move::ClassTransfer { to_perp: true } => Draws::Spot,
131 Move::ClassTransfer { to_perp: false } | Move::Send | Move::Withdraw => Draws::Perp,
132 }
133 }
134
135 #[must_use]
138 pub fn refuses(self, movement: Move) -> Option<String> {
139 let account = match (self, movement) {
140 (Keeping::Unified, Move::ClassTransfer { .. }) => "a unified account",
141 (Keeping::Portfolio, Move::ClassTransfer { .. }) => "a portfolio margin account",
142 _ => return None,
143 };
144 Some(format!("{account} holds USDC in one balance, so there is nothing to move between perps and spot"))
145 }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
150pub enum Move {
151 ClassTransfer { to_perp: bool },
153 Send,
155 Withdraw,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
161pub enum Draws {
162 Spot,
164 Perp,
166 Pooled,
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, Hash)]
172pub enum Pays {
173 Perps { dex: String },
175 UsdcPerps,
177 Spot,
179 Any,
181}
182
183#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct Balance {
187 pub token: u32,
188 pub total: Dec,
190 pub available: Dec,
192 pub pays: Pays,
193}
194
195#[must_use]
198pub fn payer<'a>(balances: &'a [Balance], surface: Surface<'_>, collateral: &Collateral) -> Option<&'a Balance> {
199 let token = surface.token(collateral);
200 balances.iter().find(|balance| {
201 balance.token == token
202 && match (&balance.pays, surface) {
203 (Pays::Any, _) => true,
204 (Pays::Spot, Surface::Spot { .. } | Surface::Outcomes) => true,
205 (Pays::Perps { dex }, Surface::Perps { dex: wanted }) => dex == wanted,
206 (Pays::UsdcPerps, Surface::Perps { .. }) => token == USDC,
207 _ => false,
208 }
209 })
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Hash)]
214pub enum Source {
215 Perps {
217 dex: String,
218 },
219 NativePerps,
221 Spot,
222 Unified,
223 PortfolioMargin,
224}
225
226impl Source {
227 #[must_use]
228 pub fn words(&self) -> String {
229 match self {
230 Source::Perps { dex } if dex.is_empty() => "perps".to_owned(),
231 Source::Perps { dex } => format!("perps on {dex}"),
232 Source::NativePerps => "perps, the main dex's USDC".to_owned(),
233 Source::Spot => "spot".to_owned(),
234 Source::Unified => "unified, in spot".to_owned(),
235 Source::PortfolioMargin => "portfolio margin".to_owned(),
236 }
237 }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq)]
242pub struct Usable {
243 pub amount: Dec,
244 pub token: u32,
246 pub from: Source,
247 pub borrowable: Dec,
249}
250
251#[derive(Debug, Clone, Default, PartialEq, Eq)]
253pub struct DexState {
254 pub dex: String,
256 pub state: PerpState,
257}
258
259#[derive(Debug, Clone, Default, PartialEq, Eq)]
261pub struct Standing {
262 pub mode: Option<Abstraction>,
264 pub dexes: Vec<DexState>,
265 pub spot: Option<SpotState>,
266 pub heard: Option<u64>,
268}
269
270impl Standing {
271 #[must_use]
272 pub fn dex(&self, dex: &str) -> Option<&PerpState> {
273 self.dexes.iter().find(|state| state.dex == dex).map(|state| &state.state)
274 }
275
276 #[must_use]
277 pub fn keeping(&self) -> Keeping {
278 Keeping::of(self.mode.as_ref(), self.spot.as_ref())
279 }
280
281 #[must_use]
284 pub fn pools(&self) -> bool {
285 self.keeping().pools()
286 }
287
288 #[must_use]
290 pub fn free(&self, token: u32) -> Dec {
291 self.spot.as_ref().map_or(Dec::ZERO, |spot| spot.free(token))
292 }
293
294 #[must_use]
299 pub fn balances(&self, collateral: &Collateral) -> Vec<Balance> {
300 let keeping = self.keeping();
301 let mut out = Vec::new();
302 if !keeping.pools() {
303 for dex in &self.dexes {
304 let token = collateral.of(&dex.dex);
305 let pays = match (keeping, dex.dex.is_empty(), token == USDC) {
306 (Keeping::Native, true, _) => Pays::UsdcPerps,
307 (Keeping::Native, false, true) => continue,
308 _ => Pays::Perps { dex: dex.dex.clone() },
309 };
310 let summary = &dex.state.margin_summary;
311 out.push(Balance { token, total: summary.account_value, available: dex.state.withdrawable, pays });
312 }
313 }
314 let pays = match keeping.pools() {
315 true => Pays::Any,
316 false => Pays::Spot,
317 };
318 let counted = collateral.tokens();
319 let rows = self.spot.iter().flat_map(|spot| &spot.balances).filter(|row| !row.is_outcome());
320 let mut usdc = false;
321 for row in rows {
322 let Some(token) = row.token.filter(|token| counted.contains(token)) else { continue };
323 let available = row.available().unwrap_or(Dec::ZERO);
324 if token != USDC && row.total.is_zero() && available.is_zero() {
325 continue;
326 }
327 usdc |= token == USDC;
328 out.push(Balance { token, total: row.total, available, pays: pays.clone() });
329 }
330 if !usdc {
331 let none = Balance { token: USDC, total: Dec::ZERO, available: Dec::ZERO, pays };
332 let at = out.iter().position(|balance| balance.pays == Pays::Spot || balance.pays == Pays::Any);
333 out.insert(at.unwrap_or(out.len()), none);
334 }
335 out
336 }
337
338 #[must_use]
342 pub fn usable(&self, surface: Surface<'_>, collateral: &Collateral, price: impl Fn(&str) -> Option<Dec>) -> Usable {
343 let keeping = self.keeping();
344 let token = surface.token(collateral);
345 let balances = self.balances(collateral);
346 let paying = payer(&balances, surface, collateral);
347 let from = match (keeping, paying.map(|balance| &balance.pays), surface) {
348 (Keeping::Unified, _, _) => Source::Unified,
349 (Keeping::Portfolio, _, _) => Source::PortfolioMargin,
350 (_, Some(Pays::UsdcPerps), Surface::Perps { dex }) if !dex.is_empty() => Source::NativePerps,
351 (_, _, Surface::Perps { dex }) => Source::Perps { dex: dex.to_owned() },
352 (_, _, Surface::Spot { .. } | Surface::Outcomes) => Source::Spot,
353 };
354 let free = paying.map_or(Dec::ZERO, |balance| balance.available);
355 let borrowable = match (keeping, token == USDC) {
356 (Keeping::Portfolio, true) => self.borrowable(&price),
357 _ => Dec::ZERO,
358 };
359 let amount = free.checked_add(borrowable).unwrap_or(free);
360 Usable { amount, token, from, borrowable }
361 }
362
363 #[must_use]
368 pub fn borrowable(&self, price: &impl Fn(&str) -> Option<Dec>) -> Dec {
369 let Some(spot) = self.spot.as_ref().filter(|_| self.keeping() == Keeping::Portfolio) else {
370 return Dec::ZERO;
371 };
372 if spot.risk() == crate::account::state::Risk::Liquidatable {
373 return Dec::ZERO;
374 }
375 let rows = || spot.balances.iter().filter(|row| !row.is_outcome());
376 let room = rows()
377 .filter(|row| row.token != Some(USDC))
378 .filter_map(|row| {
379 let ltv = row.ltv.or_else(|| {
380 LTV.iter().find(|(coin, _)| *coin == row.coin).map(|(_, hundredths)| Dec::lit::<2>(*hundredths))
381 })?;
382 row.total.checked_mul(price(&row.coin)?)?.checked_mul(ltv).filter(|lent| lent.is_positive())
383 })
384 .fold(Dec::ZERO, |sum, lent| sum.checked_add(lent).unwrap_or(sum));
385 let owed =
386 rows().filter_map(|row| row.borrowed).fold(Dec::ZERO, |sum, owed| sum.checked_add(owed).unwrap_or(sum));
387 let mut out = room.checked_sub(owed).unwrap_or(Dec::ZERO).max(Dec::ZERO);
388 if let Some(after) = spot.available_after_maintenance(USDC) {
389 out = out.min(after.max(Dec::ZERO));
390 }
391 out
392 }
393}
394
395#[derive(Debug, Clone, Default, PartialEq, Eq)]
398pub struct Standings {
399 by: BTreeMap<Address, Standing>,
400 followed: Vec<Address>,
402}
403
404impl Standings {
405 #[must_use]
406 pub fn of(&self, address: Address) -> Option<&Standing> {
407 self.by.get(&address)
408 }
409
410 #[must_use]
411 pub fn followed(&self) -> &[Address] {
412 &self.followed
413 }
414
415 pub fn follow(&mut self, users: &[Address]) -> bool {
418 let mut wanted: Vec<Address> = Vec::new();
419 for user in users {
420 if !wanted.contains(user) && wanted.len() < MOST_USERS {
421 wanted.push(*user);
422 }
423 }
424 if wanted == self.followed {
425 return false;
426 }
427 self.by.retain(|address, _| wanted.contains(address));
428 self.followed = wanted;
429 true
430 }
431
432 pub fn accept(&mut self, user: Address, now: u64, change: impl FnOnce(&mut Standing)) -> bool {
435 if !self.followed.contains(&user) {
436 return false;
437 }
438 let standing = self.by.entry(user).or_default();
439 change(standing);
440 standing.heard = Some(now);
441 true
442 }
443
444 pub fn accept_mode(&mut self, user: Address, mode: Option<Abstraction>, now: u64) {
446 if let Some(mode) = mode {
447 self.accept(user, now, |standing| standing.mode = Some(mode));
448 }
449 }
450}
451
452#[cfg(test)]
453#[path = "tests/usable.rs"]
454mod tests;