// file: kb_lib/src/token_metadata.rs //! Token metadata resolution and backfill. //! //! This module enriches already discovered token mints with stable metadata. //! It intentionally stays independent from DEX-specific decoding so every DEX //! benefits from the same metadata path. const NATIVE_SOL_MINT_ALIAS: &str = "SOL"; const NATIVE_SOL_SYMBOL: &str = "SOL"; const NATIVE_SOL_NAME: &str = "Solana"; const WRAPPED_SOL_SYMBOL: &str = "WSOL"; const WRAPPED_SOL_NAME: &str = "Wrapped SOL"; const METAPLEX_TOKEN_METADATA_PROGRAM_ID: &str = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"; #[derive(Debug, Clone, Copy)] struct KnownLocalTokenMetadata { mint: &'static str, symbol: &'static str, name: &'static str, decimals: u8, token_program: &'static str, is_quote_token: std::option::Option, } const KNOWN_LOCAL_TOKEN_METADATA: &[KnownLocalTokenMetadata] = &[ KnownLocalTokenMetadata { mint: crate::WSOL_MINT_ID, symbol: WRAPPED_SOL_SYMBOL, name: WRAPPED_SOL_NAME, decimals: 9, token_program: crate::SPL_TOKEN_PROGRAM_ID, is_quote_token: Some(true), }, KnownLocalTokenMetadata { mint: crate::USDC_MINT_ID, symbol: "USDC", name: "USD Coin", decimals: 6, token_program: crate::SPL_TOKEN_PROGRAM_ID, is_quote_token: Some(true), }, KnownLocalTokenMetadata { mint: crate::USDT_MINT_ID, symbol: "USDT", name: "Tether USD", decimals: 6, token_program: crate::SPL_TOKEN_PROGRAM_ID, is_quote_token: Some(true), }, KnownLocalTokenMetadata { mint: crate::JUP_MINT_ID, symbol: "JUP", name: "Jupiter", decimals: 6, token_program: crate::SPL_TOKEN_PROGRAM_ID, is_quote_token: None, }, KnownLocalTokenMetadata { mint: crate::RAY_MINT_ID, symbol: "RAY", name: "Raydium", decimals: 6, token_program: crate::SPL_TOKEN_PROGRAM_ID, is_quote_token: None, }, KnownLocalTokenMetadata { mint: crate::BONK_MINT_ID, symbol: "BONK", name: "Bonk", decimals: 5, token_program: crate::SPL_TOKEN_PROGRAM_ID, is_quote_token: None, }, ]; /// Summary produced by a token metadata backfill pass. #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] pub struct TokenMetadataBackfillResult { /// Number of tokens considered by this pass. pub total_token_count: usize, /// Number of tokens for which a resolution attempt was made. pub attempted_token_count: usize, /// Number of tokens enriched from local deterministic metadata. pub local_metadata_count: usize, /// Number of tokens enriched from Pump.fun decoded payloads. pub pump_fun_payload_metadata_count: usize, /// Number of tokens enriched from the SPL mint account. pub mint_account_metadata_count: usize, /// Number of tokens enriched from the Metaplex metadata account. pub metaplex_metadata_count: usize, /// Number of token rows updated. pub updated_token_count: usize, /// Number of token rows already complete or not resolvable without error. pub skipped_token_count: usize, /// Number of non-fatal per-token errors. pub error_count: usize, /// Number of pair display symbols updated after metadata enrichment. pub pair_symbol_updated_count: usize, /// Number of pair display symbols already correct after metadata enrichment. pub pair_symbol_skipped_count: usize, /// Number of pair display symbols skipped because a related token row was missing. pub pair_symbol_missing_token_count: usize, } impl TokenMetadataBackfillResult { fn merge(&mut self, other: &TokenMetadataBackfillResult) { self.total_token_count += other.total_token_count; self.attempted_token_count += other.attempted_token_count; self.local_metadata_count += other.local_metadata_count; self.pump_fun_payload_metadata_count += other.pump_fun_payload_metadata_count; self.mint_account_metadata_count += other.mint_account_metadata_count; self.metaplex_metadata_count += other.metaplex_metadata_count; self.updated_token_count += other.updated_token_count; self.skipped_token_count += other.skipped_token_count; self.error_count += other.error_count; self.pair_symbol_updated_count += other.pair_symbol_updated_count; self.pair_symbol_skipped_count += other.pair_symbol_skipped_count; self.pair_symbol_missing_token_count += other.pair_symbol_missing_token_count; } } /// Service that enriches persisted token rows with mint and display metadata. #[derive(Debug, Clone)] pub struct TokenMetadataBackfillService { database: std::sync::Arc, http_pool: std::option::Option>, http_role: std::string::String, } impl TokenMetadataBackfillService { /// Creates a metadata backfill service backed by Solana HTTP RPC. pub fn new( http_pool: std::sync::Arc, database: std::sync::Arc, http_role: std::string::String, ) -> Self { return Self { database, http_pool: Some(http_pool), http_role, }; } /// Creates a metadata backfill service that can only apply local and DB-derived metadata. pub fn new_local(database: std::sync::Arc) -> Self { return Self { database, http_pool: None, http_role: "local_metadata".to_string(), }; } /// Enriches all tokens whose metadata is incomplete. pub async fn backfill_missing_token_metadata( &self, limit: std::option::Option, ) -> Result { let tokens_result = crate::query_tokens_list_missing_metadata(self.database.as_ref(), limit).await; let tokens = match tokens_result { Ok(tokens) => tokens, Err(error) => return Err(error), }; let mut result = crate::TokenMetadataBackfillResult::default(); for token in tokens { let token_result = self.backfill_token_metadata_by_mint(token.mint.as_str()).await; match token_result { Ok(token_result) => result.merge(&token_result), Err(error) => { result.total_token_count += 1; result.attempted_token_count += 1; result.error_count += 1; tracing::warn!( mint = %token.mint, error = %error, "token metadata backfill failed for one mint" ); }, } } let pair_symbol_result = crate::refresh_pair_symbols(self.database.as_ref()).await; match pair_symbol_result { Ok(pair_symbol_result) => { result.pair_symbol_updated_count += pair_symbol_result.updated_pair_count; result.pair_symbol_skipped_count += pair_symbol_result.skipped_pair_count; result.pair_symbol_missing_token_count += pair_symbol_result.missing_token_count; }, Err(error) => { result.error_count += 1; tracing::warn!( error = %error, "pair symbol refresh failed after token metadata backfill" ); }, } return Ok(result); } /// Enriches one token mint with local, DEX-payload, mint-account, and Metaplex metadata. pub async fn backfill_token_metadata_by_mint( &self, mint: &str, ) -> Result { let trimmed_mint = mint.trim(); if trimmed_mint.is_empty() { return Err(crate::Error::Config("token metadata mint must not be empty".to_string())); } let mut result = crate::TokenMetadataBackfillResult { total_token_count: 1, attempted_token_count: 1, local_metadata_count: 0, pump_fun_payload_metadata_count: 0, mint_account_metadata_count: 0, metaplex_metadata_count: 0, updated_token_count: 0, skipped_token_count: 0, error_count: 0, pair_symbol_updated_count: 0, pair_symbol_skipped_count: 0, pair_symbol_missing_token_count: 0, }; let token_result = crate::query_tokens_get_by_mint(self.database.as_ref(), trimmed_mint).await; let token_option = match token_result { Ok(token_option) => token_option, Err(error) => return Err(error), }; let token_existed = token_option.is_some(); let mut token = match token_option { Some(token) => token, None => build_empty_token_dto(trimmed_mint), }; let original_token = token.clone(); let mut resolved = ResolvedTokenMetadata::default(); let local_metadata_option = resolve_local_token_metadata(trimmed_mint); if let Some(local_metadata) = local_metadata_option { result.local_metadata_count += 1; resolved.merge(local_metadata); } let pump_fun_metadata_result = resolve_pump_fun_metadata_from_decoded_events(self.database.as_ref(), trimmed_mint) .await; match pump_fun_metadata_result { Ok(pump_fun_metadata_option) => { if let Some(pump_fun_metadata) = pump_fun_metadata_option { result.pump_fun_payload_metadata_count += 1; resolved.merge(pump_fun_metadata); } }, Err(error) => { result.error_count += 1; tracing::warn!( mint = %trimmed_mint, error = %error, "pump.fun metadata payload resolution failed" ); }, } if let Some(http_pool) = &self.http_pool { let mint_account_result = fetch_mint_account_metadata( http_pool.as_ref(), self.http_role.as_str(), trimmed_mint, ) .await; match mint_account_result { Ok(mint_account_option) => { if let Some(mint_account) = mint_account_option { result.mint_account_metadata_count += 1; resolved.merge(mint_account); } }, Err(error) => { result.error_count += 1; tracing::warn!( mint = %trimmed_mint, error = %error, "mint account metadata resolution failed" ); }, } if should_try_metaplex_metadata(&token, &resolved) { let metaplex_result = fetch_metaplex_metadata( http_pool.as_ref(), self.http_role.as_str(), trimmed_mint, ) .await; match metaplex_result { Ok(metaplex_option) => { if let Some(metaplex_metadata) = metaplex_option { result.metaplex_metadata_count += 1; resolved.merge(metaplex_metadata); } }, Err(error) => { result.error_count += 1; tracing::warn!( mint = %trimmed_mint, error = %error, "metaplex metadata resolution failed" ); }, } } } let changed = apply_resolved_metadata_to_token(&mut token, &resolved); if !token_existed && !changed { result.skipped_token_count += 1; return Ok(result); } if changed || token.id.is_none() { token.updated_at = chrono::Utc::now(); let upsert_result = crate::query_tokens_upsert(self.database.as_ref(), &token).await; match upsert_result { Ok(_) => { if changed || original_token.id.is_none() { result.updated_token_count += 1; } else { result.skipped_token_count += 1; } }, Err(error) => return Err(error), } } else { result.skipped_token_count += 1; } return Ok(result); } } #[derive(Debug, Clone, Default)] struct ResolvedTokenMetadata { symbol: std::option::Option, name: std::option::Option, decimals: std::option::Option, token_program: std::option::Option, is_quote_token: std::option::Option, } impl ResolvedTokenMetadata { fn merge(&mut self, other: ResolvedTokenMetadata) { if option_string_is_missing(self.symbol.as_deref()) && other.symbol.is_some() { self.symbol = other.symbol; } if option_string_is_missing(self.name.as_deref()) && other.name.is_some() { self.name = other.name; } if self.decimals.is_none() && other.decimals.is_some() { self.decimals = other.decimals; } if option_string_is_missing(self.token_program.as_deref()) && other.token_program.is_some() { self.token_program = other.token_program; } if self.is_quote_token.is_none() && other.is_quote_token.is_some() { self.is_quote_token = other.is_quote_token; } } } fn build_empty_token_dto(mint: &str) -> crate::TokenDto { return crate::TokenDto::new( mint.to_string(), None, None, None, crate::SPL_TOKEN_PROGRAM_ID.to_string(), false, ); } fn resolve_local_token_metadata(mint: &str) -> std::option::Option { if mint == NATIVE_SOL_MINT_ALIAS { return Some(ResolvedTokenMetadata { symbol: Some(NATIVE_SOL_SYMBOL.to_string()), name: Some(NATIVE_SOL_NAME.to_string()), decimals: Some(9), token_program: Some(crate::SYSTEM_PROGRAM_ID.to_string()), is_quote_token: Some(true), }); } for known_token in KNOWN_LOCAL_TOKEN_METADATA { if mint == known_token.mint { return Some(ResolvedTokenMetadata { symbol: Some(known_token.symbol.to_string()), name: Some(known_token.name.to_string()), decimals: Some(known_token.decimals), token_program: Some(known_token.token_program.to_string()), is_quote_token: known_token.is_quote_token, }); } } return None; } async fn resolve_pump_fun_metadata_from_decoded_events( database: &crate::Database, mint: &str, ) -> Result, crate::Error> { let payload_result = crate::query_dex_decoded_events_get_latest_pump_fun_create_payload_by_mint(database, mint) .await; let payload_option = match payload_result { Ok(payload_option) => payload_option, Err(error) => return Err(error), }; let payload_text = match payload_option { Some(payload_text) => payload_text, None => return Ok(None), }; let payload_result = serde_json::from_str::(payload_text.as_str()); let payload = match payload_result { Ok(payload) => payload, Err(error) => { return Err(crate::Error::Json(format!( "cannot parse pump.fun metadata payload for mint '{}': {}", mint, error ))); }, }; return Ok(extract_pump_fun_metadata_from_payload(&payload)); } fn extract_pump_fun_metadata_from_payload( payload: &serde_json::Value, ) -> std::option::Option { let symbol = extract_string_by_candidate_keys(payload, &["symbol", "tokenSymbol"]); let name = extract_string_by_candidate_keys(payload, &["name", "tokenName"]); let decimals = extract_u8_by_candidate_keys(payload, &["decimals", "tokenDecimals"]); if symbol.is_none() && name.is_none() && decimals.is_none() { return None; } return Some(ResolvedTokenMetadata { symbol, name, decimals, token_program: None, is_quote_token: None, }); } fn should_try_metaplex_metadata(token: &crate::TokenDto, resolved: &ResolvedTokenMetadata) -> bool { let missing_symbol = option_string_is_missing(token.symbol.as_deref()) && option_string_is_missing(resolved.symbol.as_deref()); let missing_name = option_string_is_missing(token.name.as_deref()) && option_string_is_missing(resolved.name.as_deref()); return missing_symbol || missing_name; } fn apply_resolved_metadata_to_token( token: &mut crate::TokenDto, resolved: &ResolvedTokenMetadata, ) -> bool { let mut changed = false; if option_string_is_missing(token.symbol.as_deref()) { if let Some(symbol) = &resolved.symbol { if !symbol.trim().is_empty() { token.symbol = Some(symbol.trim().to_string()); changed = true; } } } if option_string_is_missing(token.name.as_deref()) { if let Some(name) = &resolved.name { if !name.trim().is_empty() { token.name = Some(name.trim().to_string()); changed = true; } } } if token.decimals.is_none() { if let Some(decimals) = resolved.decimals { token.decimals = Some(decimals); changed = true; } } if let Some(token_program) = &resolved.token_program { if !token_program.trim().is_empty() && token.token_program != *token_program { token.token_program = token_program.clone(); changed = true; } } if let Some(is_quote_token) = resolved.is_quote_token { if token.is_quote_token != is_quote_token { token.is_quote_token = is_quote_token; changed = true; } } return changed; } async fn fetch_mint_account_metadata( http_pool: &crate::HttpEndpointPool, http_role: &str, mint: &str, ) -> Result, crate::Error> { let config = serde_json::json!({ "encoding": "jsonParsed", "commitment": "confirmed" }); let account_result = http_pool .get_account_info_raw_for_role(http_role, mint.to_string(), Some(config)) .await; let account_value = match account_result { Ok(account_value) => account_value, Err(error) => return Err(error), }; let value_option = account_value.get("value"); let value = match value_option { Some(value) => value, None => return Ok(None), }; if value.is_null() { return Ok(None); } let owner = value .get("owner") .and_then(serde_json::Value::as_str) .map(|value| return value.to_string()); let decimals = value .get("data") .and_then(|data| return data.get("parsed")) .and_then(|parsed| return parsed.get("info")) .and_then(|info| return info.get("decimals")) .and_then(serde_json::Value::as_u64) .and_then(u64_to_u8); let token_2022_metadata = extract_token_2022_metadata_from_mint_account_value(value); let mut resolved = ResolvedTokenMetadata { symbol: None, name: None, decimals, token_program: owner, is_quote_token: None, }; if let Some(token_2022_metadata) = token_2022_metadata { resolved.merge(token_2022_metadata); } if resolved.symbol.is_none() && resolved.name.is_none() && resolved.decimals.is_none() && resolved.token_program.is_none() { return Ok(None); } return Ok(Some(resolved)); } fn extract_token_2022_metadata_from_mint_account_value( value: &serde_json::Value, ) -> std::option::Option { let extensions_option = value .get("data") .and_then(|data| return data.get("parsed")) .and_then(|parsed| return parsed.get("info")) .and_then(|info| return info.get("extensions")) .and_then(serde_json::Value::as_array); let extensions = match extensions_option { Some(extensions) => extensions, None => return None, }; for extension in extensions { let extension_name_option = extension.get("extension").and_then(serde_json::Value::as_str); let extension_name = match extension_name_option { Some(extension_name) => extension_name, None => continue, }; if extension_name != "tokenMetadata" { continue; } let state_option = extension .get("state") .or_else(|| return extension.get("metadata")) .or_else(|| return extension.get("value")); let state = match state_option { Some(state) => state, None => continue, }; let symbol = extract_string_by_candidate_keys(state, &["symbol", "tokenSymbol"]); let name = extract_string_by_candidate_keys(state, &["name", "tokenName"]); if symbol.is_none() && name.is_none() { continue; } return Some(ResolvedTokenMetadata { symbol, name, decimals: None, token_program: None, is_quote_token: None, }); } return None; } async fn fetch_metaplex_metadata( http_pool: &crate::HttpEndpointPool, http_role: &str, mint: &str, ) -> Result, crate::Error> { let metadata_address_result = derive_metaplex_metadata_address(mint); let metadata_address = match metadata_address_result { Ok(metadata_address) => metadata_address, Err(error) => return Err(error), }; let config = serde_json::json!({ "encoding": "base64", "commitment": "confirmed" }); let account_result = http_pool .get_account_info_raw_for_role(http_role, metadata_address, Some(config)) .await; let account_value = match account_result { Ok(account_value) => account_value, Err(error) => return Err(error), }; let value_option = account_value.get("value"); let value = match value_option { Some(value) => value, None => return Ok(None), }; if value.is_null() { return Ok(None); } let data_text_option = extract_base64_account_data(value); let data_text = match data_text_option { Some(data_text) => data_text, None => return Ok(None), }; let bytes_result = decode_base64_standard(data_text.as_str()); let bytes = match bytes_result { Ok(bytes) => bytes, Err(error) => return Err(error), }; return Ok(parse_metaplex_token_metadata_account(&bytes)); } fn derive_metaplex_metadata_address(mint: &str) -> Result { let program_id_result = ::from_str( METAPLEX_TOKEN_METADATA_PROGRAM_ID, ); let program_id = match program_id_result { Ok(program_id) => program_id, Err(error) => { return Err(crate::Error::Config(format!( "invalid metaplex metadata program id '{}': {}", METAPLEX_TOKEN_METADATA_PROGRAM_ID, error ))); }, }; let mint_pubkey_result = ::from_str(mint); let mint_pubkey = match mint_pubkey_result { Ok(mint_pubkey) => mint_pubkey, Err(error) => { return Err(crate::Error::Config(format!("invalid token mint '{}': {}", mint, error))); }, }; let (metadata_address, _) = solana_sdk::pubkey::Pubkey::find_program_address( &[b"metadata", program_id.as_ref(), mint_pubkey.as_ref()], &program_id, ); return Ok(metadata_address.to_string()); } fn extract_base64_account_data( value: &serde_json::Value, ) -> std::option::Option { let data = match value.get("data") { Some(data) => data, None => return None, }; if let Some(data_array) = data.as_array() { let first = match data_array.first() { Some(first) => first, None => return None, }; return first.as_str().map(|value| return value.to_string()); } return data.as_str().map(|value| return value.to_string()); } fn parse_metaplex_token_metadata_account( bytes: &[u8], ) -> std::option::Option { if bytes.len() < 69 { return None; } let mut offset = 65usize; let name_option = read_borsh_string(bytes, &mut offset); let symbol_option = read_borsh_string(bytes, &mut offset); let _uri_option = read_borsh_string(bytes, &mut offset); let name = match name_option { Some(name) => clean_metaplex_string(name.as_str()), None => None, }; let symbol = match symbol_option { Some(symbol) => clean_metaplex_string(symbol.as_str()), None => None, }; if name.is_none() && symbol.is_none() { return None; } return Some(ResolvedTokenMetadata { symbol, name, decimals: None, token_program: None, is_quote_token: None, }); } fn read_borsh_string(bytes: &[u8], offset: &mut usize) -> std::option::Option { if bytes.len() < *offset + 4 { return None; } let length = u32::from_le_bytes([ bytes[*offset], bytes[*offset + 1], bytes[*offset + 2], bytes[*offset + 3], ]) as usize; *offset += 4; if bytes.len() < *offset + length { return None; } let data = &bytes[*offset..*offset + length]; *offset += length; let text_result = std::string::String::from_utf8(data.to_vec()); match text_result { Ok(text) => return Some(text), Err(_) => return None, } } fn clean_metaplex_string(value: &str) -> std::option::Option { let cleaned = value.trim_matches(char::from(0)).trim().to_string(); if cleaned.is_empty() { return None } else { return Some(cleaned) } } fn decode_base64_standard(text: &str) -> Result, crate::Error> { use base64::Engine; let decoded_result = base64::engine::general_purpose::STANDARD.decode(text); match decoded_result { Ok(decoded) => return Ok(decoded), Err(error) => { return Err(crate::Error::Json(format!( "cannot decode standard base64 payload: {}", error ))); }, } } fn extract_string_by_candidate_keys( payload: &serde_json::Value, candidate_keys: &[&str], ) -> std::option::Option { for key in candidate_keys { let value_option = payload.get(*key); let value = match value_option { Some(value) => value, None => continue, }; if let Some(text) = value.as_str() { let trimmed = text.trim(); if !trimmed.is_empty() { return Some(trimmed.to_string()); } continue; } if value.is_number() || value.is_boolean() { return Some(value.to_string()); } } return None; } fn extract_u8_by_candidate_keys( payload: &serde_json::Value, candidate_keys: &[&str], ) -> std::option::Option { for key in candidate_keys { let value_option = payload.get(*key); let value = match value_option { Some(value) => value, None => continue, }; if let Some(number) = value.as_u64() { let converted = u64_to_u8(number); if converted.is_some() { return converted; } } if let Some(text) = value.as_str() { let parsed_result = text.trim().parse::(); let parsed = match parsed_result { Ok(parsed) => parsed, Err(_) => continue, }; let converted = u64_to_u8(parsed); if converted.is_some() { return converted; } } } return None; } fn option_string_is_missing(value: std::option::Option<&str>) -> bool { match value { Some(value) => return value.trim().is_empty(), None => return true, } } fn u64_to_u8(value: u64) -> std::option::Option { let converted_result = u8::try_from(value); match converted_result { Ok(converted) => return Some(converted), Err(_) => return None, } } #[cfg(test)] mod tests { fn build_test_metadata_account(name: &str, symbol: &str, uri: &str) -> std::vec::Vec { let mut bytes = std::vec::Vec::new(); bytes.push(4_u8); bytes.extend_from_slice(&[1_u8; 32]); bytes.extend_from_slice(&[2_u8; 32]); push_borsh_string(&mut bytes, name); push_borsh_string(&mut bytes, symbol); push_borsh_string(&mut bytes, uri); return bytes; } fn push_borsh_string(bytes: &mut std::vec::Vec, value: &str) { let length = value.len() as u32; bytes.extend_from_slice(&length.to_le_bytes()); bytes.extend_from_slice(value.as_bytes()); } async fn make_database() -> std::sync::Arc { let tempdir_result = tempfile::tempdir(); let tempdir = match tempdir_result { Ok(tempdir) => tempdir, Err(error) => panic!("tempdir must succeed: {}", error), }; let database_path = tempdir.path().join("token_metadata.sqlite3"); let config = crate::DatabaseConfig { enabled: true, backend: crate::DatabaseBackend::Sqlite, sqlite: crate::SqliteDatabaseConfig { path: database_path.to_string_lossy().to_string(), create_if_missing: true, busy_timeout_ms: 5000, max_connections: 1, auto_initialize_schema: true, use_wal: true, }, }; let database_result = crate::Database::connect_and_initialize(&config).await; let database = match database_result { Ok(database) => database, Err(error) => panic!("database init must succeed: {}", error), }; return std::sync::Arc::new(database); } #[test] fn local_metadata_resolves_wsol() { let mint = crate::WSOL_MINT_ID.to_string(); let metadata_option = super::resolve_local_token_metadata(mint.as_str()); let metadata = match metadata_option { Some(metadata) => metadata, None => panic!("WSOL metadata must resolve"), }; assert_eq!(metadata.symbol.as_deref(), Some("WSOL")); assert_eq!(metadata.name.as_deref(), Some("Wrapped SOL")); assert_eq!(metadata.decimals, Some(9)); assert_eq!(metadata.is_quote_token, Some(true)); } #[test] fn local_metadata_resolves_known_ecosystem_tokens_without_forcing_quote_flag() { let cases = [ (crate::JUP_MINT_ID, "JUP", "Jupiter", 6_u8), (crate::RAY_MINT_ID, "RAY", "Raydium", 6_u8), (crate::BONK_MINT_ID, "BONK", "Bonk", 5_u8), ]; for (mint, expected_symbol, expected_name, expected_decimals) in cases { let metadata_option = super::resolve_local_token_metadata(mint); let metadata = match metadata_option { Some(metadata) => metadata, None => panic!("known ecosystem token metadata must resolve"), }; assert_eq!(metadata.symbol.as_deref(), Some(expected_symbol)); assert_eq!(metadata.name.as_deref(), Some(expected_name)); assert_eq!(metadata.decimals, Some(expected_decimals)); assert_eq!(metadata.is_quote_token, None); } } #[test] fn pump_fun_payload_metadata_extracts_name_and_symbol() { let payload = serde_json::json!({ "mint": "PumpMint111", "symbol": "PUMPX", "name": "Pump Example", "uri": "https://example.invalid/pump.json" }); let metadata_option = super::extract_pump_fun_metadata_from_payload(&payload); let metadata = match metadata_option { Some(metadata) => metadata, None => panic!("pump.fun metadata must parse"), }; assert_eq!(metadata.symbol.as_deref(), Some("PUMPX")); assert_eq!(metadata.name.as_deref(), Some("Pump Example")); } #[test] fn token_2022_inline_metadata_extracts_name_and_symbol() { let account = serde_json::json!({ "data": { "parsed": { "info": { "extensions": [ { "extension": "tokenMetadata", "state": { "symbol": "T22", "name": "Token 2022 Example" } } ] } } } }); let metadata_option = super::extract_token_2022_metadata_from_mint_account_value(&account); let metadata = match metadata_option { Some(metadata) => metadata, None => panic!("token-2022 metadata must parse"), }; assert_eq!(metadata.symbol.as_deref(), Some("T22")); assert_eq!(metadata.name.as_deref(), Some("Token 2022 Example")); } #[test] fn base64_decoder_decodes_standard_payload() { let decoded_result = super::decode_base64_standard("AQIDBA=="); let decoded = match decoded_result { Ok(decoded) => decoded, Err(error) => panic!("base64 decode must succeed: {}", error), }; assert_eq!(decoded, vec![1_u8, 2_u8, 3_u8, 4_u8]); } #[test] fn metaplex_parser_extracts_name_and_symbol() { let bytes = build_test_metadata_account( "Example Token\0\0\0", "EXM\0\0", "https://example.invalid/token.json", ); let metadata_option = super::parse_metaplex_token_metadata_account(&bytes); let metadata = match metadata_option { Some(metadata) => metadata, None => panic!("metadata must parse"), }; assert_eq!(metadata.name.as_deref(), Some("Example Token")); assert_eq!(metadata.symbol.as_deref(), Some("EXM")); } #[tokio::test] async fn local_backfill_updates_wsol_without_http() { let database = make_database().await; let token = crate::TokenDto::new( crate::WSOL_MINT_ID.to_string(), None, None, None, crate::SPL_TOKEN_PROGRAM_ID.to_string(), false, ); let upsert_result = crate::query_tokens_upsert(database.as_ref(), &token).await; if let Err(error) = upsert_result { panic!("token upsert must succeed: {}", error); } let service = crate::TokenMetadataBackfillService::new_local(database.clone()); let result = service.backfill_missing_token_metadata(Some(10)).await; let result = match result { Ok(result) => result, Err(error) => panic!("metadata backfill must succeed: {}", error), }; assert_eq!(result.updated_token_count, 1); let fetched_result = crate::query_tokens_get_by_mint( database.as_ref(), crate::WSOL_MINT_ID.to_string().as_str(), ) .await; let fetched_option = match fetched_result { Ok(fetched_option) => fetched_option, Err(error) => panic!("token fetch must succeed: {}", error), }; let fetched = match fetched_option { Some(fetched) => fetched, None => panic!("token must exist"), }; assert_eq!(fetched.symbol.as_deref(), Some("WSOL")); assert_eq!(fetched.name.as_deref(), Some("Wrapped SOL")); assert_eq!(fetched.decimals, Some(9)); assert!(fetched.is_quote_token); } #[tokio::test] async fn local_backfill_updates_stable_quotes_without_http() { let database = make_database().await; let usdc = crate::TokenDto::new( crate::USDC_MINT_ID.to_string(), None, None, None, crate::SPL_TOKEN_PROGRAM_ID.to_string(), false, ); let usdt = crate::TokenDto::new( crate::USDT_MINT_ID.to_string(), None, None, None, crate::SPL_TOKEN_PROGRAM_ID.to_string(), false, ); let usdc_upsert_result = crate::query_tokens_upsert(database.as_ref(), &usdc).await; if let Err(error) = usdc_upsert_result { panic!("usdc token upsert must succeed: {}", error); } let usdt_upsert_result = crate::query_tokens_upsert(database.as_ref(), &usdt).await; if let Err(error) = usdt_upsert_result { panic!("usdt token upsert must succeed: {}", error); } let service = crate::TokenMetadataBackfillService::new_local(database.clone()); let result = service.backfill_missing_token_metadata(Some(10)).await; let result = match result { Ok(result) => result, Err(error) => panic!("metadata backfill must succeed: {}", error), }; assert_eq!(result.updated_token_count, 2); let usdc_result = crate::query_tokens_get_by_mint(database.as_ref(), crate::USDC_MINT_ID).await; let usdc_option = match usdc_result { Ok(usdc_option) => usdc_option, Err(error) => panic!("usdc token fetch must succeed: {}", error), }; let usdc = match usdc_option { Some(usdc) => usdc, None => panic!("usdc token must exist"), }; assert_eq!(usdc.symbol.as_deref(), Some("USDC")); assert_eq!(usdc.name.as_deref(), Some("USD Coin")); assert_eq!(usdc.decimals, Some(6)); assert!(usdc.is_quote_token); let usdt_result = crate::query_tokens_get_by_mint(database.as_ref(), crate::USDT_MINT_ID).await; let usdt_option = match usdt_result { Ok(usdt_option) => usdt_option, Err(error) => panic!("usdt token fetch must succeed: {}", error), }; let usdt = match usdt_option { Some(usdt) => usdt, None => panic!("usdt token must exist"), }; assert_eq!(usdt.symbol.as_deref(), Some("USDT")); assert_eq!(usdt.name.as_deref(), Some("Tether USD")); assert_eq!(usdt.decimals, Some(6)); assert!(usdt.is_quote_token); } #[tokio::test] async fn local_backfill_updates_known_ecosystem_tokens_without_http() { let database = make_database().await; let token_mints = [crate::JUP_MINT_ID, crate::RAY_MINT_ID, crate::BONK_MINT_ID]; for mint in token_mints { let token = crate::TokenDto::new( mint.to_string(), None, None, None, crate::SPL_TOKEN_PROGRAM_ID.to_string(), false, ); let upsert_result = crate::query_tokens_upsert(database.as_ref(), &token).await; if let Err(error) = upsert_result { panic!("known token upsert must succeed: {}", error); } } let service = crate::TokenMetadataBackfillService::new_local(database.clone()); let result = service.backfill_missing_token_metadata(Some(10)).await; let result = match result { Ok(result) => result, Err(error) => panic!("metadata backfill must succeed: {}", error), }; assert_eq!(result.updated_token_count, 3); let jup_result = crate::query_tokens_get_by_mint(database.as_ref(), crate::JUP_MINT_ID).await; let jup_option = match jup_result { Ok(jup_option) => jup_option, Err(error) => panic!("jup token fetch must succeed: {}", error), }; let jup = match jup_option { Some(jup) => jup, None => panic!("jup token must exist"), }; assert_eq!(jup.symbol.as_deref(), Some("JUP")); assert_eq!(jup.name.as_deref(), Some("Jupiter")); assert_eq!(jup.decimals, Some(6)); assert!(!jup.is_quote_token); let ray_result = crate::query_tokens_get_by_mint(database.as_ref(), crate::RAY_MINT_ID).await; let ray_option = match ray_result { Ok(ray_option) => ray_option, Err(error) => panic!("ray token fetch must succeed: {}", error), }; let ray = match ray_option { Some(ray) => ray, None => panic!("ray token must exist"), }; assert_eq!(ray.symbol.as_deref(), Some("RAY")); assert_eq!(ray.name.as_deref(), Some("Raydium")); assert_eq!(ray.decimals, Some(6)); assert!(!ray.is_quote_token); let bonk_result = crate::query_tokens_get_by_mint(database.as_ref(), crate::BONK_MINT_ID).await; let bonk_option = match bonk_result { Ok(bonk_option) => bonk_option, Err(error) => panic!("bonk token fetch must succeed: {}", error), }; let bonk = match bonk_option { Some(bonk) => bonk, None => panic!("bonk token must exist"), }; assert_eq!(bonk.symbol.as_deref(), Some("BONK")); assert_eq!(bonk.name.as_deref(), Some("Bonk")); assert_eq!(bonk.decimals, Some(5)); assert!(!bonk.is_quote_token); } #[tokio::test] async fn local_backfill_does_not_overwrite_existing_display_metadata() { let database = make_database().await; let token = crate::TokenDto::new( crate::WSOL_MINT_ID.to_string(), Some("SOL".to_string()), Some("Custom Sol".to_string()), None, crate::SPL_TOKEN_PROGRAM_ID.to_string(), false, ); let upsert_result = crate::query_tokens_upsert(database.as_ref(), &token).await; if let Err(error) = upsert_result { panic!("token upsert must succeed: {}", error); } let service = crate::TokenMetadataBackfillService::new_local(database.clone()); let result = service .backfill_token_metadata_by_mint(crate::WSOL_MINT_ID.to_string().as_str()) .await; if let Err(error) = result { panic!("metadata backfill must succeed: {}", error); } let fetched_result = crate::query_tokens_get_by_mint( database.as_ref(), crate::WSOL_MINT_ID.to_string().as_str(), ) .await; let fetched_option = match fetched_result { Ok(fetched_option) => fetched_option, Err(error) => panic!("token fetch must succeed: {}", error), }; let fetched = match fetched_option { Some(fetched) => fetched, None => panic!("token must exist"), }; assert_eq!(fetched.symbol.as_deref(), Some("SOL")); assert_eq!(fetched.name.as_deref(), Some("Custom Sol")); assert_eq!(fetched.decimals, Some(9)); assert!(fetched.is_quote_token); } }