0.7.38
This commit is contained in:
@@ -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";
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user