#[allow(deprecated)]
use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
system_program,
sysvar,
};
use sha2::{Sha256, Digest};
use std::collections::HashMap;
use std::sync::RwLock;
use crate::types::{VAEA_PROGRAM_ID, VAEA_API_URL, FlashTier, TokenCapacity};
#[derive(Debug, Clone)]
pub struct TokenEntry {
pub symbol: String,
pub mint: Pubkey,
pub name: String,
pub decimals: u8,
pub max_amount: f64,
pub max_amount_usd: f64,
pub source_protocol: String,
pub route_type: String,
pub status: String,
pub logo_uri: Option<String>,
}
lazy_static::lazy_static! {
static ref PROGRAM_ID: Pubkey = VAEA_PROGRAM_ID.parse().unwrap();
static ref DISC_BEGIN_FLASH: [u8; 8] = anchor_discriminator("begin_flash");
static ref DISC_END_FLASH: [u8; 8] = anchor_discriminator("end_flash");
static ref CONFIG_PDA: Pubkey = derive_config();
static ref FEE_VAULT_PDA: Pubkey = derive_fee_vault();
static ref TOKEN_REGISTRY: RwLock<HashMap<String, TokenEntry>> = {
let mut m = HashMap::new();
for (sym, mint_str, name, dec) in FALLBACK_TOKENS.iter() {
if let Ok(mint) = mint_str.parse::<Pubkey>() {
let entry = TokenEntry {
symbol: sym.to_string(),
mint,
name: name.to_string(),
decimals: *dec,
max_amount: 0.0,
max_amount_usd: 0.0,
source_protocol: "unknown".to_string(),
route_type: "direct".to_string(),
status: "unknown".to_string(),
logo_uri: None,
};
m.insert(sym.to_lowercase(), entry.clone());
m.insert(mint_str.to_string(), entry);
}
}
RwLock::new(m)
};
static ref REGISTRY_SYNCED: RwLock<bool> = RwLock::new(false);
}
const FALLBACK_TOKENS: [(&str, &str, &str, u8); 12] = [
("SOL", "So11111111111111111111111111111111111111112", "Solana", 9),
("USDC", "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "USD Coin", 6),
("USDT", "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", "Tether USD", 6),
("JitoSOL", "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", "Jito Staked SOL", 9),
("JupSOL", "jupSoLaHXQiZZTSfEWMTRRgpnyFm8f6sZdosWBjx93v", "Jupiter SOL", 9),
("JUP", "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", "Jupiter", 6),
("JLP", "27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4", "Jupiter LP", 6),
("cbBTC", "cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij", "Coinbase BTC", 8),
("mSOL", "mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So", "Marinade SOL", 9),
("bSOL", "bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1", "Blaze SOL", 9),
("INF", "5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm", "Infinity", 9),
("laineSOL", "LAinEtNLgpmCP9Rvsf5Hn8W6EhNiKLZQti1xfWMLy6X", "Laine SOL", 9),
];
const BLACKLIST: [&str; 4] = ["EURCV", "FBTC", "USDCV", "wstUSR"];
fn is_valid_token(t: &TokenCapacity) -> bool {
let sym = &t.symbol;
if FALLBACK_TOKENS.iter().any(|(s, _, _, _)| s.eq_ignore_ascii_case(sym)) {
return true;
}
if BLACKLIST.contains(&sym.as_str()) { return false; }
if t.name.starts_with("SPL Single Pool") { return false; }
if sym.len() > 2 && sym.starts_with('k') && sym.contains('-') {
let after_k = &sym[1..];
if after_k.chars().next().map_or(false, |c| c.is_uppercase()) {
return false;
}
}
if t.name.is_empty() || t.name == "Unknown Token" { return false; }
if sym.len() >= 12 || sym.contains("...") { return false; }
true
}
pub fn update_registry_from_capacity(tokens: &[TokenCapacity]) {
let mut reg = TOKEN_REGISTRY.write().unwrap();
for t in tokens {
if !is_valid_token(t) { continue; }
if let Ok(mint) = t.mint.parse::<Pubkey>() {
let entry = TokenEntry {
symbol: t.symbol.clone(),
mint,
name: if t.name.is_empty() { t.symbol.clone() } else { t.name.clone() },
decimals: t.decimals,
max_amount: t.max_amount,
max_amount_usd: t.max_amount_usd,
source_protocol: t.source_protocol.clone(),
route_type: t.route_type.clone(),
status: t.status.clone(),
logo_uri: None,
};
reg.insert(t.symbol.to_lowercase(), entry.clone());
reg.insert(t.mint.clone(), entry);
}
}
*REGISTRY_SYNCED.write().unwrap() = true;
}
pub async fn sync_registry(api_url: Option<&str>) -> usize {
let url = api_url.unwrap_or(VAEA_API_URL);
match reqwest::get(&format!("{}/v1/capacity", url)).await {
Ok(res) => {
if let Ok(data) = res.json::<crate::types::CapacityResponse>().await {
update_registry_from_capacity(&data.tokens);
}
}
Err(_) => {}
}
let reg = TOKEN_REGISTRY.read().unwrap();
let mut seen = std::collections::HashSet::new();
for entry in reg.values() { seen.insert(entry.symbol.clone()); }
seen.len()
}
pub fn is_registry_synced() -> bool {
*REGISTRY_SYNCED.read().unwrap()
}
pub fn get_token(token: &str) -> Option<TokenEntry> {
let reg = TOKEN_REGISTRY.read().unwrap();
reg.get(&token.to_lowercase()).cloned()
.or_else(|| reg.get(token).cloned())
}
pub fn get_all_tokens() -> Vec<TokenEntry> {
let reg = TOKEN_REGISTRY.read().unwrap();
let mut seen = std::collections::HashSet::new();
let mut result: Vec<TokenEntry> = Vec::new();
for entry in reg.values() {
if seen.insert(entry.symbol.clone()) {
result.push(entry.clone());
}
}
result.sort_by(|a, b| b.max_amount_usd.partial_cmp(&a.max_amount_usd).unwrap_or(std::cmp::Ordering::Equal));
result
}
fn anchor_discriminator(name: &str) -> [u8; 8] {
let mut hasher = Sha256::new();
hasher.update(format!("global:{}", name));
let result = hasher.finalize();
let mut disc = [0u8; 8];
disc.copy_from_slice(&result[..8]);
disc
}
fn derive_flash_state(payer: &Pubkey, token_mint: &Pubkey) -> Pubkey {
Pubkey::find_program_address(
&[b"flash", payer.as_ref(), token_mint.as_ref()],
&PROGRAM_ID,
).0
}
fn derive_config() -> Pubkey {
Pubkey::find_program_address(&[b"config"], &PROGRAM_ID).0
}
fn derive_fee_vault() -> Pubkey {
Pubkey::find_program_address(&[b"fee_vault"], &PROGRAM_ID).0
}
pub enum TokenId {
Symbol(String),
Mint(Pubkey),
}
impl From<&str> for TokenId {
fn from(s: &str) -> Self { TokenId::Symbol(s.to_string()) }
}
impl From<Pubkey> for TokenId {
fn from(p: Pubkey) -> Self { TokenId::Mint(p) }
}
pub struct LocalBuildParams {
pub payer: Pubkey,
pub token: TokenId,
pub amount: f64,
pub tier: FlashTier,
}
pub struct LocalBuildResult {
pub begin_flash: Instruction,
pub end_flash: Instruction,
pub token_mint: Pubkey,
pub decimals: u8,
pub expected_fee_native: u64,
}
pub fn local_build(params: LocalBuildParams) -> Result<LocalBuildResult, String> {
let (token_mint, decimals) = match ¶ms.token {
TokenId::Symbol(sym) => {
let entry = get_token(sym)
.ok_or_else(|| {
let available: Vec<String> = get_all_tokens().iter().map(|t| t.symbol.clone()).collect();
format!(
"Unknown token: {}. Available: {}. Use TokenId::Mint for unlisted tokens, or call sync_registry() first.",
sym, available.join(", ")
)
})?;
(entry.mint, entry.decimals)
}
TokenId::Mint(mint) => {
let entry = get_token(&mint.to_string());
let decimals = entry.map(|e| e.decimals).unwrap_or(9);
(*mint, decimals)
}
};
let fee_bps = params.tier.fee_bps();
let decimals_factor = 10u64.pow(decimals as u32);
let amount_native = (params.amount * decimals_factor as f64) as u64;
let expected_fee_native = (amount_native as u128)
.checked_mul(fee_bps as u128)
.and_then(|v| v.checked_div(10_000))
.ok_or_else(|| "Fee calculation overflow".to_string())? as u64;
let flash_state = derive_flash_state(¶ms.payer, &token_mint);
let source_tier = params.tier as u8;
let mut begin_data = Vec::with_capacity(57);
begin_data.extend_from_slice(&*DISC_BEGIN_FLASH);
begin_data.extend_from_slice(token_mint.as_ref());
begin_data.extend_from_slice(&amount_native.to_le_bytes());
begin_data.extend_from_slice(&expected_fee_native.to_le_bytes());
begin_data.push(source_tier);
let begin_flash = Instruction {
program_id: *PROGRAM_ID,
accounts: vec![
AccountMeta::new(params.payer, true),
AccountMeta::new(flash_state, false),
AccountMeta::new_readonly(*CONFIG_PDA, false),
AccountMeta::new_readonly(sysvar::instructions::id(), false),
AccountMeta::new_readonly(system_program::id(), false),
],
data: begin_data,
};
let mut end_data = Vec::with_capacity(16);
end_data.extend_from_slice(&*DISC_END_FLASH);
end_data.extend_from_slice(&amount_native.to_le_bytes());
let end_flash = Instruction {
program_id: *PROGRAM_ID,
accounts: vec![
AccountMeta::new(params.payer, true),
AccountMeta::new(flash_state, false),
AccountMeta::new(*FEE_VAULT_PDA, false),
AccountMeta::new_readonly(sysvar::instructions::id(), false),
AccountMeta::new_readonly(system_program::id(), false),
],
data: end_data,
};
Ok(LocalBuildResult {
begin_flash,
end_flash,
token_mint,
decimals,
expected_fee_native,
})
}