Skip to main content

hyperliquid_sdk/
types.rs

1//! Core types for the Hyperliquid SDK.
2//!
3//! These types mirror the Hyperliquid API exactly for byte-identical serialization.
4
5use alloy::primitives::{Address, B128, U256};
6use alloy::sol_types::{eip712_domain, Eip712Domain};
7use either::Either;
8use rust_decimal::Decimal;
9use serde::{Deserialize, Serialize};
10use std::fmt;
11use std::str::FromStr;
12
13// ══════════════════════════════════════════════════════════════════════════════
14// Type Aliases
15// ══════════════════════════════════════════════════════════════════════════════
16
17/// Client Order ID - 128-bit unique identifier
18pub type Cloid = B128;
19
20/// Either an order ID (u64) or a client order ID (Cloid)
21pub type OidOrCloid = Either<u64, Cloid>;
22
23// ══════════════════════════════════════════════════════════════════════════════
24// Chain
25// ══════════════════════════════════════════════════════════════════════════════
26
27/// Hyperliquid chain (Mainnet or Testnet)
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
29#[serde(rename_all = "PascalCase")]
30pub enum Chain {
31    #[default]
32    Mainnet,
33    Testnet,
34}
35
36impl Chain {
37    /// Returns true if this is mainnet
38    pub fn is_mainnet(&self) -> bool {
39        matches!(self, Chain::Mainnet)
40    }
41
42    /// Returns the chain as a string ("Mainnet" or "Testnet")
43    pub fn as_str(&self) -> &'static str {
44        match self {
45            Chain::Mainnet => "Mainnet",
46            Chain::Testnet => "Testnet",
47        }
48    }
49
50    /// Returns the signature chain ID for EIP-712 signing
51    pub fn signature_chain_id(&self) -> &'static str {
52        match self {
53            Chain::Mainnet => "0xa4b1", // Arbitrum One
54            Chain::Testnet => "0x66eee", // Arbitrum Sepolia
55        }
56    }
57
58    /// Returns the EVM chain ID
59    pub fn evm_chain_id(&self) -> u64 {
60        match self {
61            Chain::Mainnet => 999,
62            Chain::Testnet => 998,
63        }
64    }
65}
66
67impl fmt::Display for Chain {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        match self {
70            Chain::Mainnet => write!(f, "Mainnet"),
71            Chain::Testnet => write!(f, "Testnet"),
72        }
73    }
74}
75
76// ══════════════════════════════════════════════════════════════════════════════
77// Side
78// ══════════════════════════════════════════════════════════════════════════════
79
80/// Order side
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "lowercase")]
83pub enum Side {
84    Buy,
85    Sell,
86}
87
88impl Side {
89    /// Returns true if this is a buy side
90    pub fn is_buy(&self) -> bool {
91        matches!(self, Side::Buy)
92    }
93
94    /// Converts to bool for API (true = buy, false = sell)
95    pub fn as_bool(&self) -> bool {
96        self.is_buy()
97    }
98}
99
100impl fmt::Display for Side {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        match self {
103            Side::Buy => write!(f, "buy"),
104            Side::Sell => write!(f, "sell"),
105        }
106    }
107}
108
109// ══════════════════════════════════════════════════════════════════════════════
110// HIP-4 Prediction Markets
111// ══════════════════════════════════════════════════════════════════════════════
112
113/// A tradeable HIP-4 outcome side.
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115#[serde(rename_all = "camelCase")]
116pub struct PredictionSide {
117    pub outcome: u64,
118    pub side: usize,
119    pub name: String,
120    pub symbol: String,
121    pub token: String,
122    pub asset_id: usize,
123    pub mid: Option<String>,
124    pub sz_decimals: u8,
125    pub supports_priority_fee: bool,
126}
127
128impl fmt::Display for PredictionSide {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        write!(f, "{}", self.symbol)
131    }
132}
133
134impl From<PredictionSide> for String {
135    fn from(side: PredictionSide) -> Self {
136        side.symbol
137    }
138}
139
140impl From<&PredictionSide> for String {
141    fn from(side: &PredictionSide) -> Self {
142        side.symbol.clone()
143    }
144}
145
146/// A HIP-4 prediction market with yes/no tradeable sides.
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148#[serde(rename_all = "camelCase")]
149pub struct PredictionMarket {
150    pub outcome: u64,
151    pub name: String,
152    pub description: String,
153    pub title: String,
154    pub slug: String,
155    pub underlying: Option<String>,
156    pub target_price: Option<String>,
157    pub expiry: Option<String>,
158    pub period: Option<String>,
159    pub collateral: String,
160    pub min_order_value: String,
161    pub aliases: Vec<String>,
162    pub yes: PredictionSide,
163    pub no: PredictionSide,
164    pub sides: Vec<PredictionSide>,
165}
166
167impl PredictionMarket {
168    pub fn matches(&self, query: &str) -> bool {
169        let normalized = query.to_lowercase();
170        let mut values = vec![
171            self.slug.clone(),
172            self.title.to_lowercase(),
173            self.name.to_lowercase(),
174            self.underlying.clone().unwrap_or_default().to_lowercase(),
175            self.yes.symbol.to_lowercase(),
176            self.no.symbol.to_lowercase(),
177            self.yes.token.to_lowercase(),
178            self.no.token.to_lowercase(),
179        ];
180        values.extend(self.aliases.iter().map(|alias| alias.to_lowercase()));
181        values.iter().any(|value| value == &normalized || value.contains(&normalized))
182    }
183}
184
185/// Filter for selecting an active HIP-4 prediction market.
186#[derive(Debug, Clone, Default)]
187pub struct PredictionMarketFilter {
188    pub query: Option<String>,
189    pub underlying: Option<String>,
190    pub target_price: Option<String>,
191    pub expiry: Option<String>,
192}
193
194impl FromStr for Side {
195    type Err = String;
196
197    fn from_str(s: &str) -> Result<Self, Self::Err> {
198        match s.to_lowercase().as_str() {
199            "buy" | "b" | "long" => Ok(Side::Buy),
200            "sell" | "s" | "short" => Ok(Side::Sell),
201            _ => Err(format!("invalid side: {}", s)),
202        }
203    }
204}
205
206// ══════════════════════════════════════════════════════════════════════════════
207// Time In Force (TIF)
208// ══════════════════════════════════════════════════════════════════════════════
209
210/// Time in force for orders
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
212#[serde(rename_all = "lowercase")]
213pub enum TIF {
214    /// Immediate or Cancel - fill immediately or cancel
215    #[default]
216    Ioc,
217    /// Good Till Cancel - stays on book until filled or cancelled
218    Gtc,
219    /// Add Liquidity Only (post-only) - rejected if would cross
220    Alo,
221    /// Market order (converted to IOC with slippage)
222    Market,
223}
224
225impl fmt::Display for TIF {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        match self {
228            TIF::Ioc => write!(f, "ioc"),
229            TIF::Gtc => write!(f, "gtc"),
230            TIF::Alo => write!(f, "alo"),
231            TIF::Market => write!(f, "market"),
232        }
233    }
234}
235
236impl FromStr for TIF {
237    type Err = String;
238
239    fn from_str(s: &str) -> Result<Self, Self::Err> {
240        match s.to_lowercase().as_str() {
241            "ioc" => Ok(TIF::Ioc),
242            "gtc" => Ok(TIF::Gtc),
243            "alo" | "post_only" => Ok(TIF::Alo),
244            "market" => Ok(TIF::Market),
245            _ => Err(format!("invalid tif: {}", s)),
246        }
247    }
248}
249
250// ══════════════════════════════════════════════════════════════════════════════
251// TimeInForce (API format)
252// ══════════════════════════════════════════════════════════════════════════════
253
254/// Time in force for the wire format (PascalCase)
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
256pub enum TimeInForce {
257    Alo,
258    Ioc,
259    Gtc,
260    FrontendMarket,
261}
262
263impl From<TIF> for TimeInForce {
264    fn from(tif: TIF) -> Self {
265        match tif {
266            TIF::Ioc => TimeInForce::Ioc,
267            TIF::Gtc => TimeInForce::Gtc,
268            TIF::Alo => TimeInForce::Alo,
269            TIF::Market => TimeInForce::Ioc, // Market orders use IOC with slippage
270        }
271    }
272}
273
274// ══════════════════════════════════════════════════════════════════════════════
275// TpSl (Take Profit / Stop Loss)
276// ══════════════════════════════════════════════════════════════════════════════
277
278/// Take profit or stop loss trigger type
279#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
280#[serde(rename_all = "lowercase")]
281pub enum TpSl {
282    /// Take profit
283    Tp,
284    /// Stop loss
285    Sl,
286}
287
288impl fmt::Display for TpSl {
289    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290        match self {
291            TpSl::Tp => write!(f, "tp"),
292            TpSl::Sl => write!(f, "sl"),
293        }
294    }
295}
296
297// ══════════════════════════════════════════════════════════════════════════════
298// Order Grouping
299// ══════════════════════════════════════════════════════════════════════════════
300
301/// Order grouping for TP/SL attachment
302#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
303#[serde(rename_all = "camelCase")]
304pub enum OrderGrouping {
305    /// No grouping
306    #[default]
307    Na,
308    /// Normal TP/SL grouping
309    NormalTpsl,
310    /// Position-based TP/SL grouping
311    PositionTpsl,
312}
313
314impl fmt::Display for OrderGrouping {
315    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
316        match self {
317            OrderGrouping::Na => write!(f, "na"),
318            OrderGrouping::NormalTpsl => write!(f, "normalTpsl"),
319            OrderGrouping::PositionTpsl => write!(f, "positionTpsl"),
320        }
321    }
322}
323
324// ══════════════════════════════════════════════════════════════════════════════
325// EIP-712 Domain
326// ══════════════════════════════════════════════════════════════════════════════
327
328/// EIP-712 domain for Hyperliquid signing
329pub const CORE_MAINNET_EIP712_DOMAIN: Eip712Domain = eip712_domain! {
330    name: "Exchange",
331    version: "1",
332    chain_id: 1337,
333    verifying_contract: Address::ZERO,
334};
335
336// ══════════════════════════════════════════════════════════════════════════════
337// Signature
338// ══════════════════════════════════════════════════════════════════════════════
339
340/// ECDSA signature (r, s, v format)
341#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
342pub struct Signature {
343    #[serde(
344        serialize_with = "serialize_u256_hex",
345        deserialize_with = "deserialize_u256_hex"
346    )]
347    pub r: U256,
348    #[serde(
349        serialize_with = "serialize_u256_hex",
350        deserialize_with = "deserialize_u256_hex"
351    )]
352    pub s: U256,
353    pub v: u64,
354}
355
356impl From<alloy::signers::Signature> for Signature {
357    fn from(sig: alloy::signers::Signature) -> Self {
358        Self {
359            r: sig.r(),
360            s: sig.s(),
361            v: if sig.v() { 28 } else { 27 },
362        }
363    }
364}
365
366// ══════════════════════════════════════════════════════════════════════════════
367// Order Type Placement
368// ══════════════════════════════════════════════════════════════════════════════
369
370/// Order type for placement (limit or trigger)
371#[derive(Debug, Clone, Serialize, Deserialize)]
372#[serde(rename_all = "camelCase")]
373pub enum OrderTypePlacement {
374    /// Limit order
375    Limit {
376        tif: TimeInForce,
377    },
378    /// Trigger order (stop loss / take profit)
379    #[serde(rename_all = "camelCase")]
380    Trigger {
381        is_market: bool,
382        #[serde(with = "decimal_normalized")]
383        trigger_px: Decimal,
384        tpsl: TpSl,
385    },
386}
387
388// ══════════════════════════════════════════════════════════════════════════════
389// Order Request
390// ══════════════════════════════════════════════════════════════════════════════
391
392/// Order request for the API
393#[derive(Debug, Clone, Serialize, Deserialize)]
394#[serde(rename_all = "camelCase")]
395pub struct OrderRequest {
396    /// Asset index
397    #[serde(rename = "a")]
398    pub asset: usize,
399    /// Is buy (true) or sell (false)
400    #[serde(rename = "b")]
401    pub is_buy: bool,
402    /// Limit price
403    #[serde(rename = "p", with = "decimal_normalized")]
404    pub limit_px: Decimal,
405    /// Size
406    #[serde(rename = "s", with = "decimal_normalized")]
407    pub sz: Decimal,
408    /// Reduce only
409    #[serde(rename = "r")]
410    pub reduce_only: bool,
411    /// Order type
412    #[serde(rename = "t")]
413    pub order_type: OrderTypePlacement,
414    /// Client order ID
415    #[serde(
416        rename = "c",
417        serialize_with = "serialize_cloid_hex",
418        deserialize_with = "deserialize_cloid_hex"
419    )]
420    pub cloid: Cloid,
421}
422
423// ══════════════════════════════════════════════════════════════════════════════
424// Batch Order
425// ══════════════════════════════════════════════════════════════════════════════
426
427/// Batch of orders
428#[derive(Debug, Clone, Serialize, Deserialize)]
429#[serde(rename_all = "camelCase")]
430pub struct BatchOrder {
431    pub orders: Vec<OrderRequest>,
432    pub grouping: OrderGrouping,
433}
434
435// ══════════════════════════════════════════════════════════════════════════════
436// Modify
437// ══════════════════════════════════════════════════════════════════════════════
438
439/// Order modification
440#[derive(Debug, Clone, Serialize, Deserialize)]
441#[serde(rename_all = "camelCase")]
442pub struct Modify {
443    #[serde(with = "oid_or_cloid")]
444    pub oid: OidOrCloid,
445    pub order: OrderRequest,
446}
447
448/// Batch modification
449#[derive(Debug, Clone, Serialize, Deserialize)]
450#[serde(rename_all = "camelCase")]
451pub struct BatchModify {
452    pub modifies: Vec<Modify>,
453}
454
455// ══════════════════════════════════════════════════════════════════════════════
456// Cancel
457// ══════════════════════════════════════════════════════════════════════════════
458
459/// Cancel request
460#[derive(Debug, Clone, Serialize, Deserialize)]
461#[serde(rename_all = "camelCase")]
462pub struct Cancel {
463    #[serde(rename = "a")]
464    pub asset: usize,
465    #[serde(rename = "o")]
466    pub oid: u64,
467}
468
469/// Batch cancel
470#[derive(Debug, Clone, Serialize, Deserialize)]
471#[serde(rename_all = "camelCase")]
472pub struct BatchCancel {
473    pub cancels: Vec<Cancel>,
474}
475
476/// Cancel by client order ID
477#[derive(Debug, Clone, Serialize, Deserialize)]
478#[serde(rename_all = "camelCase")]
479pub struct CancelByCloid {
480    pub asset: u32,
481    #[serde(with = "const_hex_b128")]
482    pub cloid: B128,
483}
484
485/// Batch cancel by client order ID
486#[derive(Debug, Clone, Serialize, Deserialize)]
487#[serde(rename_all = "camelCase")]
488pub struct BatchCancelCloid {
489    pub cancels: Vec<CancelByCloid>,
490}
491
492/// Schedule cancel (dead-man's switch)
493#[derive(Debug, Clone, Serialize, Deserialize)]
494#[serde(rename_all = "camelCase")]
495pub struct ScheduleCancel {
496    pub time: Option<u64>,
497}
498
499// ══════════════════════════════════════════════════════════════════════════════
500// TWAP Orders
501// ══════════════════════════════════════════════════════════════════════════════
502
503/// TWAP order specification
504#[derive(Debug, Clone, Serialize, Deserialize)]
505#[serde(rename_all = "camelCase")]
506pub struct TwapSpec {
507    #[serde(rename = "a")]
508    pub asset: String,
509    #[serde(rename = "b")]
510    pub is_buy: bool,
511    #[serde(rename = "s")]
512    pub sz: String,
513    #[serde(rename = "r")]
514    pub reduce_only: bool,
515    #[serde(rename = "m")]
516    pub duration_minutes: i64,
517    #[serde(rename = "t")]
518    pub randomize: bool,
519}
520
521/// TWAP order
522#[derive(Debug, Clone, Serialize, Deserialize)]
523#[serde(rename_all = "camelCase")]
524pub struct TwapOrder {
525    pub twap: TwapSpec,
526}
527
528/// TWAP cancel
529#[derive(Debug, Clone, Serialize, Deserialize)]
530#[serde(rename_all = "camelCase")]
531pub struct TwapCancel {
532    #[serde(rename = "a")]
533    pub asset: String,
534    #[serde(rename = "t")]
535    pub twap_id: i64,
536}
537
538// ══════════════════════════════════════════════════════════════════════════════
539// Leverage Management
540// ══════════════════════════════════════════════════════════════════════════════
541
542/// Update leverage
543#[derive(Debug, Clone, Serialize, Deserialize)]
544#[serde(rename_all = "camelCase")]
545pub struct UpdateLeverage {
546    pub asset: u32,
547    pub is_cross: bool,
548    pub leverage: i32,
549}
550
551/// Update isolated margin
552#[derive(Debug, Clone, Serialize, Deserialize)]
553#[serde(rename_all = "camelCase")]
554pub struct UpdateIsolatedMargin {
555    pub asset: u32,
556    pub is_buy: bool,
557    pub ntli: i64,
558}
559
560/// Top up isolated only margin
561#[derive(Debug, Clone, Serialize, Deserialize)]
562#[serde(rename_all = "camelCase")]
563pub struct TopUpIsolatedOnlyMargin {
564    pub asset: u32,
565    pub leverage: String,
566}
567
568// ══════════════════════════════════════════════════════════════════════════════
569// Transfer Operations
570// ══════════════════════════════════════════════════════════════════════════════
571
572/// USD transfer
573#[derive(Debug, Clone, Serialize, Deserialize)]
574#[serde(rename_all = "camelCase")]
575pub struct UsdSend {
576    pub hyperliquid_chain: Chain,
577    pub signature_chain_id: String,
578    pub destination: String,
579    pub amount: String,
580    pub time: u64,
581}
582
583/// Spot token transfer
584#[derive(Debug, Clone, Serialize, Deserialize)]
585#[serde(rename_all = "camelCase")]
586pub struct SpotSend {
587    pub hyperliquid_chain: Chain,
588    pub signature_chain_id: String,
589    pub token: String,
590    pub destination: String,
591    pub amount: String,
592    pub time: u64,
593}
594
595/// Withdraw to Arbitrum
596#[derive(Debug, Clone, Serialize, Deserialize)]
597#[serde(rename_all = "camelCase")]
598pub struct Withdraw3 {
599    pub hyperliquid_chain: Chain,
600    pub signature_chain_id: String,
601    pub destination: String,
602    pub amount: String,
603    pub time: u64,
604}
605
606/// USD class transfer (perp <-> spot)
607#[derive(Debug, Clone, Serialize, Deserialize)]
608#[serde(rename_all = "camelCase")]
609pub struct UsdClassTransfer {
610    pub hyperliquid_chain: Chain,
611    pub signature_chain_id: String,
612    pub amount: String,
613    pub to_perp: bool,
614    pub nonce: u64,
615}
616
617/// Send asset
618#[derive(Debug, Clone, Serialize, Deserialize)]
619#[serde(rename_all = "camelCase")]
620pub struct SendAsset {
621    pub hyperliquid_chain: Chain,
622    pub signature_chain_id: String,
623    pub destination: String,
624    pub source_dex: String,
625    pub destination_dex: String,
626    pub token: String,
627    pub amount: String,
628    pub from_sub_account: String,
629    pub nonce: u64,
630}
631
632// ══════════════════════════════════════════════════════════════════════════════
633// Vault Operations
634// ══════════════════════════════════════════════════════════════════════════════
635
636/// Vault transfer (deposit/withdraw)
637#[derive(Debug, Clone, Serialize, Deserialize)]
638#[serde(rename_all = "camelCase")]
639pub struct VaultTransfer {
640    pub vault_address: String,
641    pub is_deposit: bool,
642    pub usd: f64,
643}
644
645// ══════════════════════════════════════════════════════════════════════════════
646// Agent/API Key Management
647// ══════════════════════════════════════════════════════════════════════════════
648
649/// Approve agent (API key)
650#[derive(Debug, Clone, Serialize, Deserialize)]
651#[serde(rename_all = "camelCase")]
652pub struct ApproveAgent {
653    pub hyperliquid_chain: Chain,
654    pub signature_chain_id: String,
655    pub agent_address: String,
656    pub agent_name: Option<String>,
657    pub nonce: u64,
658}
659
660/// Approve builder fee
661#[derive(Debug, Clone, Serialize, Deserialize)]
662#[serde(rename_all = "camelCase")]
663pub struct ApproveBuilderFee {
664    pub hyperliquid_chain: Chain,
665    pub signature_chain_id: String,
666    pub max_fee_rate: String,
667    pub builder: String,
668    pub nonce: u64,
669}
670
671// ══════════════════════════════════════════════════════════════════════════════
672// Account Abstraction
673// ══════════════════════════════════════════════════════════════════════════════
674
675/// User set abstraction
676#[derive(Debug, Clone, Serialize, Deserialize)]
677#[serde(rename_all = "camelCase")]
678pub struct UserSetAbstraction {
679    pub hyperliquid_chain: Chain,
680    pub signature_chain_id: String,
681    pub user: String,
682    pub abstraction: String,
683    pub nonce: u64,
684}
685
686/// Agent set abstraction
687#[derive(Debug, Clone, Serialize, Deserialize)]
688#[serde(rename_all = "camelCase")]
689pub struct AgentSetAbstraction {
690    pub abstraction: String,
691}
692
693// ══════════════════════════════════════════════════════════════════════════════
694// Staking Operations
695// ══════════════════════════════════════════════════════════════════════════════
696
697/// Stake (cDeposit)
698#[derive(Debug, Clone, Serialize, Deserialize)]
699#[serde(rename_all = "camelCase")]
700pub struct CDeposit {
701    pub hyperliquid_chain: Chain,
702    pub signature_chain_id: String,
703    pub wei: u128,
704    pub nonce: u64,
705}
706
707/// Unstake (cWithdraw)
708#[derive(Debug, Clone, Serialize, Deserialize)]
709#[serde(rename_all = "camelCase")]
710pub struct CWithdraw {
711    pub hyperliquid_chain: Chain,
712    pub signature_chain_id: String,
713    pub wei: u128,
714    pub nonce: u64,
715}
716
717/// Delegate tokens
718#[derive(Debug, Clone, Serialize, Deserialize)]
719#[serde(rename_all = "camelCase")]
720pub struct TokenDelegate {
721    pub hyperliquid_chain: Chain,
722    pub signature_chain_id: String,
723    pub validator: String,
724    pub is_undelegate: bool,
725    pub wei: u128,
726    pub nonce: u64,
727}
728
729// ══════════════════════════════════════════════════════════════════════════════
730// Misc Operations
731// ══════════════════════════════════════════════════════════════════════════════
732
733/// Reserve request weight (purchase rate limit capacity)
734#[derive(Debug, Clone, Serialize, Deserialize)]
735#[serde(rename_all = "camelCase")]
736pub struct ReserveRequestWeight {
737    pub weight: i32,
738}
739
740/// No-op (consume nonce)
741#[derive(Debug, Clone, Serialize, Deserialize)]
742#[serde(rename_all = "camelCase")]
743pub struct Noop {}
744
745/// Validator L1 stream (vote on risk-free rate)
746#[derive(Debug, Clone, Serialize, Deserialize)]
747#[serde(rename_all = "camelCase")]
748pub struct ValidatorL1Stream {
749    pub risk_free_rate: String,
750}
751
752/// Close position
753#[derive(Debug, Clone, Serialize, Deserialize)]
754#[serde(rename_all = "camelCase")]
755pub struct ClosePosition {
756    pub asset: String,
757    pub user: String,
758}
759
760// ══════════════════════════════════════════════════════════════════════════════
761// Action (all possible actions)
762// ══════════════════════════════════════════════════════════════════════════════
763
764/// All possible actions that can be sent to the exchange
765#[derive(Debug, Clone, Serialize, Deserialize)]
766#[serde(tag = "type")]
767#[serde(rename_all = "camelCase")]
768pub enum Action {
769    // Trading actions (require builder fees)
770    Order(BatchOrder),
771    BatchModify(BatchModify),
772
773    // Cancel actions (no builder fees)
774    Cancel(BatchCancel),
775    CancelByCloid(BatchCancelCloid),
776    ScheduleCancel(ScheduleCancel),
777
778    // TWAP orders
779    TwapOrder(TwapOrder),
780    TwapCancel(TwapCancel),
781
782    // Leverage management
783    UpdateLeverage(UpdateLeverage),
784    UpdateIsolatedMargin(UpdateIsolatedMargin),
785    TopUpIsolatedOnlyMargin(TopUpIsolatedOnlyMargin),
786
787    // Transfer operations
788    UsdSend(UsdSend),
789    SpotSend(SpotSend),
790    Withdraw3(Withdraw3),
791    UsdClassTransfer(UsdClassTransfer),
792    SendAsset(SendAsset),
793
794    // Vault operations
795    VaultTransfer(VaultTransfer),
796
797    // Agent/API key management
798    ApproveAgent(ApproveAgent),
799    ApproveBuilderFee(ApproveBuilderFee),
800
801    // Account abstraction
802    UserSetAbstraction(UserSetAbstraction),
803    AgentSetAbstraction(AgentSetAbstraction),
804
805    // Staking operations
806    CDeposit(CDeposit),
807    CWithdraw(CWithdraw),
808    TokenDelegate(TokenDelegate),
809
810    // Rate limiting
811    ReserveRequestWeight(ReserveRequestWeight),
812
813    // Noop
814    Noop(Noop),
815
816    // Validator operations
817    ValidatorL1Stream(ValidatorL1Stream),
818
819    // Close position
820    ClosePosition(ClosePosition),
821}
822
823impl Action {
824    /// Compute the MessagePack hash of this action for signing
825    pub fn hash(
826        &self,
827        nonce: u64,
828        vault_address: Option<Address>,
829        expires_after: Option<u64>,
830    ) -> Result<alloy::primitives::B256, rmp_serde::encode::Error> {
831        crate::signing::rmp_hash(self, nonce, vault_address, expires_after)
832    }
833}
834
835// ══════════════════════════════════════════════════════════════════════════════
836// Action Request
837// ══════════════════════════════════════════════════════════════════════════════
838
839/// Signed action request
840#[derive(Debug, Clone, Serialize, Deserialize)]
841#[serde(rename_all = "camelCase")]
842pub struct ActionRequest {
843    pub action: Action,
844    pub nonce: u64,
845    pub signature: Signature,
846    #[serde(skip_serializing_if = "Option::is_none")]
847    pub vault_address: Option<Address>,
848    #[serde(skip_serializing_if = "Option::is_none")]
849    pub expires_after: Option<u64>,
850}
851
852// ══════════════════════════════════════════════════════════════════════════════
853// Builder
854// ══════════════════════════════════════════════════════════════════════════════
855
856/// Builder fee information
857#[derive(Debug, Clone, Serialize, Deserialize)]
858pub struct Builder {
859    /// Builder address
860    #[serde(rename = "b")]
861    pub address: String,
862    /// Fee in tenths of basis points (40 = 0.04%)
863    #[serde(rename = "f")]
864    pub fee: u16,
865}
866
867// ══════════════════════════════════════════════════════════════════════════════
868// Serde Helpers
869// ══════════════════════════════════════════════════════════════════════════════
870
871/// Normalized decimal serialization (removes trailing zeros)
872pub mod decimal_normalized {
873    use rust_decimal::Decimal;
874    use serde::{de, Deserialize, Deserializer, Serializer};
875    use std::str::FromStr;
876
877    pub fn serialize<S>(value: &Decimal, serializer: S) -> Result<S::Ok, S::Error>
878    where
879        S: Serializer,
880    {
881        let normalized = value.normalize();
882        serializer.serialize_str(&normalized.to_string())
883    }
884
885    pub fn deserialize<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
886    where
887        D: Deserializer<'de>,
888    {
889        let s = String::deserialize(deserializer)?;
890        Decimal::from_str(&s)
891            .map(|d| d.normalize())
892            .map_err(de::Error::custom)
893    }
894}
895
896/// Serde module for OidOrCloid
897pub mod oid_or_cloid {
898    use super::Cloid;
899    use either::Either;
900    use serde::{de, Deserializer, Serializer};
901
902    pub fn serialize<S>(value: &Either<u64, Cloid>, serializer: S) -> Result<S::Ok, S::Error>
903    where
904        S: Serializer,
905    {
906        match value {
907            Either::Left(oid) => serializer.serialize_u64(*oid),
908            Either::Right(cloid) => serializer.serialize_str(&format!("{:#x}", cloid)),
909        }
910    }
911
912    pub fn deserialize<'de, D>(deserializer: D) -> Result<Either<u64, Cloid>, D::Error>
913    where
914        D: Deserializer<'de>,
915    {
916        struct Visitor;
917
918        impl<'de> serde::de::Visitor<'de> for Visitor {
919            type Value = Either<u64, Cloid>;
920
921            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
922                f.write_str("a u64 oid or a hex string cloid")
923            }
924
925            fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
926                Ok(Either::Left(v))
927            }
928
929            fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
930                v.parse::<Cloid>().map(Either::Right).map_err(de::Error::custom)
931            }
932        }
933
934        deserializer.deserialize_any(Visitor)
935    }
936}
937
938/// B128 hex serialization
939pub mod const_hex_b128 {
940    use alloy::primitives::B128;
941    use serde::{Deserialize, Deserializer, Serializer};
942
943    pub fn serialize<S>(value: &B128, serializer: S) -> Result<S::Ok, S::Error>
944    where
945        S: Serializer,
946    {
947        serializer.serialize_str(&format!("{:#x}", value))
948    }
949
950    pub fn deserialize<'de, D>(deserializer: D) -> Result<B128, D::Error>
951    where
952        D: Deserializer<'de>,
953    {
954        let s = String::deserialize(deserializer)?;
955        s.parse::<B128>().map_err(serde::de::Error::custom)
956    }
957}
958
959fn serialize_cloid_hex<S>(value: &Cloid, serializer: S) -> Result<S::Ok, S::Error>
960where
961    S: serde::Serializer,
962{
963    serializer.serialize_str(&format!("{:#x}", value))
964}
965
966fn deserialize_cloid_hex<'de, D>(deserializer: D) -> Result<Cloid, D::Error>
967where
968    D: serde::Deserializer<'de>,
969{
970    let s = String::deserialize(deserializer)?;
971    s.parse::<Cloid>().map_err(serde::de::Error::custom)
972}
973
974fn serialize_u256_hex<S>(value: &U256, serializer: S) -> Result<S::Ok, S::Error>
975where
976    S: serde::Serializer,
977{
978    serializer.serialize_str(&format!("{:#x}", value))
979}
980
981fn deserialize_u256_hex<'de, D>(deserializer: D) -> Result<U256, D::Error>
982where
983    D: serde::Deserializer<'de>,
984{
985    let s = String::deserialize(deserializer)?;
986    let s = s.strip_prefix("0x").unwrap_or(&s);
987    U256::from_str_radix(s, 16).map_err(serde::de::Error::custom)
988}