Skip to main content

drift_rs/
types.rs

1use std::{
2    cell::{BorrowError, BorrowMutError},
3    cmp::Ordering,
4    str::FromStr,
5};
6
7use dashmap::DashMap;
8pub use solana_rpc_client_api::config::RpcSendTransactionConfig;
9pub use solana_sdk::{
10    commitment_config::CommitmentConfig, message::VersionedMessage,
11    transaction::VersionedTransaction,
12};
13use solana_sdk::{
14    instruction::{AccountMeta, InstructionError},
15    pubkey::Pubkey,
16    transaction::TransactionError,
17};
18use thiserror::Error;
19use tokio::sync::oneshot;
20use tokio_tungstenite::tungstenite;
21
22// re-export types in public API
23pub use crate::drift_idl::{
24    accounts::{self},
25    errors::{self},
26    events::{self},
27    instructions::{self},
28    types::*,
29};
30use crate::{
31    constants::{ids, LUTS_DEVNET, LUTS_MAINNET},
32    drift_idl::errors::ErrorCode,
33    grpc::grpc_subscriber::GrpcError,
34    Wallet,
35};
36
37/// Map from K => V
38pub type MapOf<K, V> = DashMap<K, V, ahash::RandomState>;
39
40/// Handle for unsubscribing from network updates
41pub type UnsubHandle = oneshot::Sender<()>;
42
43pub type SdkResult<T> = Result<T, SdkError>;
44
45pub fn is_one_of_variant<T: PartialEq>(value: &T, variants: &[T]) -> bool {
46    variants.iter().any(|variant| value == variant)
47}
48
49/// Drift program context
50///
51/// Contains network specific variables necessary for interacting with drift program
52/// on different networks
53#[derive(Debug, Copy, Clone, PartialEq)]
54pub struct Context {
55    name: &'static str,
56    /// market lookup table
57    luts: &'static [Pubkey],
58    /// pyth program ID
59    pyth: Pubkey,
60}
61
62impl Context {
63    /// Target MainNet context
64    #[allow(non_upper_case_globals)]
65    pub const MainNet: Context = Self {
66        name: "mainnet",
67        luts: LUTS_MAINNET,
68        pyth: ids::pyth_program::ID,
69    };
70    /// Target DevNet context
71    #[allow(non_upper_case_globals)]
72    pub const DevNet: Context = Self {
73        name: "devnet",
74        luts: LUTS_DEVNET,
75        pyth: ids::pyth_program::ID_DEVNET,
76    };
77
78    /// Return drift lookup table address(es)
79    pub fn luts(&self) -> &[Pubkey] {
80        self.luts
81    }
82
83    /// Return pyth owner address
84    pub fn pyth(&self) -> Pubkey {
85        self.pyth
86    }
87
88    /// Return name
89    pub fn name(&self) -> &'static str {
90        self.name
91    }
92}
93
94/// Some data from chain along with the retreived slot
95#[derive(Debug, Clone)]
96pub struct DataAndSlot<T> {
97    pub slot: u64,
98    pub data: T,
99}
100
101/// Id of a Drift market
102#[derive(Copy, Clone, Default, PartialEq, Eq, Hash)]
103pub struct MarketId {
104    index: u16,
105    kind: MarketType,
106}
107
108// there are derived/auto-generated trait impls for `MarketType` so
109// it can be used a key in maps, within `MarketId`
110// doing here rather than adding to all structs or special casing in IDL generation
111impl core::cmp::Eq for MarketType {}
112impl core::hash::Hash for MarketType {
113    fn hash<H: core::hash::Hasher>(&self, ra_expand_state: &mut H) {
114        core::mem::discriminant(self).hash(ra_expand_state);
115        match self {
116            MarketType::Spot => {}
117            MarketType::Perp => {}
118        }
119    }
120}
121
122impl MarketId {
123    /// Create a new `MarketId` from parts
124    pub fn new(index: u16, kind: MarketType) -> Self {
125        Self { index, kind }
126    }
127    /// `MarketId` for the USDC Spot Market
128    pub const QUOTE_SPOT: Self = Self {
129        index: 0,
130        kind: MarketType::Spot,
131    };
132    /// Id of a perp market
133    pub const fn perp(index: u16) -> Self {
134        Self {
135            index,
136            kind: MarketType::Perp,
137        }
138    }
139    /// Id of a spot market
140    pub const fn spot(index: u16) -> Self {
141        Self {
142            index,
143            kind: MarketType::Spot,
144        }
145    }
146    /// uint index of the market
147    pub fn index(&self) -> u16 {
148        self.index
149    }
150    /// type of the market
151    pub fn kind(&self) -> MarketType {
152        self.kind
153    }
154    /// Convert self into its parts
155    pub fn to_parts(self) -> (u16, MarketType) {
156        (self.index, self.kind)
157    }
158    pub fn is_perp(self) -> bool {
159        self.kind == MarketType::Perp
160    }
161    pub fn is_spot(self) -> bool {
162        self.kind == MarketType::Spot
163    }
164}
165
166impl std::fmt::Debug for MarketId {
167    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
168        match self.kind {
169            MarketType::Perp => {
170                write!(f, "perp/{}", self.index)
171            }
172            MarketType::Spot => {
173                write!(f, "spot/{}", self.index)
174            }
175        }
176    }
177}
178
179impl From<(u16, MarketType)> for MarketId {
180    fn from(value: (u16, MarketType)) -> Self {
181        Self {
182            index: value.0,
183            kind: value.1,
184        }
185    }
186}
187
188/// Provides builder API for Orders
189#[derive(Default)]
190pub struct NewOrder {
191    order_type: OrderType,
192    direction: PositionDirection,
193    reduce_only: bool,
194    market_id: MarketId,
195    post_only: PostOnlyParam,
196    ioc: bool,
197    amount: u64,
198    price: u64,
199    user_order_id: u8,
200}
201
202impl NewOrder {
203    /// Create a market order
204    pub fn market(market_id: MarketId) -> Self {
205        Self {
206            order_type: OrderType::Market,
207            market_id,
208            ..Default::default()
209        }
210    }
211    /// Create a limit order
212    pub fn limit(market_id: MarketId) -> Self {
213        Self {
214            order_type: OrderType::Limit,
215            market_id,
216            ..Default::default()
217        }
218    }
219    /// Set order amount
220    ///
221    /// A sub-zero amount indicates a short
222    pub fn amount(mut self, amount: i64) -> Self {
223        self.direction = if amount >= 0 {
224            PositionDirection::Long
225        } else {
226            PositionDirection::Short
227        };
228        self.amount = amount.unsigned_abs();
229
230        self
231    }
232    /// Set order price
233    pub fn price(mut self, price: u64) -> Self {
234        self.price = price;
235        self
236    }
237    /// Set reduce only (default: false)
238    pub fn reduce_only(mut self, flag: bool) -> Self {
239        self.reduce_only = flag;
240        self
241    }
242    /// Set immediate or cancel (default: false)
243    pub fn ioc(mut self, flag: bool) -> Self {
244        self.ioc = flag;
245        self
246    }
247    /// Set post-only (default: None)
248    pub fn post_only(mut self, value: PostOnlyParam) -> Self {
249        self.post_only = value;
250        self
251    }
252    /// Set user order id
253    pub fn user_order_id(mut self, user_order_id: u8) -> Self {
254        self.user_order_id = user_order_id;
255        self
256    }
257    /// Call to complete building the Order
258    pub fn build(self) -> OrderParams {
259        OrderParams {
260            order_type: self.order_type,
261            market_index: self.market_id.index,
262            market_type: self.market_id.kind,
263            price: self.price,
264            base_asset_amount: self.amount,
265            reduce_only: self.reduce_only,
266            direction: self.direction,
267            immediate_or_cancel: self.ioc,
268            post_only: self.post_only,
269            user_order_id: self.user_order_id,
270            ..Default::default()
271        }
272    }
273}
274
275#[derive(Debug, Error)]
276pub enum SdkError {
277    #[error("{0}")]
278    Rpc(#[from] solana_rpc_client_api::client_error::Error),
279    #[error("{0}")]
280    Ws(#[from] drift_pubsub_client::PubsubClientError),
281    #[error("{0}")]
282    Anchor(#[from] Box<anchor_lang::error::Error>),
283    #[error("error while deserializing")]
284    Deserializing,
285    #[error("invalid drift account")]
286    InvalidAccount,
287    #[error("invalid oracle account")]
288    InvalidOracle,
289    #[error("invalid keypair seed")]
290    InvalidSeed,
291    #[error("invalid base58 value")]
292    InvalidBase58,
293    #[error("user does not have position: {0}")]
294    NoPosition(u16),
295    #[error("insufficient SOL balance for fees")]
296    OutOfSOL,
297    #[error("{0}")]
298    Signing(#[from] solana_sdk::signer::SignerError),
299    #[error("Received Error from websocket")]
300    WebsocketError,
301    #[error("Missed DLOB heartbeat")]
302    MissedHeartbeat,
303    #[error("Unsupported account data format")]
304    UnsupportedAccountData,
305    #[error("Could not decode data: {0}")]
306    CouldntDecode(#[from] base64::DecodeError),
307    #[error("Couldn't join task: {0}")]
308    CouldntJoin(#[from] tokio::task::JoinError),
309    #[error("Couldn't send unsubscribe message")]
310    CouldntUnsubscribe,
311    #[error("MathError")]
312    MathError(String),
313    #[error("{0}")]
314    BorrowMutError(#[from] BorrowMutError),
315    #[error("{0}")]
316    BorrowError(#[from] BorrowError),
317    #[error("{0}")]
318    Generic(String),
319    #[error("max connection attempts reached")]
320    MaxReconnectionAttemptsReached,
321    #[error("jit taker order not found")]
322    JitOrderNotFound,
323    #[error("market data unavailable. subscribe market: {0:?}")]
324    NoMarketData(MarketId),
325    #[error("account data unavailable. subscribe account: {0:?}")]
326    NoAccountData(Pubkey),
327    #[error("component is already subscribed")]
328    AlreadySubscribed,
329    #[error("invalid URL")]
330    InvalidUrl,
331    #[error("{0}")]
332    WsClient(#[from] tungstenite::Error),
333    #[error("libdrift_ffi_sys out-of-date")]
334    LibDriftVersion,
335    #[error("wallet signing disabled")]
336    WalletSigningDisabled,
337    #[error("{0}")]
338    Grpc(#[from] GrpcError),
339}
340
341impl SdkError {
342    /// extract anchor error code from the SdkError if it exists
343    pub fn to_anchor_error_code(&self) -> Option<ErrorCode> {
344        if let SdkError::Rpc(inner) = self {
345            if let Some(TransactionError::InstructionError(_, InstructionError::Custom(code))) =
346                inner.get_transaction_error()
347            {
348                // inverse of anchor's 'From<ErrorCode> for u32'
349                return Some(unsafe {
350                    std::mem::transmute::<u32, ErrorCode>(
351                        code - anchor_lang::error::ERROR_CODE_OFFSET,
352                    )
353                });
354            }
355        }
356        None
357    }
358    /// convert to 'out of sol' error is possible
359    pub fn to_out_of_sol_error(&self) -> Option<SdkError> {
360        if let SdkError::Rpc(inner) = self {
361            if let Some(
362                TransactionError::InsufficientFundsForFee
363                | TransactionError::InsufficientFundsForRent { account_index: _ },
364            ) = inner.get_transaction_error()
365            {
366                return Some(Self::OutOfSOL);
367            }
368        }
369        None
370    }
371}
372
373/// Helper type for Accounts included in drift instructions
374///
375/// Provides sorting implementation matching drift program
376#[derive(Copy, Clone, Debug, PartialEq, Eq)]
377#[repr(u8)]
378pub(crate) enum RemainingAccount {
379    Oracle { pubkey: Pubkey },
380    Spot { pubkey: Pubkey, writable: bool },
381    Perp { pubkey: Pubkey, writable: bool },
382}
383
384impl RemainingAccount {
385    fn pubkey(&self) -> &Pubkey {
386        match self {
387            Self::Oracle { pubkey } => pubkey,
388            Self::Spot { pubkey, .. } => pubkey,
389            Self::Perp { pubkey, .. } => pubkey,
390        }
391    }
392    fn parts(self) -> (Pubkey, bool) {
393        match self {
394            Self::Oracle { pubkey } => (pubkey, false),
395            Self::Spot {
396                pubkey, writable, ..
397            } => (pubkey, writable),
398            Self::Perp {
399                pubkey, writable, ..
400            } => (pubkey, writable),
401        }
402    }
403    fn discriminant(&self) -> u8 {
404        // SAFETY: Because `Self` is marked `repr(u8)`, its layout is a `repr(C)` `union`
405        // between `repr(C)` structs, each of which has the `u8` discriminant as its first
406        // field, so we can read the discriminant without offsetting the pointer.
407        let ptr = <*const RemainingAccount>::from(self);
408        unsafe { *ptr.cast::<u8>() }
409    }
410}
411
412impl Ord for RemainingAccount {
413    fn cmp(&self, other: &Self) -> Ordering {
414        let type_order = self.discriminant().cmp(&other.discriminant());
415        if let Ordering::Equal = type_order {
416            self.pubkey().cmp(other.pubkey())
417        } else {
418            type_order
419        }
420    }
421}
422
423impl PartialOrd for RemainingAccount {
424    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
425        Some(self.cmp(other))
426    }
427}
428
429impl From<RemainingAccount> for AccountMeta {
430    fn from(value: RemainingAccount) -> Self {
431        let (pubkey, is_writable) = value.parts();
432        AccountMeta {
433            pubkey,
434            is_writable,
435            is_signer: false,
436        }
437    }
438}
439
440/// Provide market precision information
441pub trait MarketPrecision {
442    // prices must be a multiple of this
443    fn price_tick(&self) -> u64;
444    // order sizes must be a multiple of this
445    fn quantity_tick(&self) -> u64;
446    /// smallest order size
447    fn min_order_size(&self) -> u64;
448}
449
450impl MarketPrecision for accounts::SpotMarket {
451    fn min_order_size(&self) -> u64 {
452        self.min_order_size
453    }
454    fn price_tick(&self) -> u64 {
455        self.order_tick_size
456    }
457    fn quantity_tick(&self) -> u64 {
458        self.order_step_size
459    }
460}
461
462impl MarketPrecision for accounts::PerpMarket {
463    fn min_order_size(&self) -> u64 {
464        self.amm.min_order_size
465    }
466    fn price_tick(&self) -> u64 {
467        self.amm.order_tick_size
468    }
469    fn quantity_tick(&self) -> u64 {
470        self.amm.order_step_size
471    }
472}
473
474#[derive(Copy, Clone)]
475pub struct ReferrerInfo {
476    referrer: Pubkey,
477    referrer_stats: Pubkey,
478}
479
480impl ReferrerInfo {
481    pub fn new(referrer: Pubkey, referrer_stats: Pubkey) -> Self {
482        Self {
483            referrer,
484            referrer_stats,
485        }
486    }
487
488    pub fn referrer(&self) -> Pubkey {
489        self.referrer
490    }
491
492    pub fn referrer_stats(&self) -> Pubkey {
493        self.referrer_stats
494    }
495
496    pub fn get_referrer_info(taker_stats: accounts::UserStats) -> Option<Self> {
497        if taker_stats.referrer == Pubkey::default() {
498            return None;
499        }
500
501        let user_account_pubkey = Wallet::derive_user_account(&taker_stats.referrer, 0);
502        let user_stats_pubkey = Wallet::derive_stats_account(&taker_stats.referrer);
503
504        Some(Self {
505            referrer: user_account_pubkey,
506            referrer_stats: user_stats_pubkey,
507        })
508    }
509}
510
511impl OrderType {
512    pub fn as_str(&self) -> &str {
513        match self {
514            OrderType::Limit => "limit",
515            OrderType::Market => "market",
516            OrderType::Oracle => "oracle",
517            OrderType::TriggerLimit => "trigger_limit",
518            OrderType::TriggerMarket => "trigger_market",
519        }
520    }
521}
522
523impl MarketType {
524    pub fn as_str(&self) -> &str {
525        match self {
526            MarketType::Perp => "perp",
527            MarketType::Spot => "spot",
528        }
529    }
530}
531
532impl FromStr for MarketType {
533    type Err = ();
534    fn from_str(s: &str) -> Result<Self, Self::Err> {
535        if s.eq_ignore_ascii_case("perp") {
536            Ok(Self::Perp)
537        } else if s.eq_ignore_ascii_case("spot") {
538            Ok(Self::Spot)
539        } else {
540            Err(())
541        }
542    }
543}
544
545#[cfg(test)]
546mod tests {
547    use std::str::FromStr;
548
549    use solana_rpc_client_api::{
550        client_error::{Error as ClientError, ErrorKind as ClientErrorKind},
551        request::{RpcError, RpcRequest, RpcResponseErrorData},
552        response::RpcSimulateTransactionResult,
553    };
554    use solana_sdk::{
555        instruction::InstructionError, pubkey::Pubkey, transaction::TransactionError,
556    };
557
558    use super::{RemainingAccount, SdkError};
559    use crate::{drift_idl::errors::ErrorCode, MarketType};
560
561    #[test]
562    fn market_type_str() {
563        assert_eq!(MarketType::from_str("PERP").unwrap(), MarketType::Perp,);
564        assert_eq!(MarketType::from_str("spot").unwrap(), MarketType::Spot,);
565        assert_eq!("perp", MarketType::Perp.as_str());
566        assert_eq!("spot", MarketType::Spot.as_str());
567    }
568
569    #[test]
570    fn extract_anchor_error() {
571        let err = SdkError::Rpc(
572            ClientError {
573                request: Some(RpcRequest::SendTransaction),
574                kind: ClientErrorKind::RpcError(
575                    RpcError::RpcResponseError {
576                        code: -32002,
577                        message: "Transaction simulation failed: Error processing Instruction 0: custom program error: 0x17b7".to_string(),
578                        data: RpcResponseErrorData::SendTransactionPreflightFailure(
579                            RpcSimulateTransactionResult {
580                                err: Some(TransactionError::InstructionError(0, InstructionError::Custom(6071))),
581                                logs: None,
582                                accounts: None,
583                                units_consumed: None,
584                                return_data: None,
585                                inner_instructions: None,
586                                replacement_blockhash: None,
587                            }
588                        )
589                    }
590                )
591            }
592        );
593
594        assert_eq!(
595            err.to_anchor_error_code().unwrap(),
596            ErrorCode::UserOrderIdAlreadyInUse,
597        );
598    }
599
600    #[test]
601    fn account_type_sorting() {
602        let mut accounts = vec![
603            RemainingAccount::Perp {
604                pubkey: Pubkey::new_from_array([4_u8; 32]),
605                writable: false,
606            },
607            RemainingAccount::Oracle {
608                pubkey: Pubkey::new_from_array([2_u8; 32]),
609            },
610            RemainingAccount::Oracle {
611                pubkey: Pubkey::new_from_array([1_u8; 32]),
612            },
613            RemainingAccount::Spot {
614                pubkey: Pubkey::new_from_array([3_u8; 32]),
615                writable: true,
616            },
617        ];
618        accounts.sort();
619
620        assert_eq!(
621            accounts,
622            vec![
623                RemainingAccount::Oracle {
624                    pubkey: Pubkey::new_from_array([1_u8; 32])
625                },
626                RemainingAccount::Oracle {
627                    pubkey: Pubkey::new_from_array([2_u8; 32])
628                },
629                RemainingAccount::Spot {
630                    pubkey: Pubkey::new_from_array([3_u8; 32]),
631                    writable: true
632                },
633                RemainingAccount::Perp {
634                    pubkey: Pubkey::new_from_array([4_u8; 32]),
635                    writable: false
636                },
637            ]
638        )
639    }
640}