df_hyperliquid_core/earn/vaults/
reads.rs1use crate::client::{ApiError, Transport, info, info_request, user_request};
7use crate::utils::{Address, Dec, Figure, Json, Round, USDC_DECIMALS};
8use serde::Deserialize;
9
10#[must_use]
13pub fn vault_details_request(vault: Address, user: Option<Address>) -> String {
14 let mut fields = vec![("vaultAddress", Json::String(vault.to_hex()))];
15 if let Some(user) = user {
16 fields.push(("user", Json::String(user.to_hex())));
17 }
18 info_request("vaultDetails", &fields)
19}
20
21#[must_use]
22pub fn user_vault_equities_request(user: Address) -> String {
23 user_request("userVaultEquities", user)
24}
25
26#[must_use]
27pub fn leading_vaults_request(user: Address) -> String {
28 user_request("leadingVaults", user)
29}
30
31pub async fn vault_details(
33 transport: &impl Transport,
34 vault: Address,
35 user: Option<Address>,
36) -> Result<Option<VaultDetails>, ApiError> {
37 info(transport, vault_details_request(vault, user)).await
38}
39
40pub async fn user_vault_equities(transport: &impl Transport, user: Address) -> Result<Vec<VaultEquity>, ApiError> {
42 info(transport, user_vault_equities_request(user)).await
43}
44
45pub async fn leading_vaults(transport: &impl Transport, user: Address) -> Result<Vec<LeadingVault>, ApiError> {
47 info(transport, leading_vaults_request(user)).await
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct VaultDetails {
53 pub name: String,
54 pub vault_address: Address,
55 pub leader: Address,
56 #[serde(default)]
57 pub description: String,
58 #[serde(default)]
60 pub apr: Option<Figure>,
61 #[serde(default)]
63 pub follower_state: Option<Follower>,
64 #[serde(default)]
66 pub leader_fraction: Option<Figure>,
67 #[serde(default)]
69 pub leader_commission: Option<Figure>,
70 #[serde(default)]
71 pub followers: Vec<Follower>,
72 #[serde(default)]
73 pub max_distributable: Option<Figure>,
74 #[serde(default)]
76 pub max_withdrawable: Option<Figure>,
77 #[serde(default)]
78 pub is_closed: bool,
79 #[serde(default)]
81 pub relationship: Relationship,
82 #[serde(default)]
83 pub allow_deposits: bool,
84 #[serde(default)]
85 pub always_close_on_withdraw: bool,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
91#[serde(tag = "type", content = "data", rename_all = "camelCase")]
92pub enum Relationship {
93 #[default]
95 Normal,
96 #[serde(rename_all = "camelCase")]
98 Parent { child_addresses: Vec<Address> },
99 Child,
101}
102
103impl VaultDetails {
104 #[must_use]
106 pub const fn takes_deposits(&self) -> bool {
107 self.allow_deposits && !self.is_closed
108 }
109
110 #[must_use]
114 pub fn withdrawable_now(&self, now_ms: u64) -> Dec {
115 match &self.follower_state {
116 Some(follower) if !follower.is_locked(now_ms) => self
117 .max_withdrawable
118 .as_ref()
119 .and_then(Figure::to_dec)
120 .map_or(Dec::ZERO, |most| most.round(USDC_DECIMALS, Round::Down).max(Dec::ZERO)),
121 _ => Dec::ZERO,
122 }
123 }
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
127#[serde(rename_all = "camelCase")]
128pub struct Follower {
129 #[serde(deserialize_with = "follower_user")]
131 pub user: Option<Address>,
132 pub vault_equity: Dec,
133 #[serde(default)]
134 pub pnl: Option<Dec>,
135 #[serde(default)]
136 pub all_time_pnl: Option<Dec>,
137 #[serde(default)]
138 pub days_following: Option<u64>,
139 #[serde(default)]
140 pub vault_entry_time: Option<u64>,
141 #[serde(default)]
143 pub lockup_until: Option<u64>,
144}
145
146impl Follower {
147 #[must_use]
149 pub fn is_locked(&self, now_ms: u64) -> bool {
150 self.lockup_until.is_some_and(|until| now_ms < until)
151 }
152}
153
154fn follower_user<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Option<Address>, D::Error> {
156 let text = String::deserialize(deserializer)?;
157 if text == "Leader" {
158 return Ok(None);
159 }
160 text.parse().map(Some).map_err(serde::de::Error::custom)
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct VaultEquity {
166 pub vault_address: Address,
167 pub equity: Dec,
168 pub locked_until_timestamp: u64,
170}
171
172impl VaultEquity {
173 #[must_use]
174 pub const fn is_locked(&self, now_ms: u64) -> bool {
175 now_ms < self.locked_until_timestamp
176 }
177
178 #[must_use]
181 pub fn withdrawable(&self, now_ms: u64) -> Dec {
182 match self.is_locked(now_ms) {
183 true => Dec::ZERO,
184 false => self.equity.round(USDC_DECIMALS, Round::Down).max(Dec::ZERO),
185 }
186 }
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
190pub struct LeadingVault {
191 pub address: Address,
192 #[serde(default)]
193 pub name: String,
194}
195
196#[cfg(test)]
197#[path = "tests/details.rs"]
198mod tests;