1#[allow(deprecated)]
12use solana_sdk::{
13 instruction::{AccountMeta, Instruction},
14 pubkey::Pubkey,
15 system_program,
16 sysvar,
17};
18use sha2::{Sha256, Digest};
19use std::collections::HashMap;
20use std::sync::RwLock;
21
22use crate::types::{VAEA_PROGRAM_ID, VAEA_API_URL, FlashTier, TokenCapacity};
23
24#[derive(Debug, Clone)]
30pub struct TokenEntry {
31 pub symbol: String,
32 pub mint: Pubkey,
33 pub name: String,
34 pub decimals: u8,
35 pub max_amount: f64,
36 pub max_amount_usd: f64,
37 pub source_protocol: String,
38 pub route_type: String,
39 pub status: String,
40 pub logo_uri: Option<String>,
41}
42
43lazy_static::lazy_static! {
48 static ref PROGRAM_ID: Pubkey = VAEA_PROGRAM_ID.parse().unwrap();
49 static ref DISC_BEGIN_FLASH: [u8; 8] = anchor_discriminator("begin_flash");
50 static ref DISC_END_FLASH: [u8; 8] = anchor_discriminator("end_flash");
51 static ref CONFIG_PDA: Pubkey = derive_config();
52 static ref FEE_VAULT_PDA: Pubkey = derive_fee_vault();
53
54 static ref TOKEN_REGISTRY: RwLock<HashMap<String, TokenEntry>> = {
57 let mut m = HashMap::new();
58 for (sym, mint_str, name, dec) in FALLBACK_TOKENS.iter() {
59 if let Ok(mint) = mint_str.parse::<Pubkey>() {
60 let entry = TokenEntry {
61 symbol: sym.to_string(),
62 mint,
63 name: name.to_string(),
64 decimals: *dec,
65 max_amount: 0.0,
66 max_amount_usd: 0.0,
67 source_protocol: "unknown".to_string(),
68 route_type: "direct".to_string(),
69 status: "unknown".to_string(),
70 logo_uri: None,
71 };
72 m.insert(sym.to_lowercase(), entry.clone());
73 m.insert(mint_str.to_string(), entry);
74 }
75 }
76 RwLock::new(m)
77 };
78
79 static ref REGISTRY_SYNCED: RwLock<bool> = RwLock::new(false);
80}
81
82const FALLBACK_TOKENS: [(&str, &str, &str, u8); 12] = [
84 ("SOL", "So11111111111111111111111111111111111111112", "Solana", 9),
85 ("USDC", "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "USD Coin", 6),
86 ("USDT", "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", "Tether USD", 6),
87 ("JitoSOL", "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", "Jito Staked SOL", 9),
88 ("JupSOL", "jupSoLaHXQiZZTSfEWMTRRgpnyFm8f6sZdosWBjx93v", "Jupiter SOL", 9),
89 ("JUP", "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", "Jupiter", 6),
90 ("JLP", "27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4", "Jupiter LP", 6),
91 ("cbBTC", "cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij", "Coinbase BTC", 8),
92 ("mSOL", "mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So", "Marinade SOL", 9),
93 ("bSOL", "bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1", "Blaze SOL", 9),
94 ("INF", "5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm", "Infinity", 9),
95 ("laineSOL", "LAinEtNLgpmCP9Rvsf5Hn8W6EhNiKLZQti1xfWMLy6X", "Laine SOL", 9),
96];
97
98const BLACKLIST: [&str; 4] = ["EURCV", "FBTC", "USDCV", "wstUSR"];
100
101fn is_valid_token(t: &TokenCapacity) -> bool {
106 let sym = &t.symbol;
107 if FALLBACK_TOKENS.iter().any(|(s, _, _, _)| s.eq_ignore_ascii_case(sym)) {
109 return true;
110 }
111 if BLACKLIST.contains(&sym.as_str()) { return false; }
113 if t.name.starts_with("SPL Single Pool") { return false; }
115 if sym.len() > 2 && sym.starts_with('k') && sym.contains('-') {
117 let after_k = &sym[1..];
118 if after_k.chars().next().map_or(false, |c| c.is_uppercase()) {
119 return false;
120 }
121 }
122 if t.name.is_empty() || t.name == "Unknown Token" { return false; }
124 if sym.len() >= 12 || sym.contains("...") { return false; }
126 true
127}
128
129pub fn update_registry_from_capacity(tokens: &[TokenCapacity]) {
136 let mut reg = TOKEN_REGISTRY.write().unwrap();
137 for t in tokens {
138 if !is_valid_token(t) { continue; }
139 if let Ok(mint) = t.mint.parse::<Pubkey>() {
140 let entry = TokenEntry {
141 symbol: t.symbol.clone(),
142 mint,
143 name: if t.name.is_empty() { t.symbol.clone() } else { t.name.clone() },
144 decimals: t.decimals,
145 max_amount: t.max_amount,
146 max_amount_usd: t.max_amount_usd,
147 source_protocol: t.source_protocol.clone(),
148 route_type: t.route_type.clone(),
149 status: t.status.clone(),
150 logo_uri: None,
151 };
152 reg.insert(t.symbol.to_lowercase(), entry.clone());
153 reg.insert(t.mint.clone(), entry);
154 }
155 }
156 *REGISTRY_SYNCED.write().unwrap() = true;
157}
158
159pub async fn sync_registry(api_url: Option<&str>) -> usize {
161 let url = api_url.unwrap_or(VAEA_API_URL);
162 match reqwest::get(&format!("{}/v1/capacity", url)).await {
163 Ok(res) => {
164 if let Ok(data) = res.json::<crate::types::CapacityResponse>().await {
165 update_registry_from_capacity(&data.tokens);
166 }
167 }
168 Err(_) => {}
169 }
170 let reg = TOKEN_REGISTRY.read().unwrap();
171 let mut seen = std::collections::HashSet::new();
173 for entry in reg.values() { seen.insert(entry.symbol.clone()); }
174 seen.len()
175}
176
177pub fn is_registry_synced() -> bool {
179 *REGISTRY_SYNCED.read().unwrap()
180}
181
182pub fn get_token(token: &str) -> Option<TokenEntry> {
184 let reg = TOKEN_REGISTRY.read().unwrap();
185 reg.get(&token.to_lowercase()).cloned()
186 .or_else(|| reg.get(token).cloned())
187}
188
189pub fn get_all_tokens() -> Vec<TokenEntry> {
191 let reg = TOKEN_REGISTRY.read().unwrap();
192 let mut seen = std::collections::HashSet::new();
193 let mut result: Vec<TokenEntry> = Vec::new();
194 for entry in reg.values() {
195 if seen.insert(entry.symbol.clone()) {
196 result.push(entry.clone());
197 }
198 }
199 result.sort_by(|a, b| b.max_amount_usd.partial_cmp(&a.max_amount_usd).unwrap_or(std::cmp::Ordering::Equal));
200 result
201}
202
203fn anchor_discriminator(name: &str) -> [u8; 8] {
208 let mut hasher = Sha256::new();
209 hasher.update(format!("global:{}", name));
210 let result = hasher.finalize();
211 let mut disc = [0u8; 8];
212 disc.copy_from_slice(&result[..8]);
213 disc
214}
215
216fn derive_flash_state(payer: &Pubkey, token_mint: &Pubkey) -> Pubkey {
217 Pubkey::find_program_address(
218 &[b"flash", payer.as_ref(), token_mint.as_ref()],
219 &PROGRAM_ID,
220 ).0
221}
222
223fn derive_config() -> Pubkey {
224 Pubkey::find_program_address(&[b"config"], &PROGRAM_ID).0
225}
226
227fn derive_fee_vault() -> Pubkey {
228 Pubkey::find_program_address(&[b"fee_vault"], &PROGRAM_ID).0
229}
230
231pub enum TokenId {
237 Symbol(String),
238 Mint(Pubkey),
239}
240
241impl From<&str> for TokenId {
242 fn from(s: &str) -> Self { TokenId::Symbol(s.to_string()) }
243}
244impl From<Pubkey> for TokenId {
245 fn from(p: Pubkey) -> Self { TokenId::Mint(p) }
246}
247
248pub struct LocalBuildParams {
250 pub payer: Pubkey,
251 pub token: TokenId,
252 pub amount: f64,
253 pub tier: FlashTier,
254}
255
256pub struct LocalBuildResult {
258 pub begin_flash: Instruction,
259 pub end_flash: Instruction,
260 pub token_mint: Pubkey,
261 pub decimals: u8,
262 pub expected_fee_native: u64,
263}
264
265pub fn local_build(params: LocalBuildParams) -> Result<LocalBuildResult, String> {
270 let (token_mint, decimals) = match ¶ms.token {
271 TokenId::Symbol(sym) => {
272 let entry = get_token(sym)
273 .ok_or_else(|| {
274 let available: Vec<String> = get_all_tokens().iter().map(|t| t.symbol.clone()).collect();
275 format!(
276 "Unknown token: {}. Available: {}. Use TokenId::Mint for unlisted tokens, or call sync_registry() first.",
277 sym, available.join(", ")
278 )
279 })?;
280 (entry.mint, entry.decimals)
281 }
282 TokenId::Mint(mint) => {
283 let entry = get_token(&mint.to_string());
284 let decimals = entry.map(|e| e.decimals).unwrap_or(9);
285 (*mint, decimals)
286 }
287 };
288
289 let fee_bps = params.tier.fee_bps();
290 let decimals_factor = 10u64.pow(decimals as u32);
291 let amount_native = (params.amount * decimals_factor as f64) as u64;
292 let expected_fee_native = (amount_native as u128)
293 .checked_mul(fee_bps as u128)
294 .and_then(|v| v.checked_div(10_000))
295 .ok_or_else(|| "Fee calculation overflow".to_string())? as u64;
296
297 let flash_state = derive_flash_state(¶ms.payer, &token_mint);
298 let source_tier = params.tier as u8;
299
300 let mut begin_data = Vec::with_capacity(57);
301 begin_data.extend_from_slice(&*DISC_BEGIN_FLASH);
302 begin_data.extend_from_slice(token_mint.as_ref());
303 begin_data.extend_from_slice(&amount_native.to_le_bytes());
304 begin_data.extend_from_slice(&expected_fee_native.to_le_bytes());
305 begin_data.push(source_tier);
306
307 let begin_flash = Instruction {
308 program_id: *PROGRAM_ID,
309 accounts: vec![
310 AccountMeta::new(params.payer, true),
311 AccountMeta::new(flash_state, false),
312 AccountMeta::new_readonly(*CONFIG_PDA, false),
313 AccountMeta::new_readonly(sysvar::instructions::id(), false),
314 AccountMeta::new_readonly(system_program::id(), false),
315 ],
316 data: begin_data,
317 };
318
319 let mut end_data = Vec::with_capacity(16);
320 end_data.extend_from_slice(&*DISC_END_FLASH);
321 end_data.extend_from_slice(&amount_native.to_le_bytes());
322
323 let end_flash = Instruction {
324 program_id: *PROGRAM_ID,
325 accounts: vec![
326 AccountMeta::new(params.payer, true),
327 AccountMeta::new(flash_state, false),
328 AccountMeta::new(*FEE_VAULT_PDA, false),
329 AccountMeta::new_readonly(sysvar::instructions::id(), false),
330 AccountMeta::new_readonly(system_program::id(), false),
331 ],
332 data: end_data,
333 };
334
335 Ok(LocalBuildResult {
336 begin_flash,
337 end_flash,
338 token_mint,
339 decimals,
340 expected_fee_native,
341 })
342}