Skip to main content

drift_rs/
marketmap.rs

1use std::{
2    collections::HashSet,
3    sync::{
4        atomic::{AtomicU64, Ordering},
5        Arc,
6    },
7};
8
9use anchor_lang::{AccountDeserialize, AnchorDeserialize};
10use dashmap::DashMap;
11use drift_pubsub_client::PubsubClient;
12use futures_util::{stream::FuturesUnordered, StreamExt};
13use serde_json::json;
14use solana_account_decoder_client_types::UiAccountEncoding;
15use solana_rpc_client::nonblocking::rpc_client::RpcClient;
16use solana_rpc_client_api::{
17    config::{RpcAccountInfoConfig, RpcProgramAccountsConfig},
18    request::RpcRequest,
19    response::{OptionalContext, RpcKeyedAccount},
20};
21use solana_sdk::{clock::Slot, commitment_config::CommitmentConfig, pubkey::Pubkey};
22
23use crate::{
24    accounts::State,
25    constants::{self, derive_perp_market_account, derive_spot_market_account, state_account},
26    drift_idl::types::OracleSource,
27    grpc::AccountUpdate,
28    memcmp::get_market_filter,
29    types::MapOf,
30    websocket_account_subscriber::WebsocketAccountSubscriber,
31    DataAndSlot, MarketId, MarketType, PerpMarket, SdkResult, SpotMarket, UnsubHandle,
32};
33
34const LOG_TARGET: &str = "marketmap";
35
36pub trait Market {
37    const MARKET_TYPE: MarketType;
38    fn market_index(&self) -> u16;
39    fn oracle_info(&self) -> (MarketId, Pubkey, OracleSource);
40}
41
42impl Market for PerpMarket {
43    const MARKET_TYPE: MarketType = MarketType::Perp;
44
45    fn market_index(&self) -> u16 {
46        self.market_index
47    }
48
49    fn oracle_info(&self) -> (MarketId, Pubkey, OracleSource) {
50        (
51            MarketId::perp(self.market_index),
52            self.amm.oracle,
53            self.amm.oracle_source,
54        )
55    }
56}
57
58impl Market for SpotMarket {
59    const MARKET_TYPE: MarketType = MarketType::Spot;
60
61    fn market_index(&self) -> u16 {
62        self.market_index
63    }
64
65    fn oracle_info(&self) -> (MarketId, Pubkey, OracleSource) {
66        (
67            MarketId::spot(self.market_index),
68            self.oracle,
69            self.oracle_source,
70        )
71    }
72}
73
74/// Dynamic map of Drift Spot or Perp market accounts
75///
76/// Caller can subscribe to updates via Ws with `.subscribe(..)`
77/// or drive the map by calling `.sync()` periodically
78pub struct MarketMap<T: AnchorDeserialize + Send> {
79    pub marketmap: Arc<DashMap<u16, DataAndSlot<T>, ahash::RandomState>>,
80    subscriptions: DashMap<u16, UnsubHandle, ahash::RandomState>,
81    latest_slot: Arc<AtomicU64>,
82    pubsub: Arc<PubsubClient>,
83    commitment: CommitmentConfig,
84}
85
86impl<T> MarketMap<T>
87where
88    T: AnchorDeserialize + Clone + Send + Sync + Market + 'static,
89{
90    pub const SUBSCRIPTION_ID: &'static str = "marketmap";
91
92    pub fn new(pubsub: Arc<PubsubClient>, commitment: CommitmentConfig) -> Self {
93        Self {
94            subscriptions: Default::default(),
95            marketmap: Arc::default(),
96            latest_slot: Arc::new(AtomicU64::new(0)),
97            pubsub,
98            commitment,
99        }
100    }
101
102    /// Return a reference to the internal map data structure
103    pub fn map(&self) -> Arc<MapOf<u16, DataAndSlot<T>>> {
104        Arc::clone(&self.marketmap)
105    }
106
107    /// Returns a hook for driving the map with new `Account` updates
108    pub(crate) fn on_account_fn(&self) -> impl Fn(&AccountUpdate) {
109        let marketmap = self.map();
110        move |update: &AccountUpdate| {
111            let market = T::deserialize(&mut &update.data[8..]).expect("deser market");
112            let idx = market.market_index();
113            marketmap.insert(
114                idx,
115                DataAndSlot {
116                    slot: update.slot,
117                    data: market,
118                },
119            );
120        }
121    }
122
123    /// Subscribe to market account updates
124    pub async fn subscribe(&self, markets: &[MarketId]) -> SdkResult<()> {
125        log::debug!(target: LOG_TARGET, "subscribing: {:?}", T::MARKET_TYPE);
126
127        let markets = HashSet::<MarketId>::from_iter(markets.iter().copied());
128        let mut pending_subscriptions =
129            Vec::<(u16, WebsocketAccountSubscriber)>::with_capacity(markets.len());
130        for market in markets {
131            if self.subscriptions.contains_key(&market.index()) {
132                continue;
133            }
134
135            let market_pubkey = match T::MARKET_TYPE {
136                MarketType::Perp => derive_perp_market_account(market.index()),
137                MarketType::Spot => derive_spot_market_account(market.index()),
138            };
139            let market_subscriber = WebsocketAccountSubscriber::new(
140                Arc::clone(&self.pubsub),
141                market_pubkey,
142                self.commitment,
143            );
144
145            pending_subscriptions.push((market.index(), market_subscriber));
146        }
147
148        let futs_iter = pending_subscriptions.into_iter().map(|(idx, fut)| {
149            let marketmap = Arc::clone(&self.marketmap);
150            let latest_slot = self.latest_slot.clone();
151            async move {
152                let unsub = fut
153                    .subscribe(Self::SUBSCRIPTION_ID, false, {
154                        move |update| {
155                            if update.slot > latest_slot.load(Ordering::Relaxed) {
156                                latest_slot.store(update.slot, Ordering::Relaxed);
157                            }
158                            marketmap.insert(
159                                idx,
160                                DataAndSlot {
161                                    slot: update.slot,
162                                    data: T::deserialize(&mut &update.data.as_slice()[8..])
163                                        .expect("valid market"),
164                                },
165                            );
166                        }
167                    })
168                    .await;
169                (idx, unsub)
170            }
171        });
172
173        let mut subscription_futs = FuturesUnordered::from_iter(futs_iter);
174        while let Some((market, unsub)) = subscription_futs.next().await {
175            log::debug!(target: LOG_TARGET, "subscribed market: {market:?}");
176            self.subscriptions.insert(market, unsub?);
177        }
178
179        log::debug!(target: LOG_TARGET, "subscribed: {:?}", T::MARKET_TYPE);
180
181        Ok(())
182    }
183
184    /// Returns whether the market is subscribed to live updates or not
185    pub fn is_subscribed(&self, market_index: u16) -> bool {
186        self.subscriptions.contains_key(&market_index)
187    }
188
189    /// Unsubscribe from updates for the given `markets`
190    pub fn unsubscribe(&self, markets: &[MarketId]) -> SdkResult<()> {
191        for market in markets {
192            if let Some((market, unsub)) = self.subscriptions.remove(&market.index()) {
193                let _ = unsub.send(());
194                self.marketmap.remove(&market);
195            }
196        }
197        log::debug!(target: LOG_TARGET, "unsubscribed markets: {markets:?}");
198
199        Ok(())
200    }
201
202    /// Unsubscribe from all market updates
203    pub fn unsubscribe_all(&self) -> SdkResult<()> {
204        let all_markets: Vec<MarketId> = self
205            .subscriptions
206            .iter()
207            .map(|x| (*x.key(), T::MARKET_TYPE).into())
208            .collect();
209        self.unsubscribe(&all_markets)
210    }
211
212    pub fn values(&self) -> Vec<T> {
213        self.marketmap.iter().map(|x| x.data.clone()).collect()
214    }
215
216    /// Returns a list of oracle info for each market
217    pub fn oracles(&self) -> Vec<(MarketId, Pubkey, OracleSource)> {
218        self.values().iter().map(|x| x.oracle_info()).collect()
219    }
220
221    pub fn len(&self) -> usize {
222        self.marketmap.len()
223    }
224
225    pub fn contains(&self, market_index: &u16) -> bool {
226        self.marketmap.contains_key(market_index)
227    }
228
229    pub fn get(&self, market_index: &u16) -> Option<DataAndSlot<T>> {
230        self.marketmap
231            .get(market_index)
232            .map(|market| market.clone())
233    }
234
235    /// Sync all market accounts
236    pub async fn sync(&self, rpc: &RpcClient) -> SdkResult<()> {
237        log::debug!(target: LOG_TARGET, "syncing marketmap: {:?}", T::MARKET_TYPE);
238        let (markets, latest_slot) = get_market_accounts_with_fallback::<T>(rpc).await?;
239        for market in markets {
240            self.marketmap.insert(
241                market.market_index(),
242                DataAndSlot {
243                    data: market,
244                    slot: latest_slot,
245                },
246            );
247        }
248        self.latest_slot.store(latest_slot, Ordering::Relaxed);
249
250        log::debug!(target: LOG_TARGET, "synced {:?} marketmap with {} markets", T::MARKET_TYPE, self.marketmap.len());
251        Ok(())
252    }
253
254    pub fn get_latest_slot(&self) -> u64 {
255        self.latest_slot.load(Ordering::Relaxed)
256    }
257}
258
259/// Fetch all market (program) accounts with multiple fallbacks
260///
261/// Tries progressively less intensive RPC methods for wider compatibility with RPC providers:
262///     getProgramAccounts, getMultipleAccounts, lastly multiple getAccountInfo
263///
264/// Returns deserialized accounts and retrieved slot
265pub async fn get_market_accounts_with_fallback<T: Market + AnchorDeserialize>(
266    rpc: &RpcClient,
267) -> SdkResult<(Vec<T>, Slot)> {
268    let mut markets = Vec::<T>::default();
269
270    let account_config = RpcAccountInfoConfig {
271        commitment: Some(rpc.commitment()),
272        encoding: Some(UiAccountEncoding::Base64Zstd),
273        ..RpcAccountInfoConfig::default()
274    };
275
276    let gpa_config = RpcProgramAccountsConfig {
277        filters: Some(vec![get_market_filter(T::MARKET_TYPE)]),
278        account_config: account_config.clone(),
279        with_context: Some(true),
280        sort_results: None,
281    };
282
283    // try 'getProgramAccounts'
284    let response: Result<OptionalContext<Vec<RpcKeyedAccount>>, _> = rpc
285        .send(
286            RpcRequest::GetProgramAccounts,
287            json!([constants::PROGRAM_ID.to_string(), gpa_config]),
288        )
289        .await;
290
291    if let Ok(OptionalContext::Context(accounts)) = response {
292        for account in accounts.value {
293            let market_data = account.account.data.decode().expect("Market data");
294            let data = T::deserialize(&mut &market_data[8..]).expect("deserializes Market");
295            markets.push(data);
296        }
297        return Ok((markets, accounts.context.slot));
298    }
299    log::debug!(target: LOG_TARGET, "syncing with getProgramAccounts failed: {:?}", T::MARKET_TYPE);
300
301    let state_response = rpc
302        .get_account_with_config(state_account(), account_config)
303        .await
304        .expect("state account fetch");
305
306    let state_data = state_response.value.expect("state has data").data;
307    let state =
308        State::try_deserialize_unchecked(&mut state_data.as_slice()).expect("state deserializes");
309
310    let market_pdas: Vec<Pubkey> = match T::MARKET_TYPE {
311        MarketType::Spot => (0..state.number_of_spot_markets)
312            .map(derive_spot_market_account)
313            .collect(),
314        MarketType::Perp => (0..state.number_of_markets)
315            .map(derive_perp_market_account)
316            .collect(),
317    };
318
319    // try 'getMultipleAccounts'
320    let mut market_requests = FuturesUnordered::new();
321    for market_chunk in market_pdas.chunks(64) {
322        market_requests
323            .push(rpc.get_multiple_accounts_with_commitment(market_chunk, rpc.commitment()));
324    }
325
326    while let Some(market_response) = market_requests.next().await {
327        match market_response {
328            Ok(data) => {
329                for market in data.value {
330                    match market {
331                        Some(market) => {
332                            markets.push(
333                                T::deserialize(&mut &market.data.as_slice()[8..])
334                                    .expect("market deserializes"),
335                            );
336                        }
337                        None => {
338                            log::warn!(target: LOG_TARGET, "failed to fetch market account (missing)");
339                            break;
340                        }
341                    }
342                }
343            }
344            Err(err) => {
345                log::warn!(target: LOG_TARGET, "failed to fetch market accounts: {err:?}");
346                return Err(err)?;
347            }
348        }
349    }
350    if market_pdas.len() == markets.len() {
351        return Ok((markets, state_response.context.slot));
352    }
353    log::debug!(target: LOG_TARGET, "syncing with getMultipleAccounts failed: {:?}", T::MARKET_TYPE);
354
355    // try multiple 'getAccount's
356    let mut market_requests =
357        FuturesUnordered::from_iter(market_pdas.iter().map(|acc| rpc.get_account_data(acc)));
358
359    while let Some(market_response) = market_requests.next().await {
360        match market_response {
361            Ok(data) => {
362                markets
363                    .push(T::deserialize(&mut &data.as_slice()[8..]).expect("market deserializes"));
364            }
365            Err(err) => {
366                log::warn!("failed to fetch market account: {err:?}");
367                return Err(err)?;
368            }
369        }
370    }
371
372    Ok((markets, state_response.context.slot))
373}
374
375#[cfg(test)]
376mod tests {
377    use std::sync::Arc;
378
379    use drift_pubsub_client::PubsubClient;
380    use solana_rpc_client::nonblocking::rpc_client::RpcClient;
381    use solana_sdk::commitment_config::CommitmentConfig;
382
383    use super::{get_market_accounts_with_fallback, MarketMap};
384    use crate::{
385        accounts::{PerpMarket, SpotMarket},
386        utils::{get_ws_url, test_envs::devnet_endpoint},
387        MarketId,
388    };
389
390    #[tokio::test]
391    async fn marketmap_subscribe() {
392        let map = MarketMap::<PerpMarket>::new(
393            Arc::new(
394                PubsubClient::new(&get_ws_url(&devnet_endpoint()).unwrap())
395                    .await
396                    .expect("ws connects"),
397            ),
398            CommitmentConfig::confirmed(),
399        );
400
401        assert!(map
402            .subscribe(&[MarketId::perp(0), MarketId::perp(1), MarketId::perp(1)])
403            .await
404            .is_ok());
405        assert!(map.is_subscribed(0));
406        assert!(map.is_subscribed(1));
407        assert_eq!(map.subscriptions.len(), 2);
408
409        assert!(map.unsubscribe_all().is_ok());
410        assert_eq!(map.subscriptions.len(), 0);
411        assert!(!map.is_subscribed(0));
412        assert!(!map.is_subscribed(1));
413    }
414
415    #[tokio::test]
416    async fn get_market_accounts_with_fallback_works() {
417        let result: Result<(Vec<PerpMarket>, _), _> =
418            get_market_accounts_with_fallback::<PerpMarket>(&RpcClient::new(devnet_endpoint()))
419                .await;
420
421        assert!(result.is_ok_and(|r| r.0.len() > 0 && r.1 > 0));
422
423        let result =
424            get_market_accounts_with_fallback::<SpotMarket>(&RpcClient::new(devnet_endpoint()))
425                .await;
426
427        assert!(result.is_ok_and(|r| r.0.len() > 0 && r.1 > 0));
428    }
429}
430
431#[cfg(feature = "rpc_tests")]
432mod rpc_tests {
433    use solana_sdk::commitment_config::CommitmentConfig;
434
435    use super::*;
436    use crate::utils::test_envs::mainnet_endpoint;
437
438    #[tokio::test]
439    async fn test_marketmap_perp() {
440        let commitment = CommitmentConfig {
441            commitment: CommitmentConfig::Processed,
442        };
443
444        let marketmap = MarketMap::<PerpMarket>::new(commitment, mainnet_endpoint(), true);
445        marketmap.subscribe().await.unwrap();
446
447        tokio::time::sleep(tokio::time::Duration::from_secs(30)).await;
448
449        dbg!(marketmap.size());
450        assert!(marketmap.size() == 28);
451
452        dbg!(marketmap.get_latest_slot());
453
454        marketmap.unsubscribe().await.unwrap();
455
456        tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
457
458        assert_eq!(marketmap.size(), 0);
459        assert_eq!(marketmap.subscribed.load(Ordering::Relaxed), false);
460    }
461
462    #[tokio::test]
463    async fn test_marketmap_spot() {
464        let commitment = CommitmentConfig {
465            commitment: CommitmentConfig::Processed,
466        };
467
468        let marketmap = MarketMap::<SpotMarket>::new(commitment, RPC, true);
469        marketmap.subscribe().await.unwrap();
470
471        tokio::time::sleep(tokio::time::Duration::from_secs(30)).await;
472
473        dbg!(marketmap.size());
474        assert!(marketmap.size() == 13);
475
476        dbg!(marketmap.get_latest_slot());
477
478        marketmap.unsubscribe().await.unwrap();
479
480        tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
481
482        assert_eq!(marketmap.size(), 0);
483        assert_eq!(marketmap.subscribed.get(), false);
484    }
485}