Skip to main content

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