pyra-redis 0.4.0

Shared Redis client, key builders, and common operations for Pyra services
Documentation
use core::fmt;

use redis::ToRedisArgs;
use solana_pubkey::Pubkey;

/// A typed Redis key with factory methods for all Pyra key patterns.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RedisKey(String);

impl RedisKey {
    /// Create a key from a raw string.
    pub fn from_string(s: String) -> Self {
        Self(s)
    }

    /// Return a reference to the inner string.
    pub fn as_str(&self) -> &str {
        &self.0
    }

    // ── Shared prefixes ──────────────────────────────────────────────

    pub const VAULT_PREFIX: &'static str = "account:pyra:vault";
    pub const WITHDRAW_ORDER_PREFIX: &'static str = "account:pyra:withdraw_order";
    pub const SPEND_LIMITS_ORDER_PREFIX: &'static str = "account:pyra:spend_limits_order";
    pub const DEPOSIT_ADDRESS_PREFIX: &'static str = "account:pyra:deposit_address";
    pub const DEPOSIT_ADDRESS_OWNER_PREFIX: &'static str = "reverse:deposit_address";
    pub const USER_WITHDRAW_ORDERS_PREFIX: &'static str = "user:orders:withdraws";
    pub const USER_SPEND_LIMITS_ORDERS_PREFIX: &'static str = "user:orders:spend_limits";
    pub const TOKEN_ACCOUNT_PREFIX: &'static str = "account:token";
    pub const PRICE_PREFIX: &'static str = "price";
    pub const ORACLE_PREFIX: &'static str = "oracle";
    pub const ORACLE_CACHE_PREFIX: &'static str = "account:oracle";
    pub const REGISTERED_WALLETS: &'static str = "registered_wallets";
    pub const REGISTERED_WALLETS_CHANGES: &'static str = "registered_wallets:changes";
    pub const USER_SOLANA_ADDRESS_PREFIX: &'static str = "user:solana_address";
    pub const USER_PROVIDER_PREFIX: &'static str = "user:provider";
    pub const PENDING_DEPOSIT_PREFIX: &'static str = "pending_deposits";

    // ── Glob pattern ──────────────────────────────────────────────────

    /// Build a SCAN/KEYS glob pattern: `"{prefix}:*"`.
    pub fn pattern(prefix: &str) -> String {
        format!("{prefix}:*")
    }

    // ── Account keys ──────────────────────────────────────────────────

    pub fn vault(vault: &Pubkey) -> Self {
        let p = Self::VAULT_PREFIX;
        Self(format!("{p}:{vault}"))
    }

    pub fn deposit_address_spl(deposit_address: &Pubkey, mint: &Pubkey) -> Self {
        let p = Self::DEPOSIT_ADDRESS_PREFIX;
        Self(format!("{p}:{deposit_address}:{mint}"))
    }

    pub fn deposit_address_sol(deposit_address: &Pubkey) -> Self {
        let p = Self::DEPOSIT_ADDRESS_PREFIX;
        Self(format!("{p}:{deposit_address}:native"))
    }

    /// Reverse mapping: deposit address → owner pubkey.
    pub fn deposit_address_owner(deposit_address: &Pubkey) -> Self {
        let p = Self::DEPOSIT_ADDRESS_OWNER_PREFIX;
        Self(format!("{p}:{deposit_address}"))
    }

    pub fn withdraw_order(order: &Pubkey) -> Self {
        let p = Self::WITHDRAW_ORDER_PREFIX;
        Self(format!("{p}:{order}"))
    }

    pub fn user_withdraw_orders(owner: &Pubkey) -> Self {
        let p = Self::USER_WITHDRAW_ORDERS_PREFIX;
        Self(format!("{p}:{owner}"))
    }

    pub fn spend_limits_order(order: &Pubkey) -> Self {
        let p = Self::SPEND_LIMITS_ORDER_PREFIX;
        Self(format!("{p}:{order}"))
    }

    pub fn user_spend_limits_orders(owner: &Pubkey) -> Self {
        let p = Self::USER_SPEND_LIMITS_ORDERS_PREFIX;
        Self(format!("{p}:{owner}"))
    }

    pub fn token_account(token_account: &Pubkey) -> Self {
        let p = Self::TOKEN_ACCOUNT_PREFIX;
        Self(format!("{p}:{token_account}"))
    }

    // ── Price / Oracle keys ───────────────────────────────────────────

    pub fn price(market_index: u16) -> Self {
        let p = Self::PRICE_PREFIX;
        Self(format!("{p}:{market_index}"))
    }

    /// Oracle pubkey → market_index mapping.
    pub fn oracle(oracle: &Pubkey) -> Self {
        let p = Self::ORACLE_PREFIX;
        Self(format!("{p}:{oracle}"))
    }

    /// Full oracle snapshot cache by market index.
    pub fn oracle_cache(market_index: u16) -> Self {
        let p = Self::ORACLE_CACHE_PREFIX;
        Self(format!("{p}:{market_index}"))
    }

    // ── User keys ─────────────────────────────────────────────────────

    pub fn registered_wallets() -> Self {
        Self(Self::REGISTERED_WALLETS.to_string())
    }

    /// Lookup user by Solana address: `"user:solana_address:{owner}"`.
    pub fn user_solana_address(owner: &str) -> Self {
        let p = Self::USER_SOLANA_ADDRESS_PREFIX;
        Self(format!("{p}:{owner}"))
    }

    /// Lookup user by provider (Privy) ID: `"user:provider:{user_id}"`.
    pub fn user_provider(user_id: &str) -> Self {
        let p = Self::USER_PROVIDER_PREFIX;
        Self(format!("{p}:{user_id}"))
    }

    /// Pending deposit key: `"pending_deposits:{owner}:{mint}"`.
    pub fn pending_deposit(owner: &str, mint: &str) -> Self {
        let p = Self::PENDING_DEPOSIT_PREFIX;
        Self(format!("{p}:{owner}:{mint}"))
    }
}

// ── Trait implementations ─────────────────────────────────────────────

impl fmt::Display for RedisKey {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

impl ToRedisArgs for RedisKey {
    fn write_redis_args<W: ?Sized + redis::RedisWrite>(&self, out: &mut W) {
        out.write_arg(self.0.as_bytes());
    }
}

impl AsRef<str> for RedisKey {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::str::FromStr;

    #[test]
    fn vault_key_format() {
        let pk = Pubkey::from_str("11111111111111111111111111111111").unwrap();
        let key = RedisKey::vault(&pk);
        assert_eq!(
            key.to_string(),
            "account:pyra:vault:11111111111111111111111111111111"
        );
    }

    #[test]
    fn deposit_address_spl_key_format() {
        let addr = Pubkey::from_str("11111111111111111111111111111111").unwrap();
        let mint = Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap();
        let key = RedisKey::deposit_address_spl(&addr, &mint);
        assert!(key.to_string().starts_with("account:pyra:deposit_address:"));
        assert!(key
            .to_string()
            .contains("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"));
    }

    #[test]
    fn deposit_address_sol_key_format() {
        let addr = Pubkey::from_str("11111111111111111111111111111111").unwrap();
        let key = RedisKey::deposit_address_sol(&addr);
        assert!(key.to_string().ends_with(":native"));
    }

    #[test]
    fn price_key_format() {
        let key = RedisKey::price(0);
        assert_eq!(key.to_string(), "price:0");
    }

    #[test]
    fn pattern_format() {
        let pat = RedisKey::pattern(RedisKey::VAULT_PREFIX);
        assert_eq!(pat, "account:pyra:vault:*");
    }

    #[test]
    fn user_keys_format() {
        let key = RedisKey::user_solana_address("abc123");
        assert_eq!(key.to_string(), "user:solana_address:abc123");

        let key = RedisKey::user_provider("did:privy:abc");
        assert_eq!(key.to_string(), "user:provider:did:privy:abc");
    }

    #[test]
    fn pending_deposit_format() {
        let key = RedisKey::pending_deposit("owner1", "mint1");
        assert_eq!(key.to_string(), "pending_deposits:owner1:mint1");
    }

    #[test]
    fn redis_key_as_ref() {
        let key = RedisKey::price(1);
        let s: &str = key.as_ref();
        assert_eq!(s, "price:1");
    }
}