Skip to main content

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