df_hyperliquid_core/account/
sub_account.rs1use std::fmt;
6
7use crate::account::signers::ActionKind;
8use crate::account::state::{PerpState, SpotState};
9use crate::client::{ApiError, Transport, info, user_request};
10use crate::utils::sign::{L1Action, Map};
11use crate::utils::{Address, AssetKind, BadUnits, Dec, exact_units};
12use serde::Deserialize;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct AccountRef {
18 pub owner: Address,
20 pub sub: Option<Address>,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)]
26pub enum SubRefusal {
27 #[error("a subaccount places no spot orders here")]
28 Spot,
29 #[error("a subaccount places no HIP-4 orders and splits or merges nothing here")]
30 Outcome,
31 #[error("a subaccount's order names its market, since its spot and HIP-4 orders are refused")]
33 MarketUnknown,
34 #[error("a subaccount moves nothing between its own ledgers here")]
35 LedgerMove,
36 #[error("a subaccount approves nothing: its master's agent signs for it")]
37 Approval,
38 #[error("a subaccount schedules no cancel here")]
39 ScheduleCancel,
40 #[error("only the master opens subaccounts and moves money in and out of them")]
43 MastersOwn,
44 #[error("a subaccount has no key of its own to sign this")]
46 NoKey,
47}
48
49impl AccountRef {
50 #[must_use]
51 pub const fn own(owner: Address) -> AccountRef {
52 AccountRef { owner, sub: None }
53 }
54
55 #[must_use]
56 pub const fn sub(owner: Address, sub: Address) -> AccountRef {
57 AccountRef { owner, sub: Some(sub) }
58 }
59
60 #[must_use]
61 pub const fn is_sub(&self) -> bool {
62 self.sub.is_some()
63 }
64
65 #[must_use]
68 pub fn read_at(&self) -> Address {
69 self.sub.unwrap_or(self.owner)
70 }
71
72 #[must_use]
74 pub const fn vault_address(&self) -> Option<Address> {
75 self.sub
76 }
77
78 #[must_use]
80 pub const fn signs_for(&self) -> Address {
81 self.owner
82 }
83
84 #[must_use]
88 pub fn refuses(&self, kind: ActionKind, market: Option<AssetKind>) -> Option<SubRefusal> {
89 self.sub?;
90 match kind {
91 ActionKind::Order | ActionKind::Modify | ActionKind::BatchModify | ActionKind::TwapOrder => match market {
92 Some(AssetKind::Perp | AssetKind::Hip3Perp) => None,
93 Some(AssetKind::Spot) => Some(SubRefusal::Spot),
94 Some(AssetKind::Outcome) => Some(SubRefusal::Outcome),
95 None => Some(SubRefusal::MarketUnknown),
96 },
97 ActionKind::UserOutcome => Some(SubRefusal::Outcome),
98 ActionKind::UsdClassTransfer | ActionKind::AgentSendAsset => Some(SubRefusal::LedgerMove),
99 ActionKind::ApproveAgent | ActionKind::ApproveBuilderFee => Some(SubRefusal::Approval),
100 ActionKind::ScheduleCancel => Some(SubRefusal::ScheduleCancel),
101 ActionKind::CreateSubAccount | ActionKind::SubAccountTransfer | ActionKind::SubAccountSpotTransfer => {
102 Some(SubRefusal::MastersOwn)
103 }
104 ActionKind::UserSetAbstraction => None,
106 user_signed if user_signed.is_user_signed() => Some(SubRefusal::NoKey),
107 _ => None,
108 }
109 }
110}
111
112impl fmt::Display for AccountRef {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 match self.sub {
115 Some(sub) => write!(f, "{sub} (a subaccount of {})", self.owner),
116 None => write!(f, "{}", self.owner),
117 }
118 }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct CreateSubAccount {
124 name: String,
125}
126
127impl CreateSubAccount {
128 #[must_use]
129 pub fn new(name: &str) -> CreateSubAccount {
130 CreateSubAccount { name: name.to_owned() }
131 }
132}
133
134impl L1Action for CreateSubAccount {
135 fn action(&self) -> Map {
136 Map::new().put("type", "createSubAccount").put("name", self.name.clone())
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub struct SubAccountTransfer {
144 sub_account: Address,
145 is_deposit: bool,
146 usd: u64,
147}
148
149impl SubAccountTransfer {
150 pub fn deposit(sub_account: Address, usdc: Dec) -> Result<SubAccountTransfer, BadUnits> {
153 Ok(SubAccountTransfer { sub_account, is_deposit: true, usd: exact_units(usdc, 6)? })
154 }
155
156 pub fn withdraw(sub_account: Address, usdc: Dec) -> Result<SubAccountTransfer, BadUnits> {
158 Ok(SubAccountTransfer { sub_account, is_deposit: false, usd: exact_units(usdc, 6)? })
159 }
160
161 #[must_use]
163 pub const fn from_micros(sub_account: Address, is_deposit: bool, usd: u64) -> SubAccountTransfer {
164 SubAccountTransfer { sub_account, is_deposit, usd }
165 }
166}
167
168impl L1Action for SubAccountTransfer {
169 fn action(&self) -> Map {
170 Map::new()
171 .put("type", "subAccountTransfer")
172 .put("subAccountUser", self.sub_account.to_hex())
173 .put("isDeposit", self.is_deposit)
174 .put("usd", self.usd)
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct SubAccountSpotTransfer {
183 sub_account: Address,
184 is_deposit: bool,
185 token: String,
186 amount: Dec,
187}
188
189impl SubAccountSpotTransfer {
190 pub fn new(
191 sub_account: Address,
192 is_deposit: bool,
193 token: &str,
194 amount: Dec,
195 ) -> Result<SubAccountSpotTransfer, BadUnits> {
196 if !amount.is_positive() {
197 return Err(BadUnits::NotPositive(amount));
198 }
199 Ok(SubAccountSpotTransfer { sub_account, is_deposit, token: token.to_owned(), amount })
200 }
201}
202
203impl L1Action for SubAccountSpotTransfer {
204 fn action(&self) -> Map {
205 Map::new()
206 .put("type", "subAccountSpotTransfer")
207 .put("subAccountUser", self.sub_account.to_hex())
208 .put("isDeposit", self.is_deposit)
209 .put("token", self.token.clone())
210 .put("amount", self.amount.to_wire())
211 }
212}
213
214#[must_use]
216pub fn sub_accounts_request(master: Address) -> String {
217 user_request("subAccounts", master)
218}
219
220pub async fn sub_accounts(transport: &impl Transport, master: Address) -> Result<Vec<SubAccount>, ApiError> {
222 let subs: Option<Vec<SubAccount>> = info(transport, sub_accounts_request(master)).await?;
223 Ok(subs.unwrap_or_default())
224}
225
226#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
229#[serde(rename_all = "camelCase")]
230pub struct SubAccount {
231 #[serde(default)]
232 pub name: String,
233 pub sub_account_user: Address,
234 pub master: Address,
235 #[serde(default)]
236 pub clearinghouse_state: Option<PerpState>,
237 #[serde(default)]
238 pub spot_state: Option<SpotState>,
239}
240
241impl SubAccount {
242 #[must_use]
244 pub const fn account(&self) -> AccountRef {
245 AccountRef::sub(self.master, self.sub_account_user)
246 }
247
248 #[must_use]
250 pub fn equity(&self) -> Dec {
251 self.clearinghouse_state.as_ref().map_or(Dec::ZERO, |state| state.margin_summary.account_value)
252 }
253
254 #[must_use]
255 pub fn notional(&self) -> Dec {
256 self.clearinghouse_state.as_ref().map_or(Dec::ZERO, |state| state.margin_summary.total_ntl_pos)
257 }
258
259 #[must_use]
260 pub fn withdrawable(&self) -> Dec {
261 self.clearinghouse_state.as_ref().map_or(Dec::ZERO, |state| state.withdrawable)
262 }
263
264 #[must_use]
266 pub fn positions(&self) -> usize {
267 self.clearinghouse_state.as_ref().map_or(0, |state| state.positions().count())
268 }
269
270 #[must_use]
272 pub fn spot_usdc(&self) -> Dec {
273 self.spot_state.as_ref().and_then(SpotState::usdc).map_or(Dec::ZERO, |usdc| usdc.total)
274 }
275}
276
277#[cfg(test)]
278#[path = "tests/sub_account.rs"]
279mod tests;