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