use crate::client::{ApiError, Transport, info, info_request, user_request};
use crate::earn::vaults::transfer::USD_DECIMALS;
use crate::utils::{Address, Dec, Figure, Json, Round};
use serde::Deserialize;
#[must_use]
pub fn vault_details_request(vault: Address, user: Option<Address>) -> String {
let mut fields = vec![("vaultAddress", Json::String(vault.to_hex()))];
if let Some(user) = user {
fields.push(("user", Json::String(user.to_hex())));
}
info_request("vaultDetails", &fields)
}
#[must_use]
pub fn user_vault_equities_request(user: Address) -> String {
user_request("userVaultEquities", user)
}
#[must_use]
pub fn leading_vaults_request(user: Address) -> String {
user_request("leadingVaults", user)
}
pub async fn vault_details(
transport: &impl Transport,
vault: Address,
user: Option<Address>,
) -> Result<Option<VaultDetails>, ApiError> {
info(transport, vault_details_request(vault, user)).await
}
pub async fn user_vault_equities(transport: &impl Transport, user: Address) -> Result<Vec<VaultEquity>, ApiError> {
info(transport, user_vault_equities_request(user)).await
}
pub async fn leading_vaults(transport: &impl Transport, user: Address) -> Result<Vec<LeadingVault>, ApiError> {
info(transport, leading_vaults_request(user)).await
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VaultDetails {
pub name: String,
pub vault_address: Address,
pub leader: Address,
#[serde(default)]
pub description: String,
#[serde(default)]
pub apr: Option<Figure>,
#[serde(default)]
pub follower_state: Option<Follower>,
#[serde(default)]
pub leader_fraction: Option<Figure>,
#[serde(default)]
pub leader_commission: Option<Figure>,
#[serde(default)]
pub followers: Vec<Follower>,
#[serde(default)]
pub max_distributable: Option<Figure>,
#[serde(default)]
pub max_withdrawable: Option<Figure>,
#[serde(default)]
pub is_closed: bool,
#[serde(default)]
pub relationship: Relationship,
#[serde(default)]
pub allow_deposits: bool,
#[serde(default)]
pub always_close_on_withdraw: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
#[serde(tag = "type", content = "data", rename_all = "camelCase")]
pub enum Relationship {
#[default]
Normal,
#[serde(rename_all = "camelCase")]
Parent { child_addresses: Vec<Address> },
Child,
}
impl VaultDetails {
#[must_use]
pub const fn takes_deposits(&self) -> bool {
self.allow_deposits && !self.is_closed
}
#[must_use]
pub fn withdrawable_now(&self, now_ms: u64) -> Dec {
match &self.follower_state {
Some(follower) if !follower.is_locked(now_ms) => self
.max_withdrawable
.as_ref()
.and_then(Figure::to_dec)
.map_or(Dec::ZERO, |most| most.round(USD_DECIMALS, Round::Down).max(Dec::ZERO)),
_ => Dec::ZERO,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Follower {
#[serde(deserialize_with = "follower_user")]
pub user: Option<Address>,
pub vault_equity: Dec,
#[serde(default)]
pub pnl: Option<Dec>,
#[serde(default)]
pub all_time_pnl: Option<Dec>,
#[serde(default)]
pub days_following: Option<u64>,
#[serde(default)]
pub vault_entry_time: Option<u64>,
#[serde(default)]
pub lockup_until: Option<u64>,
}
impl Follower {
#[must_use]
pub fn is_locked(&self, now_ms: u64) -> bool {
self.lockup_until.is_some_and(|until| now_ms < until)
}
}
fn follower_user<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Option<Address>, D::Error> {
let text = String::deserialize(deserializer)?;
if text == "Leader" {
return Ok(None);
}
text.parse().map(Some).map_err(serde::de::Error::custom)
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VaultEquity {
pub vault_address: Address,
pub equity: Dec,
pub locked_until_timestamp: u64,
}
impl VaultEquity {
#[must_use]
pub const fn is_locked(&self, now_ms: u64) -> bool {
now_ms < self.locked_until_timestamp
}
#[must_use]
pub fn withdrawable(&self, now_ms: u64) -> Dec {
match self.is_locked(now_ms) {
true => Dec::ZERO,
false => self.equity.round(USD_DECIMALS, Round::Down).max(Dec::ZERO),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct LeadingVault {
pub address: Address,
#[serde(default)]
pub name: String,
}
#[cfg(test)]
#[path = "tests/vault.rs"]
mod tests;
#[cfg(test)]
#[path = "tests/details.rs"]
mod recorded;