This commit is contained in:
2026-05-14 17:44:01 +02:00
parent 403f271083
commit 3f6d2e9f7f
21 changed files with 775 additions and 88 deletions

View File

@@ -143,6 +143,21 @@ pub const ZK_ELGAMAL_PROOF_PROGRAM_ID: &str = "ZkE1Gama1Proof1111111111111111111
/// @see solana_sdk::pubkey::Pubkey = spl_token_interface::native_mint::ID
pub const WSOL_MINT_ID: &str = "So11111111111111111111111111111111111111112";
/// Canonical Solana USDC mint identifier.
pub const USDC_MINT_ID: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
/// Canonical Solana USDT mint identifier.
pub const USDT_MINT_ID: &str = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB";
/// Canonical Jupiter governance token mint identifier.
pub const JUP_MINT_ID: &str = "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN";
/// Canonical Raydium token mint identifier.
pub const RAY_MINT_ID: &str = "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R";
/// Canonical Bonk token mint identifier.
pub const BONK_MINT_ID: &str = "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263";
/// DexLab Swap/Pool program id. ("DSwpgjMvXhtGn6BsbqmacdBZyfLj6jSWf3HJpdJtmg6N").
pub const DEXLAB_PROGRAM_ID: &str = "DSwpgjMvXhtGn6BsbqmacdBZyfLj6jSWf3HJpdJtmg6N";

View File

@@ -44,6 +44,7 @@ pub use dtos::LocalPairGapDiagnosticSampleDto;
pub use dtos::LocalPairTradingReadinessDiagnosticSummaryDto;
pub use dtos::LocalPipelineDiagnosticCountersDto;
pub use dtos::LocalPipelineDiagnosticSummaryDto;
pub use dtos::LocalTokenMetadataGapDiagnosticSampleDto;
pub use dtos::ObservedTokenDto;
pub use dtos::OnchainObservationDto;
pub use dtos::PairAnalyticSignalDto;
@@ -170,6 +171,7 @@ pub use queries::query_local_pair_without_candle_diagnostic_list_samples;
pub use queries::query_local_pair_without_trade_diagnostic_list_samples;
pub use queries::query_local_pipeline_diagnostic_get_counters;
pub use queries::query_local_pipeline_diagnostic_list_summaries;
pub use queries::query_local_token_metadata_gap_diagnostic_list_samples;
pub use queries::query_observed_tokens_get_by_mint;
pub use queries::query_observed_tokens_list;
pub use queries::query_observed_tokens_upsert;

View File

@@ -58,6 +58,7 @@ pub(crate) use local_pipeline_diagnostics::LocalPairDiagnosticSummaryRow;
pub(crate) use local_pipeline_diagnostics::LocalPairGapDiagnosticSampleRow;
pub(crate) use local_pipeline_diagnostics::LocalPairTradingReadinessDiagnosticSummaryRow;
pub(crate) use local_pipeline_diagnostics::LocalPipelineDiagnosticCountersRow;
pub(crate) use local_pipeline_diagnostics::LocalTokenMetadataGapDiagnosticSampleRow;
pub use analysis_signal::AnalysisSignalDto;
pub use chain_instruction::ChainInstructionDto;
@@ -88,6 +89,7 @@ pub use local_pipeline_diagnostics::LocalPairGapDiagnosticSampleDto;
pub use local_pipeline_diagnostics::LocalPairTradingReadinessDiagnosticSummaryDto;
pub use local_pipeline_diagnostics::LocalPipelineDiagnosticCountersDto;
pub use local_pipeline_diagnostics::LocalPipelineDiagnosticSummaryDto;
pub use local_pipeline_diagnostics::LocalTokenMetadataGapDiagnosticSampleDto;
pub use observed_token::ObservedTokenDto;
pub use onchain_observation::OnchainObservationDto;
pub use pair::PairDto;

View File

@@ -138,6 +138,8 @@ pub struct LocalPipelineDiagnosticSummaryDto {
/// Missing trade events grouped by diagnostic reason.
pub missing_trade_event_reason_summaries:
std::vec::Vec<crate::LocalMissingTradeEventReasonSummaryDto>,
/// Prioritized samples of tokens whose display metadata is still incomplete.
pub token_metadata_gap_samples: std::vec::Vec<crate::LocalTokenMetadataGapDiagnosticSampleDto>,
/// Total pairs with only non-actionable missing trade events.
pub non_actionable_pair_count: i64,
/// Pair summaries for non-actionable missing trade events.
@@ -712,6 +714,35 @@ pub struct LocalPairGapDiagnosticSampleDto {
pub pair_candle_count: i64,
}
/// Prioritized sample of an incomplete token metadata row.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct LocalTokenMetadataGapDiagnosticSampleDto {
/// Token id.
pub token_id: i64,
/// Mint address.
pub mint: std::string::String,
/// Current symbol, when present.
pub symbol: std::option::Option<std::string::String>,
/// Current name, when present.
pub name: std::option::Option<std::string::String>,
/// Current mint decimals, when known.
pub decimals: std::option::Option<i64>,
/// Token program id.
pub token_program: std::string::String,
/// Whether this token row is flagged as quote token.
pub is_quote_token: bool,
/// Whether the token appears in at least one pair with materialized trades.
pub used_by_trade_materialized_pair: bool,
/// Whether the token appears on the quote side of at least one pair.
pub used_as_quote_token: bool,
/// Number of trade-materialized pairs using this token.
pub trade_materialized_pair_count: i64,
/// Number of catalog pairs using this token.
pub total_pair_count: i64,
/// Human-readable prioritization bucket.
pub priority: std::string::String,
}
/// SQL row for missing trade event reason summaries.
#[derive(Debug, Clone, sqlx::FromRow)]
pub(crate) struct LocalMissingTradeEventReasonSummaryRow {
@@ -803,3 +834,20 @@ pub(crate) struct LocalPairGapDiagnosticSampleRow {
pub(crate) trade_event_count: i64,
pub(crate) pair_candle_count: i64,
}
/// SQL row for incomplete token metadata samples.
#[derive(Debug, Clone, sqlx::FromRow)]
pub(crate) struct LocalTokenMetadataGapDiagnosticSampleRow {
pub(crate) token_id: i64,
pub(crate) mint: std::string::String,
pub(crate) symbol: std::option::Option<std::string::String>,
pub(crate) name: std::option::Option<std::string::String>,
pub(crate) decimals: std::option::Option<i64>,
pub(crate) token_program: std::string::String,
pub(crate) is_quote_token: i64,
pub(crate) used_by_trade_materialized_pair: i64,
pub(crate) used_as_quote_token: i64,
pub(crate) trade_materialized_pair_count: i64,
pub(crate) total_pair_count: i64,
pub(crate) priority: std::string::String,
}

View File

@@ -102,6 +102,7 @@ pub use local_pipeline_diagnostics::query_local_pair_without_candle_diagnostic_l
pub use local_pipeline_diagnostics::query_local_pair_without_trade_diagnostic_list_samples;
pub use local_pipeline_diagnostics::query_local_pipeline_diagnostic_get_counters;
pub use local_pipeline_diagnostics::query_local_pipeline_diagnostic_list_summaries;
pub use local_pipeline_diagnostics::query_local_token_metadata_gap_diagnostic_list_samples;
pub use observed_token::query_observed_tokens_get_by_mint;
pub use observed_token::query_observed_tokens_list;
pub use observed_token::query_observed_tokens_upsert;

View File

@@ -1678,6 +1678,104 @@ LIMIT ?
}
}
/// Lists prioritized samples of tokens whose metadata is still incomplete.
pub async fn query_local_token_metadata_gap_diagnostic_list_samples(
database: &crate::Database,
limit: i64,
) -> Result<std::vec::Vec<crate::LocalTokenMetadataGapDiagnosticSampleDto>, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let rows_result = sqlx::query_as::<
sqlx::Sqlite,
crate::db::dtos::LocalTokenMetadataGapDiagnosticSampleRow,
>(
r#"
WITH token_pair_usage AS (
SELECT
token.id AS token_id,
COUNT(DISTINCT pair.id) AS total_pair_count,
COUNT(DISTINCT CASE WHEN te.id IS NOT NULL THEN pair.id END) AS trade_materialized_pair_count,
COUNT(DISTINCT CASE WHEN pair.quote_token_id = token.id THEN pair.id END) AS quote_pair_count
FROM k_sol_tokens token
LEFT JOIN k_sol_pairs pair
ON pair.base_token_id = token.id
OR pair.quote_token_id = token.id
LEFT JOIN k_sol_trade_events te ON te.pair_id = pair.id
GROUP BY token.id
)
SELECT
token.id AS token_id,
token.mint AS mint,
token.symbol AS symbol,
token.name AS name,
token.decimals AS decimals,
token.token_program AS token_program,
token.is_quote_token AS is_quote_token,
CASE WHEN usage.trade_materialized_pair_count > 0 THEN 1 ELSE 0 END AS used_by_trade_materialized_pair,
CASE WHEN usage.quote_pair_count > 0 THEN 1 ELSE 0 END AS used_as_quote_token,
COALESCE(usage.trade_materialized_pair_count, 0) AS trade_materialized_pair_count,
COALESCE(usage.total_pair_count, 0) AS total_pair_count,
CASE
WHEN usage.trade_materialized_pair_count > 0 AND usage.quote_pair_count > 0 THEN 'tradable_quote_missing_metadata'
WHEN usage.trade_materialized_pair_count > 0 THEN 'tradable_token_missing_metadata'
WHEN usage.quote_pair_count > 0 THEN 'quote_token_missing_metadata'
ELSE 'catalog_token_missing_metadata'
END AS priority
FROM k_sol_tokens token
LEFT JOIN token_pair_usage usage ON usage.token_id = token.id
WHERE token.symbol IS NULL
OR TRIM(token.symbol) = ''
OR token.name IS NULL
OR TRIM(token.name) = ''
ORDER BY
CASE
WHEN usage.trade_materialized_pair_count > 0 AND usage.quote_pair_count > 0 THEN 1
WHEN usage.trade_materialized_pair_count > 0 THEN 2
WHEN usage.quote_pair_count > 0 THEN 3
ELSE 4
END,
usage.trade_materialized_pair_count DESC,
usage.total_pair_count DESC,
token.mint
LIMIT ?
"#,
)
.bind(limit)
.fetch_all(pool)
.await;
let rows = match rows_result {
Ok(rows) => rows,
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot list token metadata gap diagnostic samples on sqlite: {}",
error
)));
},
};
let mut samples = std::vec::Vec::new();
for row in rows {
samples.push(crate::LocalTokenMetadataGapDiagnosticSampleDto {
token_id: row.token_id,
mint: row.mint,
symbol: row.symbol,
name: row.name,
decimals: row.decimals,
token_program: row.token_program,
is_quote_token: sqlite_i64_to_bool(row.is_quote_token),
used_by_trade_materialized_pair: sqlite_i64_to_bool(
row.used_by_trade_materialized_pair,
),
used_as_quote_token: sqlite_i64_to_bool(row.used_as_quote_token),
trade_materialized_pair_count: row.trade_materialized_pair_count,
total_pair_count: row.total_pair_count,
priority: row.priority,
});
}
return Ok(samples);
},
}
}
/// Lists samples of pairs without trade events.
pub async fn query_local_pair_without_trade_diagnostic_list_samples(
database: &crate::Database,

View File

@@ -266,6 +266,16 @@ pub use constants::SYSVAR_STAKE_HISTORY_PROGRAM_ID;
/// Vote program identifier. ("Vote111111111111111111111111111111111111111").
/// @see solana_sdk::pubkey::Pubkey = solana_sdk_ids::vote::ID
pub use constants::VOTE_PROGRAM_ID;
/// Canonical Bonk token mint identifier.
pub use constants::BONK_MINT_ID;
/// Canonical Jupiter governance token mint identifier.
pub use constants::JUP_MINT_ID;
/// Canonical Raydium token mint identifier.
pub use constants::RAY_MINT_ID;
/// Canonical Solana USDC mint identifier.
pub use constants::USDC_MINT_ID;
/// Canonical Solana USDT mint identifier.
pub use constants::USDT_MINT_ID;
/// Wrapped SOL mint identifier. ("So11111111111111111111111111111111111111112").
/// @see solana_sdk::pubkey::Pubkey = spl_token_interface::native_mint::ID
pub use constants::WSOL_MINT_ID;
@@ -375,6 +385,8 @@ pub use db::LocalPairTradingReadinessDiagnosticSummaryDto;
pub use db::LocalPipelineDiagnosticCountersDto;
/// Local pipeline diagnostics summary.
pub use db::LocalPipelineDiagnosticSummaryDto;
/// Prioritized sample of an incomplete token metadata row.
pub use db::LocalTokenMetadataGapDiagnosticSampleDto;
/// Source family for one on-chain observation.
pub use db::ObservationSourceKind;
/// Application-facing observed token DTO.
@@ -613,6 +625,8 @@ pub use db::query_local_pair_without_trade_diagnostic_list_samples;
pub use db::query_local_pipeline_diagnostic_get_counters;
/// Lists local DEX diagnostic summaries.
pub use db::query_local_pipeline_diagnostic_list_summaries;
/// Lists prioritized token metadata gap diagnostic samples.
pub use db::query_local_token_metadata_gap_diagnostic_list_samples;
/// Reads one observed token by mint.
pub use db::query_observed_tokens_get_by_mint;
/// Lists observed tokens ordered by newest first.

View File

@@ -75,6 +75,16 @@ impl LocalPipelineDiagnosticsService {
Ok(summaries) => summaries,
Err(error) => return Err(error),
};
let token_metadata_gap_samples_result =
crate::query_local_token_metadata_gap_diagnostic_list_samples(
self.database.as_ref(),
sample_limit,
)
.await;
let token_metadata_gap_samples = match token_metadata_gap_samples_result {
Ok(samples) => samples,
Err(error) => return Err(error),
};
let non_actionable_pair_summaries_result =
crate::query_local_non_actionable_pair_diagnostic_list_summaries(
self.database.as_ref(),
@@ -206,6 +216,7 @@ impl LocalPipelineDiagnosticsService {
decoded_event_summaries,
event_classification_summaries,
missing_trade_event_reason_summaries,
token_metadata_gap_samples,
non_actionable_pair_count: counters.non_actionable_pair_count,
non_actionable_pair_summaries,
missing_trade_event_samples,

View File

@@ -266,6 +266,17 @@ impl LocalPipelineValidationConfig {
config.profile_code = "0.7.37_token_metadata_catalog_enrichment".to_string();
return config;
}
/// Builds the `0.7.38` token metadata gap prioritization validation config.
///
/// This profile keeps the `0.7.37` metadata counters and exposes
/// prioritized token metadata gap samples so the next backfill targets are
/// visible without making missing metadata a blocking validation issue.
pub fn v0_7_38_token_metadata_gap_prioritization() -> Self {
let mut config = Self::v0_7_37_token_metadata_catalog_enrichment();
config.profile_code = "0.7.38_token_metadata_gap_prioritization".to_string();
return config;
}
}
/// A single local pipeline validation issue.
@@ -485,6 +496,15 @@ impl LocalPipelineValidationService {
crate::LocalPipelineValidationConfig::v0_7_37_token_metadata_catalog_enrichment();
return self.validate_current_database(&config).await;
}
/// Diagnoses the current database with the `0.7.38` metadata-gap profile.
pub async fn validate_v0_7_38_current_database(
&self,
) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
let config =
crate::LocalPipelineValidationConfig::v0_7_38_token_metadata_gap_prioritization();
return self.validate_current_database(&config).await;
}
}
/// Validates a diagnostics summary without performing database access.
@@ -606,7 +626,8 @@ pub fn validate_local_pipeline_diagnostics_summary(
== "0.7.34_non_trade_liquidity_lifecycle"
|| config.profile_code == "0.7.35_non_trade_fee_reward_admin"
|| config.profile_code == "0.7.36_meteora_family_consolidation"
|| config.profile_code == "0.7.37_token_metadata_catalog_enrichment";
|| config.profile_code == "0.7.37_token_metadata_catalog_enrichment"
|| config.profile_code == "0.7.38_token_metadata_gap_prioritization";
if config.require_all_expected_dexes || missing_expected_dex_is_warning {
for expected_dex_code in &expected_dex_codes {
if !observed_dex_codes.contains(expected_dex_code) {
@@ -1040,6 +1061,7 @@ mod tests {
decoded_event_summaries: vec![],
event_classification_summaries: vec![],
missing_trade_event_reason_summaries: vec![],
token_metadata_gap_samples: vec![],
non_actionable_pair_summaries: vec![],
missing_trade_event_samples: vec![],
duplicate_decoded_event_trade_samples: vec![],
@@ -1300,6 +1322,39 @@ mod tests {
assert_eq!(report.stable_quote_pair_count, 2);
}
#[test]
fn validation_accepts_0_7_38_metadata_gap_samples() {
let mut summary = make_0_7_28_summary_with_meteora();
summary.token_count = 108;
summary.token_metadata_missing_count = 102;
summary.tradable_token_metadata_missing_count = 6;
summary.quote_token_metadata_missing_count = 5;
summary
.token_metadata_gap_samples
.push(crate::LocalTokenMetadataGapDiagnosticSampleDto {
token_id: 42,
mint: "MissingMetaMint111".to_string(),
symbol: None,
name: None,
decimals: Some(6),
token_program: crate::SPL_TOKEN_PROGRAM_ID.to_string(),
is_quote_token: false,
used_by_trade_materialized_pair: true,
used_as_quote_token: false,
trade_materialized_pair_count: 2,
total_pair_count: 3,
priority: "tradable_token_missing_metadata".to_string(),
});
let config =
crate::LocalPipelineValidationConfig::v0_7_38_token_metadata_gap_prioritization();
let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
assert!(report.validation_passed);
assert_eq!(report.validation_profile_code, "0.7.38_token_metadata_gap_prioritization");
assert_eq!(report.blocking_issue_count, 0);
assert_eq!(report.token_metadata_missing_count, 102);
assert_eq!(summary.token_metadata_gap_samples.len(), 1);
}
#[test]
fn validation_rejects_0_7_33_pair_trading_readiness_mismatch() {
let mut summary = make_0_7_28_summary_with_meteora();

View File

@@ -13,6 +13,67 @@ 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")]
@@ -321,15 +382,16 @@ fn resolve_local_token_metadata(mint: &str) -> std::option::Option<ResolvedToken
is_quote_token: Some(true),
});
}
let wsol_mint = crate::WSOL_MINT_ID.to_string();
if mint == wsol_mint {
return Some(ResolvedTokenMetadata {
symbol: Some(WRAPPED_SOL_SYMBOL.to_string()),
name: Some(WRAPPED_SOL_NAME.to_string()),
decimals: Some(9),
token_program: Some(crate::SPL_TOKEN_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;
}
@@ -820,6 +882,26 @@ mod tests {
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!({
@@ -932,6 +1014,144 @@ mod tests {
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;