Skip to main content

df_hyperliquid_core/transfer/
actions.rs

1//! The actions that move money, keys in the `@nktkas/hyperliquid` 0.33.2 order, amounts in normal
2//! form, addresses lowercase. The user-signed ones are signed by the account's own wallet only;
3//! [`AgentSendAsset`], an L1 action, by an agent for its master.
4
5use crate::account::signers::ActionKind;
6use crate::client::Network;
7use crate::transfer::amount::Amount;
8use crate::transfer::keeping::Keeping;
9use crate::transfer::plan::Key;
10use crate::utils::Address;
11use crate::utils::sign::{Field, L1Action, Map, NonceKey, UserAction, UserSigned};
12
13/// A balance as `sendAsset` names it: spot.
14pub const SPOT_DEX: &str = "spot";
15/// The main perp dex's balance, which `sendAsset` names by the empty string.
16pub const PERP_DEX: &str = "";
17
18/// `sendAsset`: any token between any two balances of any two accounts, and the one transfer a
19/// pooled account takes.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct SendAsset {
22    destination: String,
23    source_dex: String,
24    destination_dex: String,
25    token: String,
26    amount: Amount,
27    from_sub_account: String,
28    nonce: u64,
29}
30
31impl SendAsset {
32    /// User-signed: only the account's own key.
33    pub const KEY: Key = ActionKind::SendAsset.signers();
34
35    /// `token` is `NAME:0x<tokenId>`, as `spotMeta` lists it.
36    #[must_use]
37    pub fn new(
38        destination: Address,
39        source_dex: &str,
40        destination_dex: &str,
41        token: &str,
42        amount: Amount,
43        nonce: u64,
44    ) -> SendAsset {
45        SendAsset {
46            destination: destination.to_hex(),
47            source_dex: source_dex.to_owned(),
48            destination_dex: destination_dex.to_owned(),
49            token: token.to_owned(),
50            amount,
51            from_sub_account: String::new(),
52            nonce,
53        }
54    }
55
56    /// USDC from one balance to another: to another account from spot (or,
57    /// on a standard account, from the perp balance), or to the account itself
58    /// to combine its two balances. USDC's token id is the network's own.
59    #[must_use]
60    pub fn usdc(
61        network: Network,
62        destination: Address,
63        source_dex: &str,
64        destination_dex: &str,
65        amount: Amount,
66        nonce: u64,
67    ) -> SendAsset {
68        SendAsset::new(destination, source_dex, destination_dex, network.usdc().wire, amount, nonce)
69    }
70
71    /// Sent from one of the account's subaccounts rather than the account.
72    #[must_use]
73    pub fn from_sub_account(mut self, sub_account: Address) -> SendAsset {
74        self.from_sub_account = sub_account.to_hex();
75        self
76    }
77
78    #[must_use]
79    pub const fn amount(&self) -> &Amount {
80        &self.amount
81    }
82}
83
84impl UserAction for SendAsset {
85    fn user_signed(&self) -> UserSigned<'_> {
86        UserSigned::new("sendAsset", "HyperliquidTransaction:SendAsset", NonceKey::Nonce, self.nonce)
87            .field("destination", Field::String(&self.destination))
88            .field("sourceDex", Field::String(&self.source_dex))
89            .field("destinationDex", Field::String(&self.destination_dex))
90            .field("token", Field::String(&self.token))
91            .field("amount", Field::String(self.amount.as_str()))
92            .field("fromSubAccount", Field::String(&self.from_sub_account))
93    }
94}
95
96/// `spotSend`: a spot token to another account. A pooled account refuses it.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct SpotSend {
99    destination: String,
100    token: String,
101    amount: Amount,
102    time: u64,
103}
104
105impl SpotSend {
106    /// User-signed: only the account's own key.
107    pub const KEY: Key = ActionKind::SpotSend.signers();
108
109    #[must_use]
110    pub fn new(destination: Address, token: &str, amount: Amount, time: u64) -> SpotSend {
111        SpotSend { destination: destination.to_hex(), token: token.to_owned(), amount, time }
112    }
113}
114
115impl UserAction for SpotSend {
116    fn user_signed(&self) -> UserSigned<'_> {
117        UserSigned::new("spotSend", "HyperliquidTransaction:SpotSend", NonceKey::Time, self.time)
118            .field("destination", Field::String(&self.destination))
119            .field("token", Field::String(&self.token))
120            .field("amount", Field::String(self.amount.as_str()))
121    }
122}
123
124/// `usdSend`: USDC out of the perp balance to another account.
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct UsdSend {
127    destination: String,
128    amount: Amount,
129    time: u64,
130}
131
132impl UsdSend {
133    /// User-signed: only the account's own key.
134    pub const KEY: Key = ActionKind::UsdSend.signers();
135
136    #[must_use]
137    pub fn new(destination: Address, amount: Amount, time: u64) -> UsdSend {
138        UsdSend { destination: destination.to_hex(), amount, time }
139    }
140}
141
142impl UserAction for UsdSend {
143    fn user_signed(&self) -> UserSigned<'_> {
144        UserSigned::new("usdSend", "HyperliquidTransaction:UsdSend", NonceKey::Time, self.time)
145            .field("destination", Field::String(&self.destination))
146            .field("amount", Field::String(self.amount.as_str()))
147    }
148}
149
150/// `usdClassTransfer`: USDC between the account's own spot and perp balances.
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct UsdClassTransfer {
153    amount: Amount,
154    to_perp: bool,
155    nonce: u64,
156}
157
158impl UsdClassTransfer {
159    /// User-signed: only the account's own key.
160    pub const KEY: Key = ActionKind::UsdClassTransfer.signers();
161
162    #[must_use]
163    pub const fn new(amount: Amount, to_perp: bool, nonce: u64) -> UsdClassTransfer {
164        UsdClassTransfer { amount, to_perp, nonce }
165    }
166}
167
168impl UserAction for UsdClassTransfer {
169    fn user_signed(&self) -> UserSigned<'_> {
170        UserSigned::new("usdClassTransfer", "HyperliquidTransaction:UsdClassTransfer", NonceKey::Nonce, self.nonce)
171            .field("amount", Field::String(self.amount.as_str()))
172            .field("toPerp", Field::Bool(self.to_perp))
173    }
174}
175
176/// `withdraw3`: USDC out of the perp balance to an address on Arbitrum. The venue takes its fee
177/// (1 USDC in live `withdraw` rows) out of the amount.
178#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct Withdraw3 {
180    destination: String,
181    amount: Amount,
182    time: u64,
183}
184
185impl Withdraw3 {
186    /// User-signed: only the account's own key.
187    pub const KEY: Key = ActionKind::Withdraw3.signers();
188
189    #[must_use]
190    pub fn new(destination: Address, amount: Amount, time: u64) -> Withdraw3 {
191        Withdraw3 { destination: destination.to_hex(), amount, time }
192    }
193
194    #[must_use]
195    pub const fn amount(&self) -> &Amount {
196        &self.amount
197    }
198}
199
200impl UserAction for Withdraw3 {
201    fn user_signed(&self) -> UserSigned<'_> {
202        UserSigned::new("withdraw3", "HyperliquidTransaction:Withdraw", NonceKey::Time, self.time)
203            .field("destination", Field::String(&self.destination))
204            .field("amount", Field::String(self.amount.as_str()))
205    }
206}
207
208/// A spot token out of the account: `spotSend` for a separate account, and `sendAsset` from spot
209/// to spot for a pooled one (which refuses `spotSend`) or one whose mode is not known (both kinds
210/// take it). Either goes to a person, or through a system address to HyperEVM.
211#[derive(Debug, Clone, PartialEq, Eq)]
212pub enum SpotOut {
213    SpotSend(SpotSend),
214    SendAsset(SendAsset),
215}
216
217impl SpotOut {
218    /// User-signed: only the account's own key.
219    pub const KEY: Key = SpotSend::KEY;
220
221    /// `token` is `NAME:0x<tokenId>`.
222    #[must_use]
223    pub fn new(destination: Address, token: &str, amount: Amount, keeping: Keeping, nonce: u64) -> SpotOut {
224        match keeping {
225            Keeping::Apart => SpotOut::SpotSend(SpotSend::new(destination, token, amount, nonce)),
226            Keeping::Pooled | Keeping::Unknown => {
227                SpotOut::SendAsset(SendAsset::new(destination, SPOT_DEX, SPOT_DEX, token, amount, nonce))
228            }
229        }
230    }
231}
232
233impl UserAction for SpotOut {
234    fn user_signed(&self) -> UserSigned<'_> {
235        match self {
236            SpotOut::SpotSend(send) => send.user_signed(),
237            SpotOut::SendAsset(send) => send.user_signed(),
238        }
239    }
240}
241
242/// `agentSendAsset`: `sendAsset`'s fields as an L1 action, which an agent signs for its master.
243/// The destination is the account itself, so it only moves between the account's own balances.
244/// The action carries its nonce and is signed with that same nonce.
245#[derive(Debug, Clone, PartialEq, Eq)]
246pub struct AgentSendAsset {
247    account: Address,
248    source_dex: String,
249    destination_dex: String,
250    token: String,
251    amount: Amount,
252    nonce: u64,
253}
254
255impl AgentSendAsset {
256    /// L1: an agent the account approved, or the account's own key.
257    pub const KEY: Key = ActionKind::AgentSendAsset.signers();
258
259    /// From `source_dex` to `destination_dex` of `account`, which is also the
260    /// destination. `token` is `NAME:0x<tokenId>`.
261    #[must_use]
262    pub fn new(
263        account: Address,
264        source_dex: &str,
265        destination_dex: &str,
266        token: &str,
267        amount: Amount,
268        nonce: u64,
269    ) -> AgentSendAsset {
270        AgentSendAsset {
271            account,
272            source_dex: source_dex.to_owned(),
273            destination_dex: destination_dex.to_owned(),
274            token: token.to_owned(),
275            amount,
276            nonce,
277        }
278    }
279
280    /// The nonce inside the action, which is also the one it is signed with.
281    #[must_use]
282    pub const fn nonce(&self) -> u64 {
283        self.nonce
284    }
285
286    #[must_use]
287    pub const fn amount(&self) -> &Amount {
288        &self.amount
289    }
290}
291
292impl L1Action for AgentSendAsset {
293    fn action(&self) -> Map {
294        Map::new()
295            .put("type", "agentSendAsset")
296            .put("destination", self.account.to_hex())
297            .put("sourceDex", self.source_dex.clone())
298            .put("destinationDex", self.destination_dex.clone())
299            .put("token", self.token.clone())
300            .put("amount", self.amount.as_str().to_owned())
301            .put("fromSubAccount", "")
302            .put("nonce", self.nonce)
303    }
304}
305
306#[cfg(test)]
307#[path = "tests/actions.rs"]
308mod tests;