Skip to main content

df_hyperliquid_core/account/settings/
mode.rs

1//! An account's mode: how its balances are kept, as `userAbstraction` answers, the read, and
2//! changing it with `userSetAbstraction` (the account's own wallet) or `agentSetAbstraction` (an
3//! agent, for its master). Unified back to standard is refused.
4
5use std::fmt;
6
7use serde::{Deserialize, Deserializer, de};
8
9use crate::client::{ApiError, Transport, info, user_request};
10use crate::utils::Address;
11use crate::utils::sign::{Field, L1Action, Map, NonceKey, UserAction, UserSigned};
12
13/// `{"type":"userAbstraction","user":..}`.
14#[must_use]
15pub fn abstraction_request(user: Address) -> String {
16    user_request("userAbstraction", user)
17}
18
19/// Weighs 20.
20pub async fn abstraction(transport: &impl Transport, user: Address) -> Result<Abstraction, ApiError> {
21    info(transport, abstraction_request(user)).await
22}
23
24/// How an account's balances are kept, as `userAbstraction` answers.
25#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26pub enum Abstraction {
27    /// What a new account answers, never set. Standard: perp and spot apart.
28    Default,
29    /// Standard, set: each perp dex and spot keep their own.
30    Disabled,
31    /// One balance per asset, in spot.
32    UnifiedAccount,
33    /// Unified, and an order may borrow against HYPE and BTC.
34    PortfolioMargin,
35    /// Discontinued: USDC sits with the main dex's perps, and every USDC dex
36    /// draws on it; everything else as standard.
37    DexAbstraction,
38    /// A mode this build does not know, kept as spelled. Treated as standard.
39    Unknown(String),
40}
41
42impl Abstraction {
43    #[must_use]
44    pub fn parse(text: &str) -> Abstraction {
45        match text {
46            "default" => Abstraction::Default,
47            "disabled" => Abstraction::Disabled,
48            "unifiedAccount" => Abstraction::UnifiedAccount,
49            "portfolioMargin" => Abstraction::PortfolioMargin,
50            "dexAbstraction" => Abstraction::DexAbstraction,
51            other => Abstraction::Unknown(other.to_owned()),
52        }
53    }
54
55    #[must_use]
56    pub fn wire(&self) -> &str {
57        match self {
58            Abstraction::Default => "default",
59            Abstraction::Disabled => "disabled",
60            Abstraction::UnifiedAccount => "unifiedAccount",
61            Abstraction::PortfolioMargin => "portfolioMargin",
62            Abstraction::DexAbstraction => "dexAbstraction",
63            Abstraction::Unknown(other) => other,
64        }
65    }
66
67    /// One balance, in spot: a unified or portfolio-margin account, which also
68    /// refuses `spotSend`.
69    #[must_use]
70    pub const fn is_pooled(&self) -> bool {
71        matches!(self, Abstraction::UnifiedAccount | Abstraction::PortfolioMargin)
72    }
73
74    /// Separate ledgers: each perp dex and spot keep their own.
75    #[must_use]
76    pub const fn is_standard(&self) -> bool {
77        !self.is_pooled()
78    }
79}
80
81impl fmt::Display for Abstraction {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        f.write_str(self.wire())
84    }
85}
86
87/// From the bare JSON string the venue answers.
88impl<'de> Deserialize<'de> for Abstraction {
89    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
90        struct Visitor;
91
92        impl de::Visitor<'_> for Visitor {
93            type Value = Abstraction;
94
95            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96                f.write_str("an account mode as a string")
97            }
98
99            fn visit_str<E: de::Error>(self, text: &str) -> Result<Abstraction, E> {
100                Ok(Abstraction::parse(text))
101            }
102        }
103
104        deserializer.deserialize_str(Visitor)
105    }
106}
107
108/// A mode an account can be set to.
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110pub enum AbstractionMode {
111    /// Standard: perp and spot apart.
112    Disabled,
113    /// One balance, in spot.
114    UnifiedAccount,
115    PortfolioMargin,
116}
117
118impl AbstractionMode {
119    pub const ALL: [AbstractionMode; 3] =
120        [AbstractionMode::Disabled, AbstractionMode::UnifiedAccount, AbstractionMode::PortfolioMargin];
121
122    /// As `userSetAbstraction` and `userAbstraction` spell it.
123    #[must_use]
124    pub const fn wire(self) -> &'static str {
125        match self {
126            AbstractionMode::Disabled => "disabled",
127            AbstractionMode::UnifiedAccount => "unifiedAccount",
128            AbstractionMode::PortfolioMargin => "portfolioMargin",
129        }
130    }
131
132    /// As `agentSetAbstraction` spells it.
133    #[must_use]
134    pub const fn code(self) -> &'static str {
135        match self {
136            AbstractionMode::Disabled => "i",
137            AbstractionMode::UnifiedAccount => "u",
138            AbstractionMode::PortfolioMargin => "p",
139        }
140    }
141
142    /// From `userSetAbstraction`'s spelling. `default`, a mode never set, is
143    /// none an account can be set to.
144    #[must_use]
145    pub fn from_wire(text: &str) -> Option<AbstractionMode> {
146        AbstractionMode::ALL.into_iter().find(|mode| mode.wire() == text)
147    }
148
149    /// From `agentSetAbstraction`'s one letter.
150    #[must_use]
151    pub fn from_code(text: &str) -> Option<AbstractionMode> {
152        AbstractionMode::ALL.into_iter().find(|mode| mode.code() == text)
153    }
154
155    /// Whether the venue refuses moving an account in `current` to this
156    /// mode: a unified account back to standard.
157    #[must_use]
158    pub const fn is_refused_from(self, current: &Abstraction) -> bool {
159        matches!((current, self), (Abstraction::UnifiedAccount, AbstractionMode::Disabled))
160    }
161}
162
163/// `userSetAbstraction`, signed by the account's own wallet. `user` is the
164/// account whose mode changes. Unified back to standard is refused.
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub struct UserSetAbstraction {
167    user: Address,
168    mode: AbstractionMode,
169    nonce: u64,
170}
171
172impl UserSetAbstraction {
173    #[must_use]
174    pub const fn new(user: Address, mode: AbstractionMode, nonce: u64) -> UserSetAbstraction {
175        UserSetAbstraction { user, mode, nonce }
176    }
177}
178
179impl UserAction for UserSetAbstraction {
180    fn user_signed(&self) -> UserSigned<'_> {
181        UserSigned::new("userSetAbstraction", "HyperliquidTransaction:UserSetAbstraction", NonceKey::Nonce, self.nonce)
182            .field("user", Field::Address(self.user))
183            .field("abstraction", Field::String(self.mode.wire()))
184    }
185}
186
187/// `agentSetAbstraction`: `{type, abstraction}` with the one-letter code, an
188/// L1 action an agent signs for its master. Unified back to standard is
189/// refused.
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191pub struct AgentSetAbstraction {
192    mode: AbstractionMode,
193}
194
195impl AgentSetAbstraction {
196    #[must_use]
197    pub const fn new(mode: AbstractionMode) -> AgentSetAbstraction {
198        AgentSetAbstraction { mode }
199    }
200}
201
202impl L1Action for AgentSetAbstraction {
203    fn action(&self) -> Map {
204        Map::new().put("type", "agentSetAbstraction").put("abstraction", self.mode.code())
205    }
206}
207
208#[cfg(test)]
209#[path = "tests/mode.rs"]
210mod tests;