pyra-redis 0.2.16

Shared Redis client, key builders, and common operations for Pyra services
Documentation
//! Typed fetch helpers for domain-specific Redis data.
//!
//! These methods combine [`RedisKey`] patterns with JSON deserialization
//! to return strongly-typed Pyra/Drift account data from Redis.
//!
//! All account data is stored by the indexer wrapped in [`Cache<T>`],
//! which includes the Solana slot at which the data was last updated.
//! These helpers unwrap the `Cache` and return the inner account type.
//! If you need the slot information, use [`RedisClient::get_json::<Cache<T>>`] directly.

use std::collections::HashMap;

use solana_pubkey::Pubkey;

use pyra_types::{
    Cache, DriftUser, SpendLimitsOrderAccount, SpotMarket, Vault, WithdrawOrderAccount,
};

use crate::{RedisClient, RedisKey, RedisResult};

impl RedisClient {
    // ── Single-key fetches ────────────────────────────────────────────

    /// Fetch a Pyra vault account by its PDA address.
    pub async fn fetch_vault(&self, vault: &Pubkey) -> RedisResult<Option<Vault>> {
        let key = RedisKey::vault(vault);
        let cached: Option<Cache<Vault>> = self.get_json(key.as_str()).await?;
        Ok(cached.map(|c| c.account))
    }

    /// Fetch a Drift user account by the vault (authority) address.
    pub async fn fetch_drift_user(&self, authority: &Pubkey) -> RedisResult<Option<DriftUser>> {
        let key = RedisKey::drift_user(authority);
        let cached: Option<Cache<DriftUser>> = self.get_json(key.as_str()).await?;
        Ok(cached.map(|c| c.account))
    }

    /// Fetch a single Drift spot market by index.
    pub async fn fetch_spot_market(
        &self,
        market_index: u16,
    ) -> RedisResult<Option<SpotMarket>> {
        let key = RedisKey::drift_spot_market(market_index);
        let cached: Option<Cache<SpotMarket>> = self.get_json(key.as_str()).await?;
        Ok(cached.map(|c| c.account))
    }

    /// Fetch a withdraw order account by its PDA address.
    pub async fn fetch_withdraw_order(
        &self,
        order: &Pubkey,
    ) -> RedisResult<Option<WithdrawOrderAccount>> {
        let key = RedisKey::withdraw_order(order);
        let cached: Option<Cache<WithdrawOrderAccount>> = self.get_json(key.as_str()).await?;
        Ok(cached.map(|c| c.account))
    }

    /// Fetch a spend limits order account by its PDA address.
    pub async fn fetch_spend_limits_order(
        &self,
        order: &Pubkey,
    ) -> RedisResult<Option<SpendLimitsOrderAccount>> {
        let key = RedisKey::spend_limits_order(order);
        let cached: Option<Cache<SpendLimitsOrderAccount>> = self.get_json(key.as_str()).await?;
        Ok(cached.map(|c| c.account))
    }

    /// Fetch the oracle price for a market (stored as a plain `f64` by the indexer).
    pub async fn fetch_price(&self, market_index: u16) -> RedisResult<Option<f64>> {
        let key = RedisKey::price(market_index);
        self.get_json(key.as_str()).await
    }

    // ── Batch fetches ─────────────────────────────────────────────────

    /// Fetch multiple spot markets by their indices in a single MGET round-trip.
    ///
    /// Returns only the markets that were found and successfully deserialized.
    pub async fn fetch_spot_markets(
        &self,
        indices: &[u16],
    ) -> RedisResult<HashMap<u16, SpotMarket>> {
        if indices.is_empty() {
            return Ok(HashMap::new());
        }

        let keys: Vec<String> = indices
            .iter()
            .map(|&i| RedisKey::drift_spot_market(i).to_string())
            .collect();

        let values = self.mget(&keys).await?;
        let mut result = HashMap::with_capacity(indices.len());

        for (idx, val) in indices.iter().zip(values.into_iter()) {
            if let Some(raw) = val {
                if let Ok(cached) = serde_json::from_str::<Cache<SpotMarket>>(&raw) {
                    result.insert(*idx, cached.account);
                }
            }
        }

        Ok(result)
    }

    /// Fetch all spot markets by scanning `account:drift:spot_market:*` keys.
    ///
    /// Uses SCAN + MGET for efficiency. Returns market_index -> SpotMarket.
    pub async fn fetch_all_spot_markets(&self) -> RedisResult<HashMap<u16, SpotMarket>> {
        let pattern = RedisKey::pattern(RedisKey::DRIFT_SPOT_MARKET_PREFIX);
        let keys = self.scan_keys(&pattern).await?;

        if keys.is_empty() {
            return Ok(HashMap::new());
        }

        let values = self.mget(&keys).await?;
        let mut result = HashMap::with_capacity(keys.len());

        for val in values.into_iter().flatten() {
            if let Ok(cached) = serde_json::from_str::<Cache<SpotMarket>>(&val) {
                result.insert(cached.account.market_index, cached.account);
            }
        }

        Ok(result)
    }

    /// Fetch prices for multiple markets in a single MGET round-trip.
    ///
    /// Returns only markets that have a cached price.
    pub async fn fetch_prices(
        &self,
        market_indices: &[u16],
    ) -> RedisResult<HashMap<u16, f64>> {
        if market_indices.is_empty() {
            return Ok(HashMap::new());
        }

        let keys: Vec<String> = market_indices
            .iter()
            .map(|&i| RedisKey::price(i).to_string())
            .collect();

        let values = self.mget(&keys).await?;
        let mut result = HashMap::with_capacity(market_indices.len());

        for (idx, val) in market_indices.iter().zip(values.into_iter()) {
            if let Some(raw) = val {
                if let Ok(price) = serde_json::from_str::<f64>(&raw) {
                    result.insert(*idx, price);
                }
            }
        }

        Ok(result)
    }
}