use serde::Deserialize;
use crate::account::{Asset, PoolShare, Position};
use crate::client::http::{ApiError, Call, Transport, codes, read};
use crate::earn::pools::operator::PoolStatus;
use crate::utils::ids::whole;
use crate::utils::{AccountIndex, Address, AssetId, COLLATERAL_DECIMALS, Dec, Figure, LIT_DECIMALS, Round};
pub const POOLS_FROM: u64 = (1 << 48) - 1;
pub const MOST_POOLS: u8 = 100;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum PoolFilter {
#[default]
Any,
All,
User,
Protocol,
Account,
Stake,
}
impl PoolFilter {
#[must_use]
pub const fn word(self) -> Option<&'static str> {
match self {
PoolFilter::Any => None,
PoolFilter::All => Some("all"),
PoolFilter::User => Some("user"),
PoolFilter::Protocol => Some("protocol"),
PoolFilter::Account => Some("account_index"),
PoolFilter::Stake => Some("stake"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PoolsPage {
pub filter: PoolFilter,
pub before: u64,
pub limit: u8,
pub holder: Option<AccountIndex>,
}
impl PoolsPage {
#[must_use]
pub const fn first(filter: PoolFilter, limit: u8) -> PoolsPage {
PoolsPage { filter, before: POOLS_FROM, limit, holder: None }
}
#[must_use]
pub const fn after(self, last: AccountIndex) -> PoolsPage {
PoolsPage { before: last.get(), ..self }
}
#[must_use]
pub const fn holder(self, account: AccountIndex) -> PoolsPage {
PoolsPage { holder: Some(account), ..self }
}
}
#[must_use]
pub fn public_pools_call(page: &PoolsPage) -> Call {
let call = Call::get("publicPoolsMetadata")
.maybe("filter", page.filter.word())
.field("index", page.before)
.field("limit", page.limit.clamp(1, MOST_POOLS));
match page.holder {
Some(account) => call.field("account_index", account).authed(),
None => call,
}
}
pub async fn public_pools(transport: &impl Transport, page: &PoolsPage) -> Result<Vec<PublicPool>, ApiError> {
Ok(read::<PublicPools>(transport, public_pools_call(page)).await?.public_pools)
}
#[must_use]
pub fn public_pool_call(pool: AccountIndex, filter: PoolFilter) -> Call {
public_pools_call(&PoolsPage { filter, before: pool.get().saturating_add(1), limit: 1, holder: None })
}
pub async fn public_pool(
transport: &impl Transport,
pool: AccountIndex,
filter: PoolFilter,
) -> Result<Option<PublicPool>, ApiError> {
let pools = read::<PublicPools>(transport, public_pool_call(pool, filter)).await?.public_pools;
Ok(pools.into_iter().find(|listed| listed.pool == pool))
}
#[derive(Deserialize)]
struct PublicPools {
#[serde(default)]
public_pools: Vec<PublicPool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PoolKind {
Protocol,
Operated,
Staking,
Other(u8),
}
impl PoolKind {
#[must_use]
pub const fn from_account_type(code: u8) -> PoolKind {
match code {
2 => PoolKind::Operated,
3 => PoolKind::Protocol,
4 => PoolKind::Staking,
other => PoolKind::Other(other),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct PublicPool {
#[serde(rename = "account_index")]
pub pool: AccountIndex,
pub account_type: u8,
#[serde(default)]
pub name: String,
pub l1_address: Address,
pub master_account_index: AccountIndex,
pub annual_percentage_yield: Figure,
#[serde(default)]
pub sharpe_ratio: Option<Figure>,
pub status: PoolStatus,
pub operator_fee: Dec,
pub total_asset_value: Dec,
#[serde(default)]
pub total_spot_value: Option<Dec>,
#[serde(default)]
pub total_perps_value: Option<Dec>,
#[serde(deserialize_with = "whole")]
pub total_shares: u64,
#[serde(default)]
pub account_share: Option<PoolShare>,
#[serde(default)]
pub assets: Vec<Asset>,
#[serde(default, deserialize_with = "whole")]
pub created_at: u64,
}
impl PublicPool {
#[must_use]
pub const fn kind(&self) -> PoolKind {
PoolKind::from_account_type(self.account_type)
}
#[must_use]
pub fn takes_deposits(&self) -> bool {
self.status == PoolStatus::Active
}
#[must_use]
pub fn holding(&self, asset: AssetId) -> Option<&Asset> {
self.assets.iter().find(|held| held.asset_id == asset)
}
fn collateral(&self) -> Backing {
Backing { value: self.total_asset_value, shares: self.total_shares }
}
fn lit(&self) -> Backing {
let value = self.holding(AssetId::LIT).map_or(Dec::ZERO, |lit| lit.balance);
Backing { value, shares: self.total_shares }
}
#[must_use]
pub fn share_price(&self) -> Option<Dec> {
self.collateral().price()
}
#[must_use]
pub fn shares_for(&self, collateral: Dec) -> Option<u64> {
self.collateral().shares_for(collateral)
}
#[must_use]
pub fn value_of(&self, shares: u64) -> Option<Dec> {
self.collateral().value_of(shares, COLLATERAL_DECIMALS)
}
#[must_use]
pub fn lit_per_share(&self) -> Option<Dec> {
self.lit().price()
}
#[must_use]
pub fn shares_for_lit(&self, lit: Dec) -> Option<u64> {
self.lit().shares_for(lit)
}
#[must_use]
pub fn lit_of(&self, shares: u64) -> Option<Dec> {
self.lit().value_of(shares, LIT_DECIMALS)
}
}
struct Backing {
value: Dec,
shares: u64,
}
impl Backing {
fn count(&self) -> Option<Dec> {
(self.shares > 0).then(|| Dec::new(i128::from(self.shares), 0)).flatten()
}
fn price(&self) -> Option<Dec> {
self.value.checked_div(self.count()?, 18, Round::Down)
}
fn shares_for(&self, amount: Dec) -> Option<u64> {
if !self.value.is_positive() || amount.is_negative() {
return None;
}
let shares = amount.checked_mul(self.count()?)?.checked_div(self.value, 0, Round::Down)?;
u64::try_from(shares.units_at(0, Round::Down)?).ok()
}
fn value_of(&self, shares: u64, places: u32) -> Option<Dec> {
let held = Dec::new(i128::from(shares), 0)?;
held.checked_mul(self.value)?.checked_div(self.count()?, places, Round::Down)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct PoolInfo {
pub status: PoolStatus,
pub operator_fee: Dec,
pub min_operator_share_rate: Dec,
#[serde(deserialize_with = "whole")]
pub total_shares: u64,
#[serde(deserialize_with = "whole")]
pub operator_shares: u64,
pub annual_percentage_yield: Figure,
#[serde(default)]
pub sharpe_ratio: Option<Figure>,
#[serde(default, deserialize_with = "crate::utils::lenient::list")]
pub daily_returns: Vec<DailyReturn>,
#[serde(default, deserialize_with = "crate::utils::lenient::list")]
pub share_prices: Vec<SharePrice>,
#[serde(default, deserialize_with = "crate::utils::lenient::list")]
pub strategies: Vec<Option<Strategy>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct DailyReturn {
#[serde(deserialize_with = "whole")]
pub timestamp: u64,
pub daily_return: Figure,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct SharePrice {
#[serde(deserialize_with = "whole")]
pub timestamp: u64,
pub share_price: Figure,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct Strategy {
pub collateral: Dec,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PoolDetail {
pub pool: AccountIndex,
pub account_type: u8,
pub name: String,
pub description: String,
pub l1_address: Address,
pub total_asset_value: Dec,
pub created_at: u64,
pub info: PoolInfo,
pub positions: Vec<Position>,
pub assets: Vec<Asset>,
}
impl PoolDetail {
#[must_use]
pub const fn kind(&self) -> PoolKind {
PoolKind::from_account_type(self.account_type)
}
#[must_use]
pub fn share_price(&self) -> Option<Dec> {
Backing { value: self.total_asset_value, shares: self.info.total_shares }.price()
}
}
#[derive(Deserialize)]
pub(crate) struct AccountRow {
index: AccountIndex,
#[serde(default)]
account_type: u8,
#[serde(default)]
name: String,
#[serde(default)]
description: String,
l1_address: Address,
total_asset_value: Dec,
#[serde(default, deserialize_with = "whole")]
created_at: u64,
#[serde(default)]
pool_info: Option<PoolInfo>,
#[serde(default)]
positions: Vec<Position>,
#[serde(default)]
assets: Vec<Asset>,
#[serde(default)]
pub(crate) shares: Vec<PoolShare>,
#[serde(default)]
pub(crate) pending_unlocks: Vec<crate::earn::staking::PendingUnlock>,
}
#[derive(Deserialize)]
struct Accounts {
#[serde(default)]
accounts: Vec<AccountRow>,
}
#[must_use]
pub fn pool_call(index: AccountIndex) -> Call {
Call::get("account").field("by", "index").field("value", index)
}
pub use crate::client::http::codes::NOT_FOUND;
pub(crate) async fn account_row(
transport: &impl Transport,
index: AccountIndex,
) -> Result<Option<AccountRow>, ApiError> {
match read::<Accounts>(transport, pool_call(index)).await {
Ok(page) => Ok(page.accounts.into_iter().find(|row| row.index == index)),
Err(error) if matches!(error.code(), Some(NOT_FOUND | codes::ACCOUNT_NOT_FOUND)) => Ok(None),
Err(error) => Err(error),
}
}
pub async fn pool(transport: &impl Transport, pool: AccountIndex) -> Result<Option<PoolDetail>, ApiError> {
let Some(row) = account_row(transport, pool).await? else { return Ok(None) };
Ok(row.pool_info.map(|info| PoolDetail {
pool: row.index,
account_type: row.account_type,
name: row.name,
description: row.description,
l1_address: row.l1_address,
total_asset_value: row.total_asset_value,
created_at: row.created_at,
info,
positions: row.positions,
assets: row.assets,
}))
}
pub async fn pool_shares(transport: &impl Transport, account: AccountIndex) -> Result<Vec<PoolShare>, ApiError> {
Ok(account_row(transport, account).await?.map(|row| row.shares).unwrap_or_default())
}
pub async fn shares_in(transport: &impl Transport, account: AccountIndex, pool: AccountIndex) -> Result<u64, ApiError> {
let shares = pool_shares(transport, account).await?;
Ok(shares.iter().find(|share| share.pool == pool).map_or(0, |share| share.shares))
}
#[cfg(test)]
#[path = "tests/reads.rs"]
mod tests;