Skip to main content

pyra_redis/
keys.rs

1use core::fmt;
2
3use redis::ToRedisArgs;
4use solana_pubkey::Pubkey;
5
6/// A typed Redis key with factory methods for all Pyra key patterns.
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8pub struct RedisKey(String);
9
10impl RedisKey {
11    /// Create a key from a raw string.
12    pub fn from_string(s: String) -> Self {
13        Self(s)
14    }
15
16    /// Return a reference to the inner string.
17    pub fn as_str(&self) -> &str {
18        &self.0
19    }
20
21    // ── Shared prefixes ──────────────────────────────────────────────
22
23    pub const VAULT_PREFIX: &'static str = "account:pyra:vault";
24    pub const WITHDRAW_ORDER_PREFIX: &'static str = "account:pyra:withdraw_order";
25    pub const SPEND_LIMITS_ORDER_PREFIX: &'static str = "account:pyra:spend_limits_order";
26    pub const DEPOSIT_ADDRESS_PREFIX: &'static str = "account:pyra:deposit_address";
27    pub const DEPOSIT_ADDRESS_OWNER_PREFIX: &'static str = "reverse:deposit_address";
28    pub const USER_WITHDRAW_ORDERS_PREFIX: &'static str = "user:orders:withdraws";
29    pub const USER_SPEND_LIMITS_ORDERS_PREFIX: &'static str = "user:orders:spend_limits";
30    pub const TOKEN_ACCOUNT_PREFIX: &'static str = "account:token";
31    pub const PRICE_PREFIX: &'static str = "price";
32    pub const ORACLE_PREFIX: &'static str = "oracle";
33    pub const ORACLE_CACHE_PREFIX: &'static str = "account:oracle";
34    pub const REGISTERED_WALLETS: &'static str = "registered_wallets";
35    pub const REGISTERED_WALLETS_CHANGES: &'static str = "registered_wallets:changes";
36    pub const USER_SOLANA_ADDRESS_PREFIX: &'static str = "user:solana_address";
37    pub const USER_PROVIDER_PREFIX: &'static str = "user:provider";
38    pub const PENDING_DEPOSIT_PREFIX: &'static str = "pending_deposits";
39
40    // ── Glob pattern ──────────────────────────────────────────────────
41
42    /// Build a SCAN/KEYS glob pattern: `"{prefix}:*"`.
43    pub fn pattern(prefix: &str) -> String {
44        format!("{prefix}:*")
45    }
46
47    // ── Account keys ──────────────────────────────────────────────────
48
49    pub fn vault(vault: &Pubkey) -> Self {
50        let p = Self::VAULT_PREFIX;
51        Self(format!("{p}:{vault}"))
52    }
53
54    pub fn deposit_address_spl(deposit_address: &Pubkey, mint: &Pubkey) -> Self {
55        let p = Self::DEPOSIT_ADDRESS_PREFIX;
56        Self(format!("{p}:{deposit_address}:{mint}"))
57    }
58
59    pub fn deposit_address_sol(deposit_address: &Pubkey) -> Self {
60        let p = Self::DEPOSIT_ADDRESS_PREFIX;
61        Self(format!("{p}:{deposit_address}:native"))
62    }
63
64    /// Reverse mapping: deposit address → owner pubkey.
65    pub fn deposit_address_owner(deposit_address: &Pubkey) -> Self {
66        let p = Self::DEPOSIT_ADDRESS_OWNER_PREFIX;
67        Self(format!("{p}:{deposit_address}"))
68    }
69
70    pub fn withdraw_order(order: &Pubkey) -> Self {
71        let p = Self::WITHDRAW_ORDER_PREFIX;
72        Self(format!("{p}:{order}"))
73    }
74
75    pub fn user_withdraw_orders(owner: &Pubkey) -> Self {
76        let p = Self::USER_WITHDRAW_ORDERS_PREFIX;
77        Self(format!("{p}:{owner}"))
78    }
79
80    pub fn spend_limits_order(order: &Pubkey) -> Self {
81        let p = Self::SPEND_LIMITS_ORDER_PREFIX;
82        Self(format!("{p}:{order}"))
83    }
84
85    pub fn user_spend_limits_orders(owner: &Pubkey) -> Self {
86        let p = Self::USER_SPEND_LIMITS_ORDERS_PREFIX;
87        Self(format!("{p}:{owner}"))
88    }
89
90    pub fn token_account(token_account: &Pubkey) -> Self {
91        let p = Self::TOKEN_ACCOUNT_PREFIX;
92        Self(format!("{p}:{token_account}"))
93    }
94
95    // ── Price / Oracle keys ───────────────────────────────────────────
96
97    pub fn price(asset_id: pyra_tokens::AssetId) -> Self {
98        let p = Self::PRICE_PREFIX;
99        Self(format!("{p}:{asset_id}"))
100    }
101
102    /// Oracle pubkey → market_index mapping.
103    pub fn oracle(oracle: &Pubkey) -> Self {
104        let p = Self::ORACLE_PREFIX;
105        Self(format!("{p}:{oracle}"))
106    }
107
108    /// Full oracle snapshot cache by asset ID.
109    pub fn oracle_cache(asset_id: pyra_tokens::AssetId) -> Self {
110        let p = Self::ORACLE_CACHE_PREFIX;
111        Self(format!("{p}:{asset_id}"))
112    }
113
114    // ── User keys ─────────────────────────────────────────────────────
115
116    pub fn registered_wallets() -> Self {
117        Self(Self::REGISTERED_WALLETS.to_string())
118    }
119
120    /// Lookup user by Solana address: `"user:solana_address:{owner}"`.
121    pub fn user_solana_address(owner: &str) -> Self {
122        let p = Self::USER_SOLANA_ADDRESS_PREFIX;
123        Self(format!("{p}:{owner}"))
124    }
125
126    /// Lookup user by provider (Privy) ID: `"user:provider:{user_id}"`.
127    pub fn user_provider(user_id: &str) -> Self {
128        let p = Self::USER_PROVIDER_PREFIX;
129        Self(format!("{p}:{user_id}"))
130    }
131
132    /// Pending deposit key: `"pending_deposits:{owner}:{mint}"`.
133    pub fn pending_deposit(owner: &str, mint: &str) -> Self {
134        let p = Self::PENDING_DEPOSIT_PREFIX;
135        Self(format!("{p}:{owner}:{mint}"))
136    }
137}
138
139// ── Trait implementations ─────────────────────────────────────────────
140
141impl fmt::Display for RedisKey {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        f.write_str(&self.0)
144    }
145}
146
147impl ToRedisArgs for RedisKey {
148    fn write_redis_args<W: ?Sized + redis::RedisWrite>(&self, out: &mut W) {
149        out.write_arg(self.0.as_bytes());
150    }
151}
152
153impl AsRef<str> for RedisKey {
154    fn as_ref(&self) -> &str {
155        &self.0
156    }
157}
158
159#[cfg(test)]
160#[allow(
161    clippy::allow_attributes,
162    clippy::allow_attributes_without_reason,
163    clippy::unwrap_used,
164    clippy::expect_used,
165    clippy::panic,
166    clippy::arithmetic_side_effects
167)]
168mod tests {
169    use super::*;
170    use pyra_tokens::AssetId;
171    use std::str::FromStr;
172
173    #[test]
174    fn vault_key_format() {
175        let pk = Pubkey::from_str("11111111111111111111111111111111").unwrap();
176        let key = RedisKey::vault(&pk);
177        assert_eq!(
178            key.to_string(),
179            "account:pyra:vault:11111111111111111111111111111111"
180        );
181    }
182
183    #[test]
184    fn deposit_address_spl_key_format() {
185        let addr = Pubkey::from_str("11111111111111111111111111111111").unwrap();
186        let mint = Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap();
187        let key = RedisKey::deposit_address_spl(&addr, &mint);
188        assert!(key.to_string().starts_with("account:pyra:deposit_address:"));
189        assert!(key
190            .to_string()
191            .contains("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"));
192    }
193
194    #[test]
195    fn deposit_address_sol_key_format() {
196        let addr = Pubkey::from_str("11111111111111111111111111111111").unwrap();
197        let key = RedisKey::deposit_address_sol(&addr);
198        assert!(key.to_string().ends_with(":native"));
199    }
200
201    #[test]
202    fn price_key_format() {
203        let key = RedisKey::price(AssetId::USDC);
204        assert_eq!(key.to_string(), "price:0");
205    }
206
207    #[test]
208    fn price_key_uses_asset_id() {
209        // PYUSD: asset_id 6 (drift_market_index 22 — NOT used in key)
210        let key = RedisKey::price(AssetId::PYUSD);
211        assert_eq!(key.to_string(), "price:6");
212    }
213
214    #[test]
215    fn oracle_cache_key_uses_asset_id() {
216        // PYUSD: asset_id 6
217        let key = RedisKey::oracle_cache(AssetId::PYUSD);
218        assert_eq!(key.to_string(), "account:oracle:6");
219    }
220
221    #[test]
222    fn pattern_format() {
223        let pat = RedisKey::pattern(RedisKey::VAULT_PREFIX);
224        assert_eq!(pat, "account:pyra:vault:*");
225    }
226
227    #[test]
228    fn user_keys_format() {
229        let key = RedisKey::user_solana_address("abc123");
230        assert_eq!(key.to_string(), "user:solana_address:abc123");
231
232        let key = RedisKey::user_provider("did:privy:abc");
233        assert_eq!(key.to_string(), "user:provider:did:privy:abc");
234    }
235
236    #[test]
237    fn pending_deposit_format() {
238        let key = RedisKey::pending_deposit("owner1", "mint1");
239        assert_eq!(key.to_string(), "pending_deposits:owner1:mint1");
240    }
241
242    #[test]
243    fn redis_key_as_ref() {
244        let key = RedisKey::price(AssetId::WSOL);
245        let s: &str = key.as_ref();
246        assert_eq!(s, "price:1");
247    }
248}