Skip to main content

dydx/indexer/
types.rs

1use crate::node::OrderMarketParams;
2use anyhow::{anyhow as err, Error};
3use bigdecimal::BigDecimal;
4use chrono::{DateTime, Utc};
5use cosmrs::{AccountId, Denom as CosmosDenom};
6use derive_more::{Add, Deref, DerefMut, Display, Div, From, Mul, Sub};
7use dydx_proto::dydxprotocol::subaccounts::SubaccountId as ProtoSubaccountId;
8use rand::{rng, Rng};
9use serde::{Deserialize, Deserializer, Serialize};
10use serde_with::{serde_as, DisplayFromStr};
11use std::collections::HashMap;
12use std::convert::TryFrom;
13use std::{fmt, str::FromStr};
14
15// Shared types used by REST API, WS
16
17/// A trader's account with a parent subaccount number.
18#[derive(Deserialize, Debug, Clone, Eq, Hash, PartialOrd, Ord, PartialEq)]
19#[serde(rename_all = "camelCase")]
20#[cfg_attr(any(test, feature = "strict-serde"), serde(deny_unknown_fields))]
21pub struct AccountWithParentSubaccountNumber {
22    /// Address.
23    pub address: Address,
24    /// Parent subaccount number.
25    pub parent_subaccount_number: Option<ParentSubaccountNumber>,
26}
27
28/// A trader's account.
29#[derive(Deserialize, Debug, Clone, Eq, Hash, PartialOrd, Ord, PartialEq)]
30#[serde(rename_all = "camelCase")]
31#[cfg_attr(any(test, feature = "strict-serde"), serde(deny_unknown_fields))]
32pub struct Account {
33    /// Address.
34    pub address: Address,
35    /// Parent subaccount number.
36    pub subaccount_number: Option<SubaccountNumber>,
37}
38
39/// [Address](https://dydx.exchange/crypto-learning/what-is-a-wallet-address).
40#[derive(
41    Default,
42    Serialize,
43    Deserialize,
44    Debug,
45    Clone,
46    From,
47    Display,
48    PartialEq,
49    Eq,
50    PartialOrd,
51    Ord,
52    Hash,
53)]
54pub struct Address(String);
55
56impl FromStr for Address {
57    type Err = Error;
58    fn from_str(value: &str) -> Result<Self, Error> {
59        Ok(Self(
60            value.parse::<AccountId>().map_err(Error::msg)?.to_string(),
61        ))
62    }
63}
64
65impl AsRef<str> for Address {
66    fn as_ref(&self) -> &str {
67        &self.0
68    }
69}
70
71impl From<Address> for String {
72    fn from(address: Address) -> Self {
73        address.0
74    }
75}
76
77/// Order status.
78#[derive(Deserialize, Debug, Clone)]
79#[serde(rename_all = "camelCase", untagged)]
80pub enum ApiOrderStatus {
81    /// Order status.
82    OrderStatus(OrderStatus),
83    /// Best effort.
84    BestEffort(BestEffortOpenedStatus),
85}
86
87/// [Time-in-Force](https://docs.dydx.xyz/types/time_in_force#time-in-force).
88#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
89#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
90pub enum ApiTimeInForce {
91    /// GTT represents Good-Til-Time, where an order will first match with existing orders on the book
92    /// and any remaining size will be added to the book as a maker order, which will expire at a
93    /// given expiry time.
94    Gtt,
95    /// FOK represents Fill-Or-KILl where it's enforced that an order will either be filled
96    /// completely and immediately by maker orders on the book or canceled if the entire amount can't
97    /// be filled.
98    Fok,
99    /// IOC represents Immediate-Or-Cancel, where it's enforced that an order only be matched with
100    /// maker orders on the book. If the order has remaining size after matching with existing orders
101    /// on the book, the remaining size is not placed on the book.
102    Ioc,
103}
104
105/// Asset id.
106#[derive(
107    Serialize, Deserialize, Debug, Clone, From, Display, PartialEq, Eq, PartialOrd, Ord, Hash,
108)]
109pub struct AssetId(pub String);
110
111/// Best-effort opened status.
112#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
113#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
114pub enum BestEffortOpenedStatus {
115    /// Best-effort opened.
116    BestEffortOpened,
117}
118
119/// Candle resolution.
120#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
121pub enum CandleResolution {
122    /// 1-minute.
123    #[serde(rename = "1MIN")]
124    M1,
125    /// 5-minutes.
126    #[serde(rename = "5MINS")]
127    M5,
128    /// 15-minutes.
129    #[serde(rename = "15MINS")]
130    M15,
131    /// 30-minutes.
132    #[serde(rename = "30MINS")]
133    M30,
134    /// 1-hour.
135    #[serde(rename = "1HOUR")]
136    H1,
137    /// 4-hours.
138    #[serde(rename = "4HOURS")]
139    H4,
140    /// 1-day.
141    #[serde(rename = "1DAY")]
142    D1,
143}
144
145/// Representation of an arbitrary ID.
146#[derive(Clone, Debug)]
147pub struct AnyId;
148
149/// Client ID defined by the user to identify orders.
150///
151/// This value should be different for different orders.
152/// To update a specific previously submitted order, the new [`Order`](dydx_proto::dydxprotocol::clob::Order) must have the same client ID, and the same [`OrderId`].
153/// See also: [Replacements](https://docs.dydx.xyz/concepts/trading/limit-orderbook#replacements).
154#[serde_as]
155#[derive(Deserialize, Debug, Clone, Display, PartialEq, Eq, PartialOrd, Ord, Hash)]
156pub struct ClientId(#[serde_as(as = "DisplayFromStr")] pub u32);
157
158impl ClientId {
159    /// Creates a new `ClientId` from a provided `u32`.
160    pub fn new(id: u32) -> Self {
161        ClientId(id)
162    }
163
164    /// Creates a random `ClientId` using the default rand::rng.
165    pub fn random() -> Self {
166        ClientId(rng().random())
167    }
168
169    /// Creates a random `ClientId` using a user-provided RNG.
170    pub fn random_with_rng<R: Rng>(rng: &mut R) -> Self {
171        ClientId(rng.random())
172    }
173}
174
175impl From<u32> for ClientId {
176    fn from(value: u32) -> Self {
177        Self(value)
178    }
179}
180
181impl From<AnyId> for ClientId {
182    fn from(_: AnyId) -> Self {
183        Self::random()
184    }
185}
186
187/// Clob pair id.
188#[serde_as]
189#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
190pub struct ClobPairId(#[serde_as(as = "DisplayFromStr")] pub u32);
191
192impl From<u32> for ClobPairId {
193    fn from(value: u32) -> Self {
194        Self(value)
195    }
196}
197
198impl From<&u32> for ClobPairId {
199    fn from(value: &u32) -> Self {
200        ClobPairId::from(*value)
201    }
202}
203
204/// Client metadata.
205#[serde_as]
206#[derive(Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
207pub struct ClientMetadata(#[serde_as(as = "DisplayFromStr")] pub u32);
208
209impl From<u32> for ClientMetadata {
210    fn from(value: u32) -> Self {
211        Self(value)
212    }
213}
214
215/// Fill id.
216#[derive(Deserialize, Debug, Clone, From, Display, PartialEq, Eq, PartialOrd, Ord, Hash)]
217pub struct FillId(pub String);
218
219/// Fill type.
220#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
221#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
222pub enum FillType {
223    /// LIMIT is the fill type for a fill with a limit taker order.
224    Limit,
225    /// LIQUIDATED is for the taker side of the fill where the subaccount was liquidated.
226    ///
227    /// The subaccountId associated with this fill is the liquidated subaccount.
228    Liquidated,
229    /// LIQUIDATION is for the maker side of the fill, never used for orders.
230    Liquidation,
231    /// DELEVERAGED is for the subaccount that was deleveraged in a deleveraging event.
232    ///
233    /// The fill type will be set to taker.
234    Deleveraged,
235    /// OFFSETTING is for the offsetting subaccount in a deleveraging event.
236    ///
237    /// The fill type will be set to maker.
238    Offsetting,
239}
240
241/// Block height.
242#[serde_as]
243#[derive(
244    Serialize, Deserialize, Debug, Clone, From, Display, PartialEq, Eq, PartialOrd, Ord, Hash,
245)]
246pub struct Height(#[serde_as(as = "DisplayFromStr")] pub u32);
247
248impl Height {
249    /// Get the block which is n blocks ahead.
250    pub fn ahead(&self, n: u32) -> Height {
251        Height(self.0 + n)
252    }
253}
254
255/// Liquidity position.
256///
257/// See also [Market Makers vs Market Takers](https://dydx.exchange/crypto-learning/market-makers-vs-market-takers).
258#[derive(Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
259#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
260pub enum Liquidity {
261    /// [Taker](https://dydx.exchange/crypto-learning/glossary?#taker).
262    Taker,
263    /// [Maker](https://dydx.exchange/crypto-learning/glossary?#maker).
264    Maker,
265}
266
267/// Perpetual market status
268#[derive(Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
269#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
270pub enum PerpetualMarketStatus {
271    /// Active.
272    Active,
273    /// Paused.
274    Paused,
275    /// Cancel-only.
276    CancelOnly,
277    /// Post-only.
278    PostOnly,
279    /// Initializing.
280    Initializing,
281    /// Final settlement.
282    FinalSettlement,
283}
284
285/// Perpetual position status.
286#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
287#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
288pub enum PerpetualPositionStatus {
289    /// Open.
290    Open,
291    /// Closed.
292    Closed,
293    /// Liquidated.
294    Liquidated,
295}
296
297/// Position.
298///
299/// See also [How to Short Crypto: A Beginner’s Guide](https://dydx.exchange/crypto-learning/how-to-short-crypto).
300#[derive(Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
301#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
302pub enum PositionSide {
303    /// Long.
304    Long,
305    /// Short.
306    Short,
307}
308
309/// Market type.
310#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
311#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
312pub enum MarketType {
313    /// [Perpetuals](https://dydx.exchange/crypto-learning/perpetuals-crypto).
314    Perpetual,
315    /// [Spot](https://dydx.exchange/crypto-learning/what-is-spot-trading).
316    Spot,
317}
318
319/// Perpetual market type.
320#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
321#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
322pub enum PerpetualMarketType {
323    /// Cross.
324    Cross,
325    /// [Isolated](https://docs.dydx.xyz/concepts/trading/isolated-markets#isolated-markets).
326    Isolated,
327}
328
329/// Order id.
330#[derive(Deserialize, Debug, Clone, From, Display, PartialEq, Eq, PartialOrd, Ord, Hash)]
331pub struct OrderId(pub String);
332
333/// Order status.
334#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
335#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
336pub enum OrderStatus {
337    /// Opened.
338    Open,
339    /// Filled.
340    Filled,
341    /// Canceled.
342    Canceled,
343    /// Short term cancellations are handled best-effort, meaning they are only gossiped.
344    BestEffortCanceled,
345    /// Untriggered.
346    Untriggered,
347}
348
349/// When the order enters the execution phase
350///
351/// See also [Time in force](https://docs.dydx.xyz/types/time_in_force#time-in-force).
352#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
353#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
354pub enum OrderExecution {
355    /// Leaving order execution as unspecified/empty represents the default behavior
356    /// where an order will first match with existing orders on the book, and any remaining size
357    /// will be added to the book as a maker order.
358    Default,
359    /// IOC represents Immediate-Or-Cancel, where it's enforced that an order only be matched with
360    /// maker orders on the book. If the order has remaining size after matching with existing orders
361    /// on the book, the remaining size is not placed on the book.
362    Ioc,
363    /// FOK represents Fill-Or-KILl where it's enforced that an order will either be filled
364    /// completely and immediately by maker orders on the book or canceled if the entire amount can't
365    /// be filled.
366    Fok,
367    /// Post only enforces that an order only be placed on the book as a maker order.
368    /// Note this means that validators will cancel any newly-placed post only orders that would cross with other maker orders.
369    PostOnly,
370}
371
372/// Order flags.
373#[derive(Clone, Debug, Deserialize)]
374pub enum OrderFlags {
375    /// Short-term order.
376    #[serde(rename = "0")]
377    ShortTerm = 0,
378    /// Conditional order.
379    #[serde(rename = "32")]
380    Conditional = 32,
381    /// Long-term (stateful) order.
382    #[serde(rename = "64")]
383    LongTerm = 64,
384}
385
386// TODO: Consider using 12-bytes array, and deserialize from hex
387/// Trade id.
388#[derive(
389    Serialize, Deserialize, Debug, Clone, From, Display, PartialEq, Eq, PartialOrd, Ord, Hash,
390)]
391pub struct TradeId(pub String);
392
393/// Order side.
394#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
395#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
396pub enum OrderSide {
397    /// Buy.
398    Buy,
399    /// Sell.
400    Sell,
401}
402
403/// Order types.
404///
405/// See also [OrderType](https://docs.dydx.xyz/types/order_type#ordertype).
406#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
407#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
408pub enum OrderType {
409    /// Limit.
410    Limit,
411    /// Market.
412    Market,
413    /// Stop-limit.
414    StopLimit,
415    /// Stop-market.
416    StopMarket,
417    /// Trailing-stop.
418    TrailingStop,
419    /// Take-profit.
420    TakeProfit,
421    /// Take-profit-market.
422    TakeProfitMarket,
423    /// Hard-trade.
424    HardTrade,
425    /// Failed-hard-trade.
426    FailedHardTrade,
427    /// Transfer-placeholder.
428    TransferPlaceholder,
429}
430
431/// Subaccount.
432#[derive(Deserialize, Debug, Clone, Eq, Hash, PartialOrd, Ord, PartialEq)]
433#[serde(rename_all = "camelCase")]
434#[cfg_attr(any(test, feature = "strict-serde"), serde(deny_unknown_fields))]
435pub struct Subaccount {
436    /// Address.
437    pub address: Address,
438    /// Subaccount number.
439    pub number: SubaccountNumber,
440}
441
442impl Subaccount {
443    /// Create a new Subaccount.
444    pub fn new(address: Address, number: SubaccountNumber) -> Self {
445        Self { address, number }
446    }
447
448    /// Get the parent of this Subaccount.
449    pub fn parent(&self) -> ParentSubaccount {
450        let number = ParentSubaccountNumber(self.number.0 % 128);
451        ParentSubaccount::new(self.address.clone(), number)
452    }
453
454    /// Check if this Subaccount is a parent?
455    pub fn is_parent(&self) -> bool {
456        self.number.0 < 128
457    }
458}
459
460impl From<Subaccount> for ProtoSubaccountId {
461    fn from(subacc: Subaccount) -> Self {
462        ProtoSubaccountId {
463            owner: subacc.address.0,
464            number: subacc.number.0,
465        }
466    }
467}
468
469/// Subaccount number.
470#[derive(Serialize, Debug, Clone, Display, PartialEq, Eq, PartialOrd, Ord, Hash)]
471pub struct SubaccountNumber(pub(crate) u32);
472
473impl<'de> Deserialize<'de> for SubaccountNumber {
474    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
475    where
476        D: Deserializer<'de>,
477    {
478        struct SubaccountVisitor;
479
480        impl<'de> serde::de::Visitor<'de> for SubaccountVisitor {
481            type Value = SubaccountNumber;
482
483            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
484                formatter.write_str("a u32 or a string containing a u32")
485            }
486
487            fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
488            where
489                E: serde::de::Error,
490            {
491                Ok(SubaccountNumber(value as u32))
492            }
493
494            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
495            where
496                E: serde::de::Error,
497            {
498                value
499                    .parse::<u32>()
500                    .map(SubaccountNumber)
501                    .map_err(|_| E::custom(format!("invalid u32 in string: {value}")))
502            }
503        }
504
505        deserializer.deserialize_any(SubaccountVisitor)
506    }
507}
508
509impl SubaccountNumber {
510    /// Get the subaccount number value.
511    pub fn value(&self) -> u32 {
512        self.0
513    }
514}
515
516impl TryFrom<u32> for SubaccountNumber {
517    type Error = Error;
518    fn try_from(number: u32) -> Result<Self, Error> {
519        match number {
520            0..=128_000 => Ok(SubaccountNumber(number)),
521            _ => Err(err!("Subaccount number must be [0, 128_000]")),
522        }
523    }
524}
525
526impl TryFrom<&u32> for SubaccountNumber {
527    type Error = Error;
528    fn try_from(number: &u32) -> Result<Self, Error> {
529        Self::try_from(*number)
530    }
531}
532
533impl TryFrom<String> for SubaccountNumber {
534    type Error = Error;
535    fn try_from(number: String) -> Result<Self, Error> {
536        Self::try_from(number.parse::<u32>()?)
537    }
538}
539
540impl TryFrom<&str> for SubaccountNumber {
541    type Error = Error;
542    fn try_from(number: &str) -> Result<Self, Error> {
543        Self::try_from(number.parse::<u32>()?)
544    }
545}
546
547impl From<ParentSubaccountNumber> for SubaccountNumber {
548    fn from(parent: ParentSubaccountNumber) -> Self {
549        Self(parent.value())
550    }
551}
552
553/// Parent subaccount.
554///
555/// A parent subaccount can have multiple positions opened and all posititions are cross-margined.
556/// See also [how isolated positions are handled in dYdX](https://docs.dydx.xyz/concepts/trading/isolated-positions#isolated-positions).
557#[derive(Deserialize, Debug, Clone, Eq, Hash, PartialOrd, Ord, PartialEq)]
558#[serde(rename_all = "camelCase")]
559#[cfg_attr(any(test, feature = "strict-serde"), serde(deny_unknown_fields))]
560pub struct ParentSubaccount {
561    /// Address.
562    pub address: Address,
563    /// Parent subaccount number.
564    pub number: ParentSubaccountNumber,
565}
566
567impl ParentSubaccount {
568    /// Create a new Subaccount.
569    pub fn new(address: Address, number: ParentSubaccountNumber) -> Self {
570        Self { address, number }
571    }
572}
573
574impl std::cmp::PartialEq<Subaccount> for ParentSubaccount {
575    fn eq(&self, other: &Subaccount) -> bool {
576        self.address == other.address && self.number == other.number
577    }
578}
579
580/// Subaccount number.
581#[derive(Serialize, Deserialize, Debug, Clone, Display, PartialEq, Eq, PartialOrd, Ord, Hash)]
582pub struct ParentSubaccountNumber(u32);
583
584impl ParentSubaccountNumber {
585    /// Get parent subaccount number value.
586    pub fn value(&self) -> u32 {
587        self.0
588    }
589}
590
591impl TryFrom<u32> for ParentSubaccountNumber {
592    type Error = Error;
593    fn try_from(number: u32) -> Result<Self, Error> {
594        match number {
595            0..=127 => Ok(ParentSubaccountNumber(number)),
596            _ => Err(err!("Parent subaccount number must be [0, 127]")),
597        }
598    }
599}
600
601impl std::cmp::PartialEq<SubaccountNumber> for ParentSubaccountNumber {
602    fn eq(&self, other: &SubaccountNumber) -> bool {
603        self.0 == other.value()
604    }
605}
606
607/// Subaccount id.
608#[derive(Deserialize, Debug, Clone, From, Display, PartialEq, Eq, PartialOrd, Ord, Hash)]
609pub struct SubaccountId(pub String);
610
611/// Token symbol.
612#[derive(
613    Serialize, Deserialize, Debug, Clone, From, Display, PartialEq, Eq, PartialOrd, Ord, Hash,
614)]
615pub struct Symbol(pub String);
616
617/// Trade type.
618#[derive(Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
619#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
620pub enum TradeType {
621    /// LIMIT is the trade type for a fill with a limit taker order.
622    Limit,
623    /// LIQUIDATED is the trade type for a fill with a liquidated taker order.
624    Liquidated,
625    /// DELEVERAGED is the trade type for a fill with a deleveraged taker order.
626    Deleveraged,
627}
628
629/// Transfer type.
630#[derive(Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
631#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
632pub enum TransferType {
633    /// Transfer-in.
634    TransferIn,
635    /// Transfer-out.
636    TransferOut,
637    /// Deposit.
638    Deposit,
639    /// Withdrawal.
640    Withdrawal,
641}
642
643/// Ticker.
644#[derive(
645    Serialize, Deserialize, Debug, Clone, From, Display, PartialEq, Eq, PartialOrd, Ord, Hash,
646)]
647pub struct Ticker(pub String);
648
649impl<'a> From<&'a str> for Ticker {
650    fn from(value: &'a str) -> Self {
651        Self(value.into())
652    }
653}
654
655const USDC_DENOM: &str = "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5";
656const DYDX_DENOM: &str = "adydx";
657const DYDX_TNT_DENOM: &str = "adv4tnt";
658#[cfg(feature = "noble")]
659const NOBLE_USDC_DENOM: &str = "uusdc";
660
661/// Denom.
662///
663/// A more convenient type for Cosmos' [`Denom`](CosmosDenom).
664#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
665pub enum Denom {
666    /// USDC IBC token.
667    #[serde(rename = "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5")]
668    Usdc,
669    /// dYdX native mainnet token.
670    #[serde(rename = "adydx")]
671    Dydx,
672    /// dYdX native testnet token.
673    #[serde(rename = "adv4tnt")]
674    DydxTnt,
675    /// Noble USDC token.
676    #[cfg(feature = "noble")]
677    #[serde(rename = "uusdc")]
678    NobleUsdc,
679    /// Custom denom representation.
680    #[serde(untagged)]
681    Custom(CosmosDenom),
682}
683
684impl Denom {
685    /// Gas price per atomic unit.
686    /// This price is only available for `Denom`s which can be used to cover transactions gas fees.
687    pub fn gas_price(&self) -> Option<BigDecimal> {
688        match self {
689            // Defined dYdX micro USDC per Gas unit.
690            // As defined in [1](https://docs.dydx.xyz/nodes/running-node/required-node-configs#node-configs) and [2](https://github.com/dydxprotocol/v4-chain/blob/ba731b00e3163f7c3ff553b4300d564c11eaa81f/protocol/cmd/dydxprotocold/cmd/config.go#L15).
691            Self::Usdc => Some(BigDecimal::new(25.into(), 3)),
692            // Defined dYdX native tokens per Gas unit. Recommended to be roughly the same in value as 0.025 micro USDC.
693            // As defined in [1](https://github.com/dydxprotocol/v4-chain/blob/ba731b00e3163f7c3ff553b4300d564c11eaa81f/protocol/cmd/dydxprotocold/cmd/config.go#L21).
694            Self::Dydx | Self::DydxTnt => Some(BigDecimal::new(25_000_000_000u64.into(), 0)),
695            #[cfg(feature = "noble")]
696            Self::NobleUsdc => Some(BigDecimal::new(1.into(), 1)),
697            _ => None,
698        }
699    }
700}
701
702impl FromStr for Denom {
703    type Err = Error;
704    fn from_str(value: &str) -> Result<Self, Error> {
705        match value {
706            USDC_DENOM => Ok(Self::Usdc),
707            DYDX_DENOM => Ok(Self::Dydx),
708            DYDX_TNT_DENOM => Ok(Self::DydxTnt),
709            _ => Ok(Self::Custom(
710                value.parse::<CosmosDenom>().map_err(Error::msg)?,
711            )),
712        }
713    }
714}
715
716impl AsRef<str> for Denom {
717    fn as_ref(&self) -> &str {
718        match self {
719            Self::Usdc => USDC_DENOM,
720            Self::Dydx => DYDX_DENOM,
721            Self::DydxTnt => DYDX_TNT_DENOM,
722            #[cfg(feature = "noble")]
723            Self::NobleUsdc => NOBLE_USDC_DENOM,
724            Self::Custom(denom) => denom.as_ref(),
725        }
726    }
727}
728
729impl fmt::Display for Denom {
730    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
731        f.write_str(self.as_ref())
732    }
733}
734
735impl TryFrom<Denom> for CosmosDenom {
736    type Error = Error;
737    fn try_from(value: Denom) -> Result<Self, Self::Error> {
738        value.as_ref().parse().map_err(Self::Error::msg)
739    }
740}
741
742/// Parent subaccount response.
743#[derive(Deserialize, Debug, Clone)]
744#[serde(rename_all = "camelCase")]
745#[cfg_attr(any(test, feature = "strict-serde"), serde(deny_unknown_fields))]
746pub struct ParentSubaccountResponseObject {
747    /// Address.
748    pub address: Address,
749    /// Subaccount number.
750    pub parent_subaccount_number: SubaccountNumber,
751    /// Equity.
752    pub equity: BigDecimal,
753    /// Free collateral.
754    pub free_collateral: BigDecimal,
755    /// Associated child subaccounts.
756    pub child_subaccounts: Vec<SubaccountResponseObject>,
757}
758
759/// Subaccount response.
760#[derive(Deserialize, Debug, Clone)]
761#[serde(rename_all = "camelCase")]
762#[cfg_attr(any(test, feature = "strict-serde"), serde(deny_unknown_fields))]
763pub struct SubaccountResponseObject {
764    /// Address.
765    pub address: Address,
766    /// Subaccount number.
767    pub subaccount_number: SubaccountNumber,
768    /// Equity.
769    pub equity: BigDecimal,
770    /// Free collateral.
771    pub free_collateral: BigDecimal,
772    /// Opened perpetual positions.
773    pub open_perpetual_positions: PerpetualPositionsMap,
774    /// Asset positions.
775    pub asset_positions: AssetPositionsMap,
776    /// Is margin enabled?
777    pub margin_enabled: bool,
778    /// Updated at height.
779    pub updated_at_height: Height,
780    /// Latest processed block height.
781    pub latest_processed_block_height: Height,
782}
783
784/// Asset position response.
785#[derive(Deserialize, Debug, Clone)]
786#[serde(rename_all = "camelCase")]
787#[cfg_attr(any(test, feature = "strict-serde"), serde(deny_unknown_fields))]
788pub struct AssetPositionResponseObject {
789    /// Token symbol.
790    pub symbol: Symbol,
791    /// Position.
792    pub side: PositionSide,
793    /// Size.
794    pub size: Quantity,
795    /// Subaccount number.
796    pub subaccount_number: SubaccountNumber,
797    /// Asset id.
798    pub asset_id: AssetId,
799}
800
801/// Perpetual position response.
802#[derive(Deserialize, Debug, Clone)]
803#[serde(rename_all = "camelCase")]
804#[cfg_attr(any(test, feature = "strict-serde"), serde(deny_unknown_fields))]
805pub struct PerpetualPositionResponseObject {
806    /// Market ticker.
807    pub market: Ticker,
808    /// Position status.
809    pub status: PerpetualPositionStatus,
810    /// Position.
811    pub side: PositionSide,
812    /// Size.
813    pub size: Quantity,
814    /// Maximum size.
815    pub max_size: Quantity,
816    /// Entry price.
817    pub entry_price: Price,
818    /// Actual PnL.
819    pub realized_pnl: BigDecimal,
820    /// Time(UTC).
821    pub created_at: DateTime<Utc>,
822    /// Block height.
823    pub created_at_height: Height,
824    /// Sum at open.
825    pub sum_open: BigDecimal,
826    /// Sum at close.
827    pub sum_close: BigDecimal,
828    /// Net funding.
829    pub net_funding: BigDecimal,
830    /// Potential PnL.
831    pub unrealized_pnl: BigDecimal,
832    /// Time(UTC).
833    pub closed_at: Option<DateTime<Utc>>,
834    /// Exit price.
835    pub exit_price: Option<Price>,
836    /// Subaccount number.
837    pub subaccount_number: SubaccountNumber,
838}
839
840/// Asset positions.
841pub type AssetPositionsMap = HashMap<Ticker, AssetPositionResponseObject>;
842
843/// Perpetual positions.
844pub type PerpetualPositionsMap = HashMap<Ticker, PerpetualPositionResponseObject>;
845
846/// Price.
847#[derive(
848    Add,
849    Deserialize,
850    Debug,
851    Clone,
852    Div,
853    Display,
854    Deref,
855    DerefMut,
856    PartialEq,
857    Eq,
858    Mul,
859    PartialOrd,
860    Ord,
861    Hash,
862    Sub,
863)]
864#[serde(transparent)]
865pub struct Price(pub BigDecimal);
866
867impl<T> From<T> for Price
868where
869    T: Into<BigDecimal>,
870{
871    fn from(value: T) -> Self {
872        Self(value.into())
873    }
874}
875
876impl FromStr for Price {
877    type Err = bigdecimal::ParseBigDecimalError;
878    fn from_str(s: &str) -> Result<Self, Self::Err> {
879        s.parse().map(Self)
880    }
881}
882
883/// Quantity.
884#[derive(
885    Add,
886    Deserialize,
887    Debug,
888    Clone,
889    Div,
890    Display,
891    Deref,
892    DerefMut,
893    PartialEq,
894    Eq,
895    Mul,
896    PartialOrd,
897    Ord,
898    Hash,
899    Sub,
900)]
901#[serde(transparent)]
902pub struct Quantity(pub BigDecimal);
903
904impl<T> From<T> for Quantity
905where
906    T: Into<BigDecimal>,
907{
908    fn from(value: T) -> Self {
909        Self(value.into())
910    }
911}
912
913impl FromStr for Quantity {
914    type Err = bigdecimal::ParseBigDecimalError;
915    fn from_str(s: &str) -> Result<Self, Self::Err> {
916        s.parse().map(Self)
917    }
918}
919
920/// Orderbook price level.
921#[derive(Deserialize, Debug, Clone)]
922#[serde(rename_all = "camelCase")]
923#[cfg_attr(any(test, feature = "strict-serde"), serde(deny_unknown_fields))]
924pub struct OrderbookResponsePriceLevel {
925    /// Price.
926    pub price: Price,
927    /// Size.
928    pub size: Quantity,
929}
930
931/// Orderbook response.
932#[derive(Deserialize, Debug, Clone)]
933#[serde(rename_all = "camelCase")]
934#[cfg_attr(any(test, feature = "strict-serde"), serde(deny_unknown_fields))]
935pub struct OrderBookResponseObject {
936    /// Bids.
937    pub bids: Vec<OrderbookResponsePriceLevel>,
938    /// Asks.
939    pub asks: Vec<OrderbookResponsePriceLevel>,
940}
941
942/// Order response.
943#[derive(Deserialize, Debug, Clone)]
944#[serde(rename_all = "camelCase")]
945#[cfg_attr(any(test, feature = "strict-serde"), serde(deny_unknown_fields))]
946pub struct OrderResponseObject {
947    /// Client id.
948    pub client_id: ClientId,
949    /// Client metadata.
950    pub client_metadata: ClientMetadata,
951    /// Clob pair id.
952    pub clob_pair_id: ClobPairId,
953    /// Block height.
954    pub created_at_height: Option<Height>,
955    /// Block height.
956    pub good_til_block: Option<Height>,
957    /// Time(UTC).
958    pub good_til_block_time: Option<DateTime<Utc>>,
959    /// Id.
960    pub id: OrderId,
961    /// Order flags.
962    pub order_flags: OrderFlags,
963    /// Post-only.
964    pub post_only: bool,
965    /// Price.
966    pub price: Price,
967    /// Reduce-only.
968    pub reduce_only: bool,
969    /// Side (buy/sell).
970    pub side: OrderSide,
971    /// Size.
972    pub size: Quantity,
973    /// Order status.
974    pub status: ApiOrderStatus,
975    /// Subaccount id.
976    pub subaccount_id: SubaccountId,
977    /// Subaccount number.
978    pub subaccount_number: SubaccountNumber,
979    /// Market ticker.
980    pub ticker: Ticker,
981    /// Time-in-force.
982    pub time_in_force: ApiTimeInForce,
983    /// Total filled.
984    pub total_filled: BigDecimal,
985    /// Order type.
986    #[serde(rename = "type")]
987    pub order_type: OrderType,
988    /// Time(UTC).
989    pub updated_at: Option<DateTime<Utc>>,
990    /// Block height.
991    pub updated_at_height: Option<Height>,
992    /// Trigger price.
993    pub trigger_price: Option<Price>,
994    /// Fee ppm.
995    pub fee_ppm: Option<BigDecimal>,
996    /// Builder address..
997    pub builder_address: Option<Address>,
998    /// Order router address.
999    pub order_router_address: Option<Address>,
1000    /// Duration.
1001    pub duration: Option<BigDecimal>,
1002    /// Interval.
1003    pub interval: Option<BigDecimal>,
1004    /// Price tolerance.
1005    pub price_tolerance: Option<BigDecimal>,
1006}
1007
1008/// Trade response.
1009#[derive(Deserialize, Debug, Clone)]
1010#[serde(rename_all = "camelCase")]
1011#[cfg_attr(any(test, feature = "strict-serde"), serde(deny_unknown_fields))]
1012pub struct TradeResponse {
1013    /// Trades.
1014    pub trades: Vec<TradeResponseObject>,
1015}
1016
1017/// Trade.
1018#[derive(Deserialize, Debug, Clone)]
1019#[serde(rename_all = "camelCase")]
1020#[cfg_attr(any(test, feature = "strict-serde"), serde(deny_unknown_fields))]
1021pub struct TradeResponseObject {
1022    /// Trade id.
1023    pub id: TradeId,
1024    /// Block height.
1025    pub created_at_height: Height,
1026    /// Time(UTC).
1027    pub created_at: DateTime<Utc>,
1028    /// Side (buy/sell).
1029    pub side: OrderSide,
1030    /// Price.
1031    pub price: Price,
1032    /// Size.
1033    pub size: Quantity,
1034    /// Trade type.
1035    #[serde(rename = "type")]
1036    pub trade_type: TradeType,
1037}
1038
1039/// Perpetual markets.
1040#[derive(Deserialize, Debug, Clone)]
1041#[serde(rename_all = "camelCase")]
1042#[cfg_attr(any(test, feature = "strict-serde"), serde(deny_unknown_fields))]
1043pub struct PerpetualMarketResponse {
1044    /// Perpetual markets.
1045    pub markets: HashMap<Ticker, PerpetualMarket>,
1046}
1047
1048/// Perpetual market.
1049#[derive(Deserialize, Debug, Clone)]
1050#[serde(rename_all = "camelCase")]
1051#[cfg_attr(any(test, feature = "strict-serde"), serde(deny_unknown_fields))]
1052pub struct PerpetualMarket {
1053    /// Clob pair id.
1054    pub clob_pair_id: ClobPairId,
1055    /// Market ticker.
1056    pub ticker: Ticker,
1057    /// Market status
1058    pub status: PerpetualMarketStatus,
1059    /// Oracle price.
1060    pub oracle_price: Option<Price>,
1061    /// 24-h price change.
1062    #[serde(rename = "priceChange24H")]
1063    pub price_change_24h: BigDecimal,
1064    /// 24-h volume.
1065    #[serde(rename = "volume24H")]
1066    pub volume_24h: Quantity,
1067    /// 24-h number of trades.
1068    #[serde(rename = "trades24H")]
1069    pub trades_24h: u64,
1070    /// Next funding rate.
1071    pub next_funding_rate: BigDecimal,
1072    /// Initial margin fraction.
1073    pub initial_margin_fraction: BigDecimal,
1074    /// Maintenance margin fraction.
1075    pub maintenance_margin_fraction: BigDecimal,
1076    /// Open interest.
1077    pub open_interest: BigDecimal,
1078    /// Atomic resolution
1079    pub atomic_resolution: i32,
1080    /// Quantum conversion exponent.
1081    pub quantum_conversion_exponent: i32,
1082    /// Tick size.
1083    pub tick_size: BigDecimal,
1084    /// Step size.
1085    pub step_size: BigDecimal,
1086    /// Step base quantums.
1087    pub step_base_quantums: u64,
1088    /// Subticks per tick.
1089    pub subticks_per_tick: u32,
1090    /// Market type.
1091    pub market_type: PerpetualMarketType,
1092    /// Open interest lower capitalization.
1093    pub open_interest_lower_cap: Option<BigDecimal>,
1094    /// Open interest upper capitalization.
1095    pub open_interest_upper_cap: Option<BigDecimal>,
1096    /// Base open interest.
1097    pub base_open_interest: BigDecimal,
1098    /// Default funding rate 1H.
1099    #[serde(rename = "defaultFundingRate1H")]
1100    pub default_funding_rate_1h: Option<BigDecimal>,
1101}
1102
1103impl PerpetualMarket {
1104    /// Creates a [`OrderMarketParams`], capable of performing price and size quantizations and other
1105    /// operations based on market data.
1106    /// These quantizations are required for `Order` placement.
1107    pub fn order_params(&self) -> OrderMarketParams {
1108        OrderMarketParams {
1109            atomic_resolution: self.atomic_resolution,
1110            clob_pair_id: self.clob_pair_id.clone(),
1111            oracle_price: self.oracle_price.clone(),
1112            quantum_conversion_exponent: self.quantum_conversion_exponent,
1113            step_base_quantums: self.step_base_quantums,
1114            subticks_per_tick: self.subticks_per_tick,
1115        }
1116    }
1117}
1118
1119/// Candle response.
1120#[derive(Deserialize, Debug, Clone)]
1121#[serde(rename_all = "camelCase")]
1122#[cfg_attr(any(test, feature = "strict-serde"), serde(deny_unknown_fields))]
1123pub struct CandleResponse {
1124    /// List of candles.
1125    pub candles: Vec<CandleResponseObject>,
1126}
1127
1128/// Candle response.
1129#[derive(Deserialize, Debug, Clone)]
1130#[serde(rename_all = "camelCase")]
1131#[cfg_attr(any(test, feature = "strict-serde"), serde(deny_unknown_fields))]
1132pub struct CandleResponseObject {
1133    /// Time(UTC).
1134    pub started_at: DateTime<Utc>,
1135    /// Market ticker.
1136    pub ticker: Ticker,
1137    /// Candle resolution.
1138    pub resolution: CandleResolution,
1139    /// Low price volume.
1140    pub low: Price,
1141    /// High price volume.
1142    pub high: Price,
1143    /// Token price at open.
1144    pub open: Price,
1145    /// Token price at close.
1146    pub close: Price,
1147    /// Base token volume.
1148    pub base_token_volume: Quantity,
1149    /// USD volume.
1150    pub usd_volume: Quantity,
1151    /// Number of trades.
1152    pub trades: u64,
1153    /// Starting open interest.
1154    pub starting_open_interest: BigDecimal,
1155    /// Orderbook mid price open.
1156    pub orderbook_mid_price_open: Option<Price>,
1157    /// Orderbook mid price close.
1158    pub orderbook_mid_price_close: Option<Price>,
1159}
1160
1161/// Block height parsed by Indexer.
1162#[derive(Deserialize, Debug, Clone)]
1163#[serde(rename_all = "camelCase")]
1164#[cfg_attr(any(test, feature = "strict-serde"), serde(deny_unknown_fields))]
1165pub struct HeightResponse {
1166    /// Block height.
1167    pub height: Height,
1168    /// Time (UTC).
1169    pub time: DateTime<Utc>,
1170}
1171
1172#[cfg(test)]
1173mod tests {
1174    use super::*;
1175
1176    #[test]
1177    fn denom_parse() {
1178        // Test if hardcoded denom is parsed correctly
1179        let _usdc = Denom::Usdc.to_string().parse::<Denom>().unwrap();
1180        let _dydx = Denom::Dydx.to_string().parse::<Denom>().unwrap();
1181        let _dydx_tnt = Denom::DydxTnt.to_string().parse::<Denom>().unwrap();
1182        let _custom: Denom = "uusdc".parse().unwrap();
1183    }
1184}