Skip to main content

pyra_tokens/
lib.rs

1use core::fmt;
2use solana_pubkey::{pubkey, Pubkey};
3
4mod macros;
5
6pub type PythFeedId = [u8; 32];
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum TokenError {
10    UnknownAssetId(u16),
11}
12
13impl fmt::Display for TokenError {
14    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
15        match self {
16            Self::UnknownAssetId(id) => write!(f, "no token with asset_id {id}"),
17        }
18    }
19}
20
21impl std::error::Error for TokenError {}
22
23/// A validated asset ID guaranteed to reference a supported token.
24///
25/// Construct via `AssetId::new()` or `TryFrom<u16>`. Once constructed,
26/// lookups like `token()` and `drift_market_index()` are infallible
27/// for the "unknown asset" case.
28#[derive(
29    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
30)]
31#[serde(try_from = "u16", into = "u16")]
32pub struct AssetId(u16);
33
34impl AssetId {
35    // Cash
36    pub const USDC: Self = Self(0);
37    // Crypto (supported)
38    pub const WSOL: Self = Self(1);
39    pub const JITOSOL: Self = Self(2);
40    pub const CBBTC: Self = Self(3);
41    pub const WETH: Self = Self(4);
42    // Crypto (legacy)
43    pub const USDT: Self = Self(5);
44    pub const PYUSD: Self = Self(6);
45    pub const USDS: Self = Self(7);
46    pub const WBTC: Self = Self(8);
47    pub const JLP: Self = Self(9);
48    pub const BSOL: Self = Self(10);
49    pub const BONK: Self = Self(11);
50    pub const JUP: Self = Self(12);
51    pub const ZBTC: Self = Self(13);
52    pub const SYRUP_USDC: Self = Self(14);
53    // xStocks
54    pub const SPYX: Self = Self(15);
55    pub const QQQX: Self = Self(16);
56    pub const TSLAX: Self = Self(17);
57    pub const NVDAX: Self = Self(18);
58    pub const GOOGLX: Self = Self(19);
59    pub const AAPLX: Self = Self(20);
60
61    /// Create a validated `AssetId`. Returns `Err` if the ID doesn't
62    /// match a supported token.
63    pub fn new(id: u16) -> Result<Self, TokenError> {
64        if (id as usize) < SUPPORTED_TOKENS.len() {
65            Ok(Self(id))
66        } else {
67            Err(TokenError::UnknownAssetId(id))
68        }
69    }
70
71    /// The raw `u16` value.
72    pub const fn value(self) -> u16 {
73        self.0
74    }
75
76    /// The token this ID refers to. Always valid — guaranteed by construction.
77    pub fn token(self) -> &'static Token {
78        &SUPPORTED_TOKENS[self.0 as usize]
79    }
80
81    /// The Drift market index for this asset, if it has one.
82    pub fn drift_market_index(self) -> Option<u16> {
83        self.token().drift_market_index
84    }
85}
86
87impl TryFrom<u16> for AssetId {
88    type Error = TokenError;
89
90    fn try_from(id: u16) -> Result<Self, Self::Error> {
91        Self::new(id)
92    }
93}
94
95impl From<AssetId> for u16 {
96    fn from(id: AssetId) -> u16 {
97        id.0
98    }
99}
100
101impl fmt::Display for AssetId {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        write!(f, "{}", self.0)
104    }
105}
106
107pub const TOKEN_PROGRAM_ID: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
108pub const TOKEN_2022_PROGRAM_ID: Pubkey = pubkey!("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb");
109
110pub const PYRA_PROGRAM_ID: Pubkey = pubkey!("6JjHXLheGSNvvexgzMthEcgjkcirDrGduc3HAKB2P1v2");
111
112pub const USDC: Token = SUPPORTED_TOKENS[0];
113pub const WSOL: Token = SUPPORTED_TOKENS[1];
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub struct Token {
117    pub asset_id: AssetId,
118    pub name: &'static str,
119    pub mint: Pubkey,
120    pub token_program: Pubkey,
121    pub decimals: u8,
122    pub is_usd_stablecoin: bool,
123    pub is_borrowable: bool,
124    pub drift_market_index: Option<u16>,
125    pub kamino_reserve: Option<Pubkey>,
126}
127
128impl Token {
129    pub fn find_by_asset_id(id: AssetId) -> &'static Self {
130        &SUPPORTED_TOKENS[id.0 as usize]
131    }
132
133    pub fn find_by_mint(mint: &Pubkey) -> Option<&'static Self> {
134        SUPPORTED_TOKENS.iter().find(|token| token.mint == *mint)
135    }
136
137    pub fn find_by_drift_market_index(drift_market_index: u16) -> Option<&'static Self> {
138        SUPPORTED_TOKENS_DRIFT
139            .iter()
140            .find(|token| token.drift_market_index == Some(drift_market_index))
141    }
142
143    pub fn find_by_kamino_reserve(reserve: &Pubkey) -> Option<&'static Self> {
144        SUPPORTED_TOKENS_KAMINO
145            .iter()
146            .find(|token| token.kamino_reserve == Some(*reserve))
147    }
148}
149
150pub const SUPPORTED_TOKENS: [Token; 21] = [
151    // Cash
152    Token {
153        asset_id: AssetId(0),
154        name: "USDC",
155        mint: pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"),
156        token_program: TOKEN_PROGRAM_ID,
157        decimals: 6,
158        is_usd_stablecoin: true,
159        is_borrowable: true,
160        drift_market_index: Some(0),
161        kamino_reserve: Some(pubkey!("97zoywd8mPZsGTg8q1wdD2Wgkdrs2tqusp1Qqcxbyj7E")),
162    },
163    // Crypto (supported)
164    Token {
165        asset_id: AssetId(1),
166        name: "wSOL",
167        mint: pubkey!("So11111111111111111111111111111111111111112"),
168        token_program: TOKEN_PROGRAM_ID,
169        decimals: 9,
170        is_usd_stablecoin: false,
171        is_borrowable: false,
172        drift_market_index: Some(1),
173        kamino_reserve: None,
174    },
175    Token {
176        asset_id: AssetId(2),
177        name: "JitoSOL",
178        mint: pubkey!("J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn"),
179        token_program: TOKEN_PROGRAM_ID,
180        decimals: 9,
181        is_usd_stablecoin: false,
182        is_borrowable: false,
183        drift_market_index: Some(6),
184        kamino_reserve: None,
185    },
186    Token {
187        asset_id: AssetId(3),
188        name: "cbBTC",
189        mint: pubkey!("cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij"),
190        token_program: TOKEN_PROGRAM_ID,
191        decimals: 8,
192        is_usd_stablecoin: false,
193        is_borrowable: false,
194        drift_market_index: Some(27),
195        kamino_reserve: None,
196    },
197    Token {
198        asset_id: AssetId(4),
199        name: "wETH",
200        mint: pubkey!("7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs"),
201        token_program: TOKEN_PROGRAM_ID,
202        decimals: 8,
203        is_usd_stablecoin: false,
204        is_borrowable: false,
205        drift_market_index: Some(4),
206        kamino_reserve: None,
207    },
208    // Crypto (legacy)
209    Token {
210        asset_id: AssetId(5),
211        name: "USDT",
212        mint: pubkey!("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"),
213        token_program: TOKEN_PROGRAM_ID,
214        decimals: 6,
215        is_usd_stablecoin: true,
216        is_borrowable: false,
217        drift_market_index: Some(5),
218        kamino_reserve: None,
219    },
220    Token {
221        asset_id: AssetId(6),
222        name: "PYUSD",
223        mint: pubkey!("2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo"),
224        token_program: TOKEN_2022_PROGRAM_ID,
225        decimals: 6,
226        is_usd_stablecoin: true,
227        is_borrowable: false,
228        drift_market_index: Some(22),
229        kamino_reserve: None,
230    },
231    Token {
232        asset_id: AssetId(7),
233        name: "USDS",
234        mint: pubkey!("USDSwr9ApdHk5bvJKMjzff41FfuX8bSxdKcR81vTwcA"),
235        token_program: TOKEN_PROGRAM_ID,
236        decimals: 6,
237        is_usd_stablecoin: true,
238        is_borrowable: false,
239        drift_market_index: Some(28),
240        kamino_reserve: None,
241    },
242    Token {
243        asset_id: AssetId(8),
244        name: "wBTC",
245        mint: pubkey!("3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh"),
246        token_program: TOKEN_PROGRAM_ID,
247        decimals: 8,
248        is_usd_stablecoin: false,
249        is_borrowable: false,
250        drift_market_index: Some(3),
251        kamino_reserve: None,
252    },
253    Token {
254        asset_id: AssetId(9),
255        name: "JLP",
256        mint: pubkey!("27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4"),
257        token_program: TOKEN_PROGRAM_ID,
258        decimals: 6,
259        is_usd_stablecoin: false,
260        is_borrowable: false,
261        drift_market_index: Some(19),
262        kamino_reserve: None,
263    },
264    Token {
265        asset_id: AssetId(10),
266        name: "bSOL",
267        mint: pubkey!("bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1"),
268        token_program: TOKEN_PROGRAM_ID,
269        decimals: 9,
270        is_usd_stablecoin: false,
271        is_borrowable: false,
272        drift_market_index: Some(8),
273        kamino_reserve: None,
274    },
275    Token {
276        asset_id: AssetId(11),
277        name: "BONK",
278        mint: pubkey!("DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"),
279        token_program: TOKEN_PROGRAM_ID,
280        decimals: 5,
281        is_usd_stablecoin: false,
282        is_borrowable: false,
283        drift_market_index: Some(32),
284        kamino_reserve: None,
285    },
286    Token {
287        asset_id: AssetId(12),
288        name: "JUP",
289        mint: pubkey!("JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN"),
290        token_program: TOKEN_PROGRAM_ID,
291        decimals: 6,
292        is_usd_stablecoin: false,
293        is_borrowable: false,
294        drift_market_index: Some(11),
295        kamino_reserve: None,
296    },
297    Token {
298        asset_id: AssetId(13),
299        name: "zBTC",
300        mint: pubkey!("zBTCug3er3tLyffELcvDNrKkCymbPWysGcWihESYfLg"),
301        token_program: TOKEN_PROGRAM_ID,
302        decimals: 8,
303        is_usd_stablecoin: false,
304        is_borrowable: false,
305        drift_market_index: Some(45),
306        kamino_reserve: None,
307    },
308    Token {
309        asset_id: AssetId(14),
310        name: "syrupUSDC",
311        mint: pubkey!("AvZZF1YaZDziPY2RCK4oJrRVrbN3mTD9NL24hPeaZeUj"),
312        token_program: TOKEN_PROGRAM_ID,
313        decimals: 6,
314        is_usd_stablecoin: false,
315        is_borrowable: false,
316        drift_market_index: Some(57),
317        kamino_reserve: None,
318    },
319    // xStocks
320    Token {
321        asset_id: AssetId(15),
322        name: "SPYx",
323        mint: pubkey!("XsoCS1TfEyfFhfvj8EtZ528L3CaKBDBRqRapnBbDF2W"),
324        token_program: TOKEN_2022_PROGRAM_ID,
325        decimals: 8,
326        is_usd_stablecoin: false,
327        is_borrowable: false,
328        drift_market_index: None,
329        kamino_reserve: Some(pubkey!("UvXjBuC7YZYaGB9Rn1PpBD1GySmjzunXgE8Zev9ua8d")),
330    },
331    Token {
332        asset_id: AssetId(16),
333        name: "QQQx",
334        mint: pubkey!("Xs8S1uUs1zvS2p7iwtsG3b6fkhpvmwz4GYU3gWAmWHZ"),
335        token_program: TOKEN_2022_PROGRAM_ID,
336        decimals: 8,
337        is_usd_stablecoin: false,
338        is_borrowable: false,
339        drift_market_index: None,
340        kamino_reserve: Some(pubkey!("2jerdAXR8r2B6z3P7P6VgSiePQX7wqcpbEqdDbm8mgeB")),
341    },
342    Token {
343        asset_id: AssetId(17),
344        name: "TSLAx",
345        mint: pubkey!("XsDoVfqeBukxuZHWhdvWHBhgEHjGNst4MLodqsJHzoB"),
346        token_program: TOKEN_2022_PROGRAM_ID,
347        decimals: 8,
348        is_usd_stablecoin: false,
349        is_borrowable: false,
350        drift_market_index: None,
351        kamino_reserve: Some(pubkey!("5iTiczqgUegqA3PpoNpotizMbY9n1sRWr3oL6igKvWuf")),
352    },
353    Token {
354        asset_id: AssetId(18),
355        name: "NVDAx",
356        mint: pubkey!("Xsc9qvGR1efVDFGLrVsmkzv3qi45LTBjeUKSPmx9qEh"),
357        token_program: TOKEN_2022_PROGRAM_ID,
358        decimals: 8,
359        is_usd_stablecoin: false,
360        is_borrowable: false,
361        drift_market_index: None,
362        kamino_reserve: Some(pubkey!("7B66Az3tJhAo4bLkX8PzTixQ9ZGyHkkjxfVLhF26sP5q")),
363    },
364    Token {
365        asset_id: AssetId(19),
366        name: "GOOGLx",
367        mint: pubkey!("XsCPL9dNWBMvFtTmwcCA5v3xWPSMEBCszbQdiLLq6aN"),
368        token_program: TOKEN_2022_PROGRAM_ID,
369        decimals: 8,
370        is_usd_stablecoin: false,
371        is_borrowable: false,
372        drift_market_index: None,
373        kamino_reserve: Some(pubkey!("4wg6rEkGgHaEuxMduP46C1xFZ24Lnp5YgdNkZAHxFzsN")),
374    },
375    Token {
376        asset_id: AssetId(20),
377        name: "AAPLx",
378        mint: pubkey!("XsbEhLAtcf6HdfpFZ5xEMdqW8nfAvcsP5bdudRLJzJp"),
379        token_program: TOKEN_2022_PROGRAM_ID,
380        decimals: 8,
381        is_usd_stablecoin: false,
382        is_borrowable: false,
383        drift_market_index: None,
384        kamino_reserve: Some(pubkey!("CKJbqakbPGyhziowm19LPYz636UszuezfkitmpRtcLSH")),
385    },
386];
387
388filter_supported_tokens!(pub SUPPORTED_TOKENS_DRIFT, drift_market_index);
389filter_supported_tokens!(pub SUPPORTED_TOKENS_KAMINO, kamino_reserve);
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394
395    #[test]
396    fn asset_id_matches_array_position() {
397        for (i, token) in SUPPORTED_TOKENS.iter().enumerate() {
398            assert_eq!(
399                token.asset_id.value(),
400                i as u16,
401                "SUPPORTED_TOKENS[{i}] has asset_id {} — must match array position",
402                token.asset_id,
403            );
404        }
405    }
406
407    #[test]
408    fn named_constants_match_tokens() {
409        assert_eq!(AssetId::USDC.token().name, "USDC");
410        assert_eq!(AssetId::WSOL.token().name, "wSOL");
411        assert_eq!(AssetId::WETH.token().name, "wETH");
412        assert_eq!(AssetId::SYRUP_USDC.token().name, "syrupUSDC");
413    }
414
415    #[test]
416    fn drift_market_index_returns_option() {
417        assert_eq!(AssetId::USDC.drift_market_index(), Some(0));
418        assert_eq!(AssetId::PYUSD.drift_market_index(), Some(22));
419    }
420
421    #[test]
422    fn invalid_asset_id_returns_error() {
423        assert!(AssetId::new(999).is_err());
424    }
425}