// 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 KB_NATIVE_SOL_MINT_ALIAS: &str = "SOL"; const KB_NATIVE_SOL_SYMBOL: &str = "SOL"; const KB_NATIVE_SOL_NAME: &str = "Solana"; const KB_WRAPPED_SOL_SYMBOL: &str = "WSOL"; const KB_WRAPPED_SOL_NAME: &str = "Wrapped SOL"; const KB_METAPLEX_TOKEN_METADATA_PROGRAM_ID: &str = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"; /// Summary produced by a token metadata backfill pass. #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] pub struct KbTokenMetadataBackfillResult { /// 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 KbTokenMetadataBackfillResult { fn merge(&mut self, other: &KbTokenMetadataBackfillResult) { 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 KbTokenMetadataBackfillService { database: std::sync::Arc, http_pool: std::option::Option>, http_role: std::string::String, } impl KbTokenMetadataBackfillService { /// 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::list_tokens_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::KbTokenMetadataBackfillResult::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::kb_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::KbError::Config( "token metadata mint must not be empty".to_string(), )); } let mut result = crate::KbTokenMetadataBackfillResult { 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::get_token_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 => kb_build_empty_token_dto(trimmed_mint), }; let original_token = token.clone(); let mut resolved = KbResolvedTokenMetadata::default(); let local_metadata_option = kb_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 = kb_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 = kb_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 kb_should_try_metaplex_metadata(&token, &resolved) { let metaplex_result = kb_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 = kb_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::upsert_token(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 KbResolvedTokenMetadata { symbol: std::option::Option, name: std::option::Option, decimals: std::option::Option, token_program: std::option::Option, is_quote_token: std::option::Option, } impl KbResolvedTokenMetadata { fn merge(&mut self, other: KbResolvedTokenMetadata) { if kb_option_string_is_missing(self.symbol.as_deref()) && other.symbol.is_some() { self.symbol = other.symbol; } if kb_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 kb_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 kb_build_empty_token_dto(mint: &str) -> crate::KbTokenDto { return crate::KbTokenDto::new( mint.to_string(), None, None, None, crate::SPL_TOKEN_PROGRAM_ID.to_string(), false, ); } fn kb_resolve_local_token_metadata(mint: &str) -> std::option::Option { if mint == KB_NATIVE_SOL_MINT_ALIAS { return Some(KbResolvedTokenMetadata { symbol: Some(KB_NATIVE_SOL_SYMBOL.to_string()), name: Some(KB_NATIVE_SOL_NAME.to_string()), decimals: Some(9), token_program: Some(crate::SYSTEM_PROGRAM_ID.to_string()), is_quote_token: Some(true), }); } let wsol_mint = crate::WSOL_MINT_ID.to_string(); if mint == wsol_mint { return Some(KbResolvedTokenMetadata { symbol: Some(KB_WRAPPED_SOL_SYMBOL.to_string()), name: Some(KB_WRAPPED_SOL_NAME.to_string()), decimals: Some(9), token_program: Some(crate::SPL_TOKEN_PROGRAM_ID.to_string()), is_quote_token: Some(true), }); } return None; } async fn kb_resolve_pump_fun_metadata_from_decoded_events( database: &crate::KbDatabase, mint: &str, ) -> Result, crate::KbError> { let payload_result = crate::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::KbError::Json(format!( "cannot parse pump.fun metadata payload for mint '{}': {}", mint, error ))); }, }; return Ok(kb_extract_pump_fun_metadata_from_payload(&payload)); } fn kb_extract_pump_fun_metadata_from_payload( payload: &serde_json::Value, ) -> std::option::Option { let symbol = kb_extract_string_by_candidate_keys(payload, &["symbol", "tokenSymbol"]); let name = kb_extract_string_by_candidate_keys(payload, &["name", "tokenName"]); let decimals = kb_extract_u8_by_candidate_keys(payload, &["decimals", "tokenDecimals"]); if symbol.is_none() && name.is_none() && decimals.is_none() { return None; } return Some(KbResolvedTokenMetadata { symbol, name, decimals, token_program: None, is_quote_token: None, }); } fn kb_should_try_metaplex_metadata( token: &crate::KbTokenDto, resolved: &KbResolvedTokenMetadata, ) -> bool { let missing_symbol = kb_option_string_is_missing(token.symbol.as_deref()) && kb_option_string_is_missing(resolved.symbol.as_deref()); let missing_name = kb_option_string_is_missing(token.name.as_deref()) && kb_option_string_is_missing(resolved.name.as_deref()); return missing_symbol || missing_name; } fn kb_apply_resolved_metadata_to_token( token: &mut crate::KbTokenDto, resolved: &KbResolvedTokenMetadata, ) -> bool { let mut changed = false; if kb_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 kb_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 kb_fetch_mint_account_metadata( http_pool: &crate::HttpEndpointPool, http_role: &str, mint: &str, ) -> Result, crate::KbError> { 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(kb_u64_to_u8); let token_2022_metadata = kb_extract_token_2022_metadata_from_mint_account_value(value); let mut resolved = KbResolvedTokenMetadata { 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 kb_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 = kb_extract_string_by_candidate_keys(state, &["symbol", "tokenSymbol"]); let name = kb_extract_string_by_candidate_keys(state, &["name", "tokenName"]); if symbol.is_none() && name.is_none() { continue; } return Some(KbResolvedTokenMetadata { symbol, name, decimals: None, token_program: None, is_quote_token: None, }); } return None; } async fn kb_fetch_metaplex_metadata( http_pool: &crate::HttpEndpointPool, http_role: &str, mint: &str, ) -> Result, crate::KbError> { let metadata_address_result = kb_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 = kb_extract_base64_account_data(value); let data_text = match data_text_option { Some(data_text) => data_text, None => return Ok(None), }; let bytes_result = kb_decode_base64_standard(data_text.as_str()); let bytes = match bytes_result { Ok(bytes) => bytes, Err(error) => return Err(error), }; return Ok(kb_parse_metaplex_token_metadata_account(&bytes)); } fn kb_derive_metaplex_metadata_address(mint: &str) -> Result { let program_id_result = ::from_str( KB_METAPLEX_TOKEN_METADATA_PROGRAM_ID, ); let program_id = match program_id_result { Ok(program_id) => program_id, Err(error) => { return Err(crate::KbError::Config(format!( "invalid metaplex metadata program id '{}': {}", KB_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::KbError::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 kb_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 kb_parse_metaplex_token_metadata_account( bytes: &[u8], ) -> std::option::Option { if bytes.len() < 69 { return None; } let mut offset = 65usize; let name_option = kb_read_borsh_string(bytes, &mut offset); let symbol_option = kb_read_borsh_string(bytes, &mut offset); let _uri_option = kb_read_borsh_string(bytes, &mut offset); let name = match name_option { Some(name) => kb_clean_metaplex_string(name.as_str()), None => None, }; let symbol = match symbol_option { Some(symbol) => kb_clean_metaplex_string(symbol.as_str()), None => None, }; if name.is_none() && symbol.is_none() { return None; } return Some(KbResolvedTokenMetadata { symbol, name, decimals: None, token_program: None, is_quote_token: None, }); } fn kb_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 kb_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 kb_decode_base64_standard(text: &str) -> Result, crate::KbError> { let mut output = std::vec::Vec::new(); let mut group = [0u8; 4]; let mut group_len = 0usize; let mut padding_count = 0usize; for byte in text.bytes() { if byte == b'\r' || byte == b'\n' || byte == b'\t' || byte == b' ' { continue; } let value_option = kb_base64_value(byte); let value = match value_option { Some(value) => value, None => { return Err(crate::KbError::Json(format!( "invalid base64 character '{}'", byte as char ))); }, }; if byte == b'=' { padding_count += 1; } group[group_len] = value; group_len += 1; if group_len == 4 { output.push((group[0] << 2) | (group[1] >> 4)); if padding_count < 2 { output.push((group[1] << 4) | (group[2] >> 2)); } if padding_count == 0 { output.push((group[2] << 6) | group[3]); } group = [0u8; 4]; group_len = 0; padding_count = 0; } } if group_len != 0 { return Err(crate::KbError::Json( "invalid base64 length: trailing partial group".to_string(), )); } return Ok(output); } fn kb_base64_value(byte: u8) -> std::option::Option { match byte { b'A'..=b'Z' => return Some(byte - b'A'), b'a'..=b'z' => return Some(byte - b'a' + 26), b'0'..=b'9' => return Some(byte - b'0' + 52), b'+' => return Some(62), b'/' => return Some(63), b'=' => return Some(0), _ => return None, } } fn kb_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 kb_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 = kb_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 = kb_u64_to_u8(parsed); if converted.is_some() { return converted; } } } return None; } fn kb_option_string_is_missing(value: std::option::Option<&str>) -> bool { match value { Some(value) => return value.trim().is_empty(), None => return true, } } fn kb_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]); kb_push_borsh_string(&mut bytes, name); kb_push_borsh_string(&mut bytes, symbol); kb_push_borsh_string(&mut bytes, uri); return bytes; } fn kb_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::KbDatabaseConfig { enabled: true, backend: crate::KbDatabaseBackend::Sqlite, sqlite: crate::KbSqliteDatabaseConfig { 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::KbDatabase::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::kb_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 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::kb_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::kb_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::kb_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::kb_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::KbTokenDto::new( crate::WSOL_MINT_ID.to_string(), None, None, None, crate::SPL_TOKEN_PROGRAM_ID.to_string(), false, ); let upsert_result = crate::upsert_token(database.as_ref(), &token).await; if let Err(error) = upsert_result { panic!("token upsert must succeed: {}", error); } let service = crate::KbTokenMetadataBackfillService::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::get_token_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_does_not_overwrite_existing_display_metadata() { let database = make_database().await; let token = crate::KbTokenDto::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::upsert_token(database.as_ref(), &token).await; if let Err(error) = upsert_result { panic!("token upsert must succeed: {}", error); } let service = crate::KbTokenMetadataBackfillService::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::get_token_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); } }