Files
khadhroony-bobobot/kb_lib/src/token_metadata.rs
2026-05-14 17:44:01 +02:00

1196 lines
44 KiB
Rust

// 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<bool>,
}
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<crate::Database>,
http_pool: std::option::Option<std::sync::Arc<crate::HttpEndpointPool>>,
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<crate::HttpEndpointPool>,
database: std::sync::Arc<crate::Database>,
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<crate::Database>) -> 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<i64>,
) -> Result<crate::TokenMetadataBackfillResult, crate::Error> {
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<crate::TokenMetadataBackfillResult, crate::Error> {
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<std::string::String>,
name: std::option::Option<std::string::String>,
decimals: std::option::Option<u8>,
token_program: std::option::Option<std::string::String>,
is_quote_token: std::option::Option<bool>,
}
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<ResolvedTokenMetadata> {
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<std::option::Option<ResolvedTokenMetadata>, 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::<serde_json::Value>(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<ResolvedTokenMetadata> {
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<std::option::Option<ResolvedTokenMetadata>, 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<ResolvedTokenMetadata> {
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<std::option::Option<ResolvedTokenMetadata>, 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<std::string::String, crate::Error> {
let program_id_result = <solana_sdk::pubkey::Pubkey as std::str::FromStr>::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 = <solana_sdk::pubkey::Pubkey as std::str::FromStr>::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<std::string::String> {
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<ResolvedTokenMetadata> {
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<std::string::String> {
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<std::string::String> {
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<std::vec::Vec<u8>, 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<std::string::String> {
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<u8> {
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::<u64>();
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<u8> {
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<u8> {
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<u8>, 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<crate::Database> {
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);
}
}