0.7.27 +Refactor
This commit is contained in:
@@ -6,17 +6,17 @@
|
||||
//! 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";
|
||||
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";
|
||||
|
||||
/// Summary produced by a token metadata backfill pass.
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct KbTokenMetadataBackfillResult {
|
||||
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.
|
||||
@@ -43,8 +43,8 @@ pub struct KbTokenMetadataBackfillResult {
|
||||
pub pair_symbol_missing_token_count: usize,
|
||||
}
|
||||
|
||||
impl KbTokenMetadataBackfillResult {
|
||||
fn merge(&mut self, other: &KbTokenMetadataBackfillResult) {
|
||||
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;
|
||||
@@ -62,17 +62,17 @@ impl KbTokenMetadataBackfillResult {
|
||||
|
||||
/// Service that enriches persisted token rows with mint and display metadata.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KbTokenMetadataBackfillService {
|
||||
database: std::sync::Arc<crate::KbDatabase>,
|
||||
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 KbTokenMetadataBackfillService {
|
||||
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::KbDatabase>,
|
||||
database: std::sync::Arc<crate::Database>,
|
||||
http_role: std::string::String,
|
||||
) -> Self {
|
||||
return Self {
|
||||
@@ -83,7 +83,7 @@ impl KbTokenMetadataBackfillService {
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
pub fn new_local(database: std::sync::Arc<crate::Database>) -> Self {
|
||||
return Self {
|
||||
database,
|
||||
http_pool: None,
|
||||
@@ -95,14 +95,14 @@ impl KbTokenMetadataBackfillService {
|
||||
pub async fn backfill_missing_token_metadata(
|
||||
&self,
|
||||
limit: std::option::Option<i64>,
|
||||
) -> Result<crate::KbTokenMetadataBackfillResult, crate::KbError> {
|
||||
) -> Result<crate::TokenMetadataBackfillResult, crate::Error> {
|
||||
let tokens_result =
|
||||
crate::list_tokens_missing_metadata(self.database.as_ref(), limit).await;
|
||||
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::KbTokenMetadataBackfillResult::default();
|
||||
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 {
|
||||
@@ -119,7 +119,7 @@ impl KbTokenMetadataBackfillService {
|
||||
},
|
||||
}
|
||||
}
|
||||
let pair_symbol_result = crate::kb_refresh_pair_symbols(self.database.as_ref()).await;
|
||||
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;
|
||||
@@ -141,14 +141,12 @@ impl KbTokenMetadataBackfillService {
|
||||
pub async fn backfill_token_metadata_by_mint(
|
||||
&self,
|
||||
mint: &str,
|
||||
) -> Result<crate::KbTokenMetadataBackfillResult, crate::KbError> {
|
||||
) -> Result<crate::TokenMetadataBackfillResult, crate::Error> {
|
||||
let trimmed_mint = mint.trim();
|
||||
if trimmed_mint.is_empty() {
|
||||
return Err(crate::KbError::Config(
|
||||
"token metadata mint must not be empty".to_string(),
|
||||
));
|
||||
return Err(crate::Error::Config("token metadata mint must not be empty".to_string()));
|
||||
}
|
||||
let mut result = crate::KbTokenMetadataBackfillResult {
|
||||
let mut result = crate::TokenMetadataBackfillResult {
|
||||
total_token_count: 1,
|
||||
attempted_token_count: 1,
|
||||
local_metadata_count: 0,
|
||||
@@ -162,7 +160,8 @@ impl KbTokenMetadataBackfillService {
|
||||
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_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),
|
||||
@@ -170,17 +169,17 @@ impl KbTokenMetadataBackfillService {
|
||||
let token_existed = token_option.is_some();
|
||||
let mut token = match token_option {
|
||||
Some(token) => token,
|
||||
None => kb_build_empty_token_dto(trimmed_mint),
|
||||
None => 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);
|
||||
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 =
|
||||
kb_resolve_pump_fun_metadata_from_decoded_events(self.database.as_ref(), trimmed_mint)
|
||||
resolve_pump_fun_metadata_from_decoded_events(self.database.as_ref(), trimmed_mint)
|
||||
.await;
|
||||
match pump_fun_metadata_result {
|
||||
Ok(pump_fun_metadata_option) => {
|
||||
@@ -199,7 +198,7 @@ impl KbTokenMetadataBackfillService {
|
||||
},
|
||||
}
|
||||
if let Some(http_pool) = &self.http_pool {
|
||||
let mint_account_result = kb_fetch_mint_account_metadata(
|
||||
let mint_account_result = fetch_mint_account_metadata(
|
||||
http_pool.as_ref(),
|
||||
self.http_role.as_str(),
|
||||
trimmed_mint,
|
||||
@@ -221,8 +220,8 @@ impl KbTokenMetadataBackfillService {
|
||||
);
|
||||
},
|
||||
}
|
||||
if kb_should_try_metaplex_metadata(&token, &resolved) {
|
||||
let metaplex_result = kb_fetch_metaplex_metadata(
|
||||
if should_try_metaplex_metadata(&token, &resolved) {
|
||||
let metaplex_result = fetch_metaplex_metadata(
|
||||
http_pool.as_ref(),
|
||||
self.http_role.as_str(),
|
||||
trimmed_mint,
|
||||
@@ -246,14 +245,14 @@ impl KbTokenMetadataBackfillService {
|
||||
}
|
||||
}
|
||||
}
|
||||
let changed = kb_apply_resolved_metadata_to_token(&mut token, &resolved);
|
||||
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::upsert_token(self.database.as_ref(), &token).await;
|
||||
let upsert_result = crate::query_tokens_upsert(self.database.as_ref(), &token).await;
|
||||
match upsert_result {
|
||||
Ok(_) => {
|
||||
if changed || original_token.id.is_none() {
|
||||
@@ -272,7 +271,7 @@ impl KbTokenMetadataBackfillService {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct KbResolvedTokenMetadata {
|
||||
struct ResolvedTokenMetadata {
|
||||
symbol: std::option::Option<std::string::String>,
|
||||
name: std::option::Option<std::string::String>,
|
||||
decimals: std::option::Option<u8>,
|
||||
@@ -280,19 +279,18 @@ struct KbResolvedTokenMetadata {
|
||||
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() {
|
||||
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 kb_option_string_is_missing(self.name.as_deref()) && other.name.is_some() {
|
||||
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 kb_option_string_is_missing(self.token_program.as_deref())
|
||||
&& other.token_program.is_some()
|
||||
if option_string_is_missing(self.token_program.as_deref()) && other.token_program.is_some()
|
||||
{
|
||||
self.token_program = other.token_program;
|
||||
}
|
||||
@@ -302,8 +300,8 @@ impl KbResolvedTokenMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_build_empty_token_dto(mint: &str) -> crate::KbTokenDto {
|
||||
return crate::KbTokenDto::new(
|
||||
fn build_empty_token_dto(mint: &str) -> crate::TokenDto {
|
||||
return crate::TokenDto::new(
|
||||
mint.to_string(),
|
||||
None,
|
||||
None,
|
||||
@@ -313,11 +311,11 @@ fn kb_build_empty_token_dto(mint: &str) -> crate::KbTokenDto {
|
||||
);
|
||||
}
|
||||
|
||||
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()),
|
||||
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),
|
||||
@@ -325,9 +323,9 @@ fn kb_resolve_local_token_metadata(mint: &str) -> std::option::Option<KbResolved
|
||||
}
|
||||
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()),
|
||||
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),
|
||||
@@ -336,11 +334,13 @@ fn kb_resolve_local_token_metadata(mint: &str) -> std::option::Option<KbResolved
|
||||
return None;
|
||||
}
|
||||
|
||||
async fn kb_resolve_pump_fun_metadata_from_decoded_events(
|
||||
database: &crate::KbDatabase,
|
||||
async fn resolve_pump_fun_metadata_from_decoded_events(
|
||||
database: &crate::Database,
|
||||
mint: &str,
|
||||
) -> Result<std::option::Option<KbResolvedTokenMetadata>, crate::KbError> {
|
||||
let payload_result = crate::get_latest_pump_fun_create_payload_by_mint(database, mint).await;
|
||||
) -> 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),
|
||||
@@ -353,25 +353,25 @@ async fn kb_resolve_pump_fun_metadata_from_decoded_events(
|
||||
let payload = match payload_result {
|
||||
Ok(payload) => payload,
|
||||
Err(error) => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot parse pump.fun metadata payload for mint '{}': {}",
|
||||
mint, error
|
||||
)));
|
||||
},
|
||||
};
|
||||
return Ok(kb_extract_pump_fun_metadata_from_payload(&payload));
|
||||
return Ok(extract_pump_fun_metadata_from_payload(&payload));
|
||||
}
|
||||
|
||||
fn kb_extract_pump_fun_metadata_from_payload(
|
||||
fn 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"]);
|
||||
) -> 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(KbResolvedTokenMetadata {
|
||||
return Some(ResolvedTokenMetadata {
|
||||
symbol,
|
||||
name,
|
||||
decimals,
|
||||
@@ -380,23 +380,20 @@ fn kb_extract_pump_fun_metadata_from_payload(
|
||||
});
|
||||
}
|
||||
|
||||
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());
|
||||
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 kb_apply_resolved_metadata_to_token(
|
||||
token: &mut crate::KbTokenDto,
|
||||
resolved: &KbResolvedTokenMetadata,
|
||||
fn apply_resolved_metadata_to_token(
|
||||
token: &mut crate::TokenDto,
|
||||
resolved: &ResolvedTokenMetadata,
|
||||
) -> bool {
|
||||
let mut changed = false;
|
||||
if kb_option_string_is_missing(token.symbol.as_deref()) {
|
||||
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());
|
||||
@@ -404,7 +401,7 @@ fn kb_apply_resolved_metadata_to_token(
|
||||
}
|
||||
}
|
||||
}
|
||||
if kb_option_string_is_missing(token.name.as_deref()) {
|
||||
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());
|
||||
@@ -433,11 +430,11 @@ fn kb_apply_resolved_metadata_to_token(
|
||||
return changed;
|
||||
}
|
||||
|
||||
async fn kb_fetch_mint_account_metadata(
|
||||
async fn fetch_mint_account_metadata(
|
||||
http_pool: &crate::HttpEndpointPool,
|
||||
http_role: &str,
|
||||
mint: &str,
|
||||
) -> Result<std::option::Option<KbResolvedTokenMetadata>, crate::KbError> {
|
||||
) -> Result<std::option::Option<ResolvedTokenMetadata>, crate::Error> {
|
||||
let config = serde_json::json!({
|
||||
"encoding": "jsonParsed",
|
||||
"commitment": "confirmed"
|
||||
@@ -467,9 +464,9 @@ async fn kb_fetch_mint_account_metadata(
|
||||
.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 {
|
||||
.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,
|
||||
@@ -489,9 +486,9 @@ async fn kb_fetch_mint_account_metadata(
|
||||
return Ok(Some(resolved));
|
||||
}
|
||||
|
||||
fn kb_extract_token_2022_metadata_from_mint_account_value(
|
||||
fn extract_token_2022_metadata_from_mint_account_value(
|
||||
value: &serde_json::Value,
|
||||
) -> std::option::Option<KbResolvedTokenMetadata> {
|
||||
) -> std::option::Option<ResolvedTokenMetadata> {
|
||||
let extensions_option = value
|
||||
.get("data")
|
||||
.and_then(|data| return data.get("parsed"))
|
||||
@@ -519,12 +516,12 @@ fn kb_extract_token_2022_metadata_from_mint_account_value(
|
||||
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"]);
|
||||
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(KbResolvedTokenMetadata {
|
||||
return Some(ResolvedTokenMetadata {
|
||||
symbol,
|
||||
name,
|
||||
decimals: None,
|
||||
@@ -535,12 +532,12 @@ fn kb_extract_token_2022_metadata_from_mint_account_value(
|
||||
return None;
|
||||
}
|
||||
|
||||
async fn kb_fetch_metaplex_metadata(
|
||||
async fn 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);
|
||||
) -> 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),
|
||||
@@ -564,29 +561,29 @@ async fn kb_fetch_metaplex_metadata(
|
||||
if value.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
let data_text_option = kb_extract_base64_account_data(value);
|
||||
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 = kb_decode_base64_standard(data_text.as_str());
|
||||
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(kb_parse_metaplex_token_metadata_account(&bytes));
|
||||
return Ok(parse_metaplex_token_metadata_account(&bytes));
|
||||
}
|
||||
|
||||
fn kb_derive_metaplex_metadata_address(mint: &str) -> Result<std::string::String, crate::KbError> {
|
||||
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(
|
||||
KB_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
|
||||
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!(
|
||||
return Err(crate::Error::Config(format!(
|
||||
"invalid metaplex metadata program id '{}': {}",
|
||||
KB_METAPLEX_TOKEN_METADATA_PROGRAM_ID, error
|
||||
METAPLEX_TOKEN_METADATA_PROGRAM_ID, error
|
||||
)));
|
||||
},
|
||||
};
|
||||
@@ -594,10 +591,7 @@ fn kb_derive_metaplex_metadata_address(mint: &str) -> Result<std::string::String
|
||||
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
|
||||
)));
|
||||
return Err(crate::Error::Config(format!("invalid token mint '{}': {}", mint, error)));
|
||||
},
|
||||
};
|
||||
let (metadata_address, _) = solana_sdk::pubkey::Pubkey::find_program_address(
|
||||
@@ -607,7 +601,7 @@ fn kb_derive_metaplex_metadata_address(mint: &str) -> Result<std::string::String
|
||||
return Ok(metadata_address.to_string());
|
||||
}
|
||||
|
||||
fn kb_extract_base64_account_data(
|
||||
fn extract_base64_account_data(
|
||||
value: &serde_json::Value,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
let data = match value.get("data") {
|
||||
@@ -624,28 +618,28 @@ fn kb_extract_base64_account_data(
|
||||
return data.as_str().map(|value| return value.to_string());
|
||||
}
|
||||
|
||||
fn kb_parse_metaplex_token_metadata_account(
|
||||
fn parse_metaplex_token_metadata_account(
|
||||
bytes: &[u8],
|
||||
) -> std::option::Option<KbResolvedTokenMetadata> {
|
||||
) -> std::option::Option<ResolvedTokenMetadata> {
|
||||
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_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) => kb_clean_metaplex_string(name.as_str()),
|
||||
Some(name) => clean_metaplex_string(name.as_str()),
|
||||
None => None,
|
||||
};
|
||||
let symbol = match symbol_option {
|
||||
Some(symbol) => kb_clean_metaplex_string(symbol.as_str()),
|
||||
Some(symbol) => clean_metaplex_string(symbol.as_str()),
|
||||
None => None,
|
||||
};
|
||||
if name.is_none() && symbol.is_none() {
|
||||
return None;
|
||||
}
|
||||
return Some(KbResolvedTokenMetadata {
|
||||
return Some(ResolvedTokenMetadata {
|
||||
symbol,
|
||||
name,
|
||||
decimals: None,
|
||||
@@ -654,10 +648,7 @@ fn kb_parse_metaplex_token_metadata_account(
|
||||
});
|
||||
}
|
||||
|
||||
fn kb_read_borsh_string(
|
||||
bytes: &[u8],
|
||||
offset: &mut usize,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
fn read_borsh_string(bytes: &[u8], offset: &mut usize) -> std::option::Option<std::string::String> {
|
||||
if bytes.len() < *offset + 4 {
|
||||
return None;
|
||||
}
|
||||
@@ -680,12 +671,12 @@ fn kb_read_borsh_string(
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_clean_metaplex_string(value: &str) -> std::option::Option<std::string::String> {
|
||||
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 kb_decode_base64_standard(text: &str) -> Result<std::vec::Vec<u8>, crate::KbError> {
|
||||
fn decode_base64_standard(text: &str) -> Result<std::vec::Vec<u8>, crate::Error> {
|
||||
let mut output = std::vec::Vec::new();
|
||||
let mut group = [0u8; 4];
|
||||
let mut group_len = 0usize;
|
||||
@@ -694,11 +685,11 @@ fn kb_decode_base64_standard(text: &str) -> Result<std::vec::Vec<u8>, crate::KbE
|
||||
if byte == b'\r' || byte == b'\n' || byte == b'\t' || byte == b' ' {
|
||||
continue;
|
||||
}
|
||||
let value_option = kb_base64_value(byte);
|
||||
let value_option = base64_value(byte);
|
||||
let value = match value_option {
|
||||
Some(value) => value,
|
||||
None => {
|
||||
return Err(crate::KbError::Json(format!(
|
||||
return Err(crate::Error::Json(format!(
|
||||
"invalid base64 character '{}'",
|
||||
byte as char
|
||||
)));
|
||||
@@ -723,14 +714,14 @@ fn kb_decode_base64_standard(text: &str) -> Result<std::vec::Vec<u8>, crate::KbE
|
||||
}
|
||||
}
|
||||
if group_len != 0 {
|
||||
return Err(crate::KbError::Json(
|
||||
return Err(crate::Error::Json(
|
||||
"invalid base64 length: trailing partial group".to_string(),
|
||||
));
|
||||
}
|
||||
return Ok(output);
|
||||
}
|
||||
|
||||
fn kb_base64_value(byte: u8) -> std::option::Option<u8> {
|
||||
fn 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),
|
||||
@@ -742,7 +733,7 @@ fn kb_base64_value(byte: u8) -> std::option::Option<u8> {
|
||||
}
|
||||
}
|
||||
|
||||
fn kb_extract_string_by_candidate_keys(
|
||||
fn extract_string_by_candidate_keys(
|
||||
payload: &serde_json::Value,
|
||||
candidate_keys: &[&str],
|
||||
) -> std::option::Option<std::string::String> {
|
||||
@@ -766,7 +757,7 @@ fn kb_extract_string_by_candidate_keys(
|
||||
return None;
|
||||
}
|
||||
|
||||
fn kb_extract_u8_by_candidate_keys(
|
||||
fn extract_u8_by_candidate_keys(
|
||||
payload: &serde_json::Value,
|
||||
candidate_keys: &[&str],
|
||||
) -> std::option::Option<u8> {
|
||||
@@ -777,7 +768,7 @@ fn kb_extract_u8_by_candidate_keys(
|
||||
None => continue,
|
||||
};
|
||||
if let Some(number) = value.as_u64() {
|
||||
let converted = kb_u64_to_u8(number);
|
||||
let converted = u64_to_u8(number);
|
||||
if converted.is_some() {
|
||||
return converted;
|
||||
}
|
||||
@@ -788,7 +779,7 @@ fn kb_extract_u8_by_candidate_keys(
|
||||
Ok(parsed) => parsed,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let converted = kb_u64_to_u8(parsed);
|
||||
let converted = u64_to_u8(parsed);
|
||||
if converted.is_some() {
|
||||
return converted;
|
||||
}
|
||||
@@ -797,14 +788,14 @@ fn kb_extract_u8_by_candidate_keys(
|
||||
return None;
|
||||
}
|
||||
|
||||
fn kb_option_string_is_missing(value: std::option::Option<&str>) -> bool {
|
||||
fn 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> {
|
||||
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),
|
||||
@@ -819,29 +810,29 @@ mod tests {
|
||||
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);
|
||||
push_borsh_string(&mut bytes, name);
|
||||
push_borsh_string(&mut bytes, symbol);
|
||||
push_borsh_string(&mut bytes, uri);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
fn kb_push_borsh_string(bytes: &mut std::vec::Vec<u8>, value: &str) {
|
||||
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::KbDatabase> {
|
||||
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::KbDatabaseConfig {
|
||||
let config = crate::DatabaseConfig {
|
||||
enabled: true,
|
||||
backend: crate::KbDatabaseBackend::Sqlite,
|
||||
sqlite: crate::KbSqliteDatabaseConfig {
|
||||
backend: crate::DatabaseBackend::Sqlite,
|
||||
sqlite: crate::SqliteDatabaseConfig {
|
||||
path: database_path.to_string_lossy().to_string(),
|
||||
create_if_missing: true,
|
||||
busy_timeout_ms: 5000,
|
||||
@@ -850,7 +841,7 @@ mod tests {
|
||||
use_wal: true,
|
||||
},
|
||||
};
|
||||
let database_result = crate::KbDatabase::connect_and_initialize(&config).await;
|
||||
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),
|
||||
@@ -861,7 +852,7 @@ mod tests {
|
||||
#[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_option = super::resolve_local_token_metadata(mint.as_str());
|
||||
let metadata = match metadata_option {
|
||||
Some(metadata) => metadata,
|
||||
None => panic!("WSOL metadata must resolve"),
|
||||
@@ -880,7 +871,7 @@ mod tests {
|
||||
"name": "Pump Example",
|
||||
"uri": "https://example.invalid/pump.json"
|
||||
});
|
||||
let metadata_option = super::kb_extract_pump_fun_metadata_from_payload(&payload);
|
||||
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"),
|
||||
@@ -908,8 +899,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
});
|
||||
let metadata_option =
|
||||
super::kb_extract_token_2022_metadata_from_mint_account_value(&account);
|
||||
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"),
|
||||
@@ -920,7 +910,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn base64_decoder_decodes_standard_payload() {
|
||||
let decoded_result = super::kb_decode_base64_standard("AQIDBA==");
|
||||
let decoded_result = super::decode_base64_standard("AQIDBA==");
|
||||
let decoded = match decoded_result {
|
||||
Ok(decoded) => decoded,
|
||||
Err(error) => panic!("base64 decode must succeed: {}", error),
|
||||
@@ -935,7 +925,7 @@ mod tests {
|
||||
"EXM\0\0",
|
||||
"https://example.invalid/token.json",
|
||||
);
|
||||
let metadata_option = super::kb_parse_metaplex_token_metadata_account(&bytes);
|
||||
let metadata_option = super::parse_metaplex_token_metadata_account(&bytes);
|
||||
let metadata = match metadata_option {
|
||||
Some(metadata) => metadata,
|
||||
None => panic!("metadata must parse"),
|
||||
@@ -947,7 +937,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn local_backfill_updates_wsol_without_http() {
|
||||
let database = make_database().await;
|
||||
let token = crate::KbTokenDto::new(
|
||||
let token = crate::TokenDto::new(
|
||||
crate::WSOL_MINT_ID.to_string(),
|
||||
None,
|
||||
None,
|
||||
@@ -955,20 +945,22 @@ mod tests {
|
||||
crate::SPL_TOKEN_PROGRAM_ID.to_string(),
|
||||
false,
|
||||
);
|
||||
let upsert_result = crate::upsert_token(database.as_ref(), &token).await;
|
||||
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::KbTokenMetadataBackfillService::new_local(database.clone());
|
||||
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::get_token_by_mint(database.as_ref(), crate::WSOL_MINT_ID.to_string().as_str())
|
||||
.await;
|
||||
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),
|
||||
@@ -986,7 +978,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn local_backfill_does_not_overwrite_existing_display_metadata() {
|
||||
let database = make_database().await;
|
||||
let token = crate::KbTokenDto::new(
|
||||
let token = crate::TokenDto::new(
|
||||
crate::WSOL_MINT_ID.to_string(),
|
||||
Some("SOL".to_string()),
|
||||
Some("Custom Sol".to_string()),
|
||||
@@ -994,20 +986,22 @@ mod tests {
|
||||
crate::SPL_TOKEN_PROGRAM_ID.to_string(),
|
||||
false,
|
||||
);
|
||||
let upsert_result = crate::upsert_token(database.as_ref(), &token).await;
|
||||
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::KbTokenMetadataBackfillService::new_local(database.clone());
|
||||
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::get_token_by_mint(database.as_ref(), crate::WSOL_MINT_ID.to_string().as_str())
|
||||
.await;
|
||||
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),
|
||||
|
||||
Reference in New Issue
Block a user