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#[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 pub const USDC: Self = Self(0);
37 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 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 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 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 pub const fn value(self) -> u16 {
73 self.0
74 }
75
76 pub fn token(self) -> &'static Token {
78 &SUPPORTED_TOKENS[self.0 as usize]
79 }
80
81 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 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 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 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 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}