Files
khadhroony-bobobot/kb_lib/src/token_metadata.rs
2026-05-05 05:03:11 +02:00

1025 lines
37 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 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<crate::KbDatabase>,
http_pool: std::option::Option<std::sync::Arc<crate::HttpEndpointPool>>,
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<crate::HttpEndpointPool>,
database: std::sync::Arc<crate::KbDatabase>,
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::KbDatabase>) -> 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::KbTokenMetadataBackfillResult, crate::KbError> {
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<crate::KbTokenMetadataBackfillResult, crate::KbError> {
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<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 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<KbResolvedTokenMetadata> {
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<std::option::Option<KbResolvedTokenMetadata>, 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::<serde_json::Value>(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<KbResolvedTokenMetadata> {
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<std::option::Option<KbResolvedTokenMetadata>, 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<KbResolvedTokenMetadata> {
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<std::option::Option<KbResolvedTokenMetadata>, 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<std::string::String, crate::KbError> {
let program_id_result = <solana_sdk::pubkey::Pubkey as std::str::FromStr>::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 = <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::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<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 kb_parse_metaplex_token_metadata_account(
bytes: &[u8],
) -> std::option::Option<KbResolvedTokenMetadata> {
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<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 kb_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 kb_decode_base64_standard(text: &str) -> Result<std::vec::Vec<u8>, 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<u8> {
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<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 kb_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 = kb_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 = 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<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]);
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<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::KbDatabase> {
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);
}
}