From 089da29b9bfed827b29122ae9521becf94be8333 Mon Sep 17 00:00:00 2001 From: defiQUG Date: Sun, 29 Mar 2026 18:25:54 -0700 Subject: [PATCH] fix(token-aggregation): tolerate missing market data tables --- .../database/repositories/market-data-repo.ts | 246 ++++++++++-------- .../src/database/repositories/token-repo.ts | 234 ++++++++++------- 2 files changed, 278 insertions(+), 202 deletions(-) diff --git a/services/token-aggregation/src/database/repositories/market-data-repo.ts b/services/token-aggregation/src/database/repositories/market-data-repo.ts index 2c19cfb..917c700 100644 --- a/services/token-aggregation/src/database/repositories/market-data-repo.ts +++ b/services/token-aggregation/src/database/repositories/market-data-repo.ts @@ -9,122 +9,160 @@ export class MarketDataRepository { this.pool = getDatabasePool(); } - async getMarketData(chainId: number, tokenAddress: string): Promise { - const result = await this.pool.query( - `SELECT chain_id, token_address, price_usd, price_change_24h, volume_24h, volume_7d, volume_30d, - market_cap_usd, liquidity_usd, holders_count, transfers_24h, last_updated - FROM token_market_data - WHERE chain_id = $1 AND token_address = $2`, - [chainId, tokenAddress.toLowerCase()] - ); - - if (result.rows.length === 0) { - return null; + private isMissingRelationError(error: unknown): boolean { + if (!error || typeof error !== 'object') { + return false; } - const row = result.rows[0]; - return { - chainId: row.chain_id, - tokenAddress: row.token_address, - priceUsd: row.price_usd ? parseFloat(row.price_usd) : undefined, - priceChange24h: row.price_change_24h ? parseFloat(row.price_change_24h) : undefined, - volume24h: parseFloat(row.volume_24h || '0'), - volume7d: parseFloat(row.volume_7d || '0'), - volume30d: parseFloat(row.volume_30d || '0'), - marketCapUsd: row.market_cap_usd ? parseFloat(row.market_cap_usd) : undefined, - liquidityUsd: parseFloat(row.liquidity_usd || '0'), - holdersCount: row.holders_count || 0, - transfers24h: row.transfers_24h || 0, - lastUpdated: row.last_updated, - }; + const code = (error as { code?: string }).code; + const message = (error as { message?: string }).message || ''; + return code === '42P01' || (message.includes('relation "') && message.includes('" does not exist')); + } + + async getMarketData(chainId: number, tokenAddress: string): Promise { + try { + const result = await this.pool.query( + `SELECT chain_id, token_address, price_usd, price_change_24h, volume_24h, volume_7d, volume_30d, + market_cap_usd, liquidity_usd, holders_count, transfers_24h, last_updated + FROM token_market_data + WHERE chain_id = $1 AND token_address = $2`, + [chainId, tokenAddress.toLowerCase()] + ); + + if (result.rows.length === 0) { + return null; + } + + const row = result.rows[0]; + return { + chainId: row.chain_id, + tokenAddress: row.token_address, + priceUsd: row.price_usd ? parseFloat(row.price_usd) : undefined, + priceChange24h: row.price_change_24h ? parseFloat(row.price_change_24h) : undefined, + volume24h: parseFloat(row.volume_24h || '0'), + volume7d: parseFloat(row.volume_7d || '0'), + volume30d: parseFloat(row.volume_30d || '0'), + marketCapUsd: row.market_cap_usd ? parseFloat(row.market_cap_usd) : undefined, + liquidityUsd: parseFloat(row.liquidity_usd || '0'), + holdersCount: row.holders_count || 0, + transfers24h: row.transfers_24h || 0, + lastUpdated: row.last_updated, + }; + } catch (error) { + if (this.isMissingRelationError(error)) { + return null; + } + throw error; + } } async upsertMarketData(data: TokenMarketData): Promise { - await this.pool.query( - `INSERT INTO token_market_data ( - chain_id, token_address, price_usd, price_change_24h, volume_24h, volume_7d, volume_30d, - market_cap_usd, liquidity_usd, holders_count, transfers_24h, last_updated - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) - ON CONFLICT (chain_id, token_address) DO UPDATE SET - price_usd = EXCLUDED.price_usd, - price_change_24h = EXCLUDED.price_change_24h, - volume_24h = EXCLUDED.volume_24h, - volume_7d = EXCLUDED.volume_7d, - volume_30d = EXCLUDED.volume_30d, - market_cap_usd = EXCLUDED.market_cap_usd, - liquidity_usd = EXCLUDED.liquidity_usd, - holders_count = EXCLUDED.holders_count, - transfers_24h = EXCLUDED.transfers_24h, - last_updated = EXCLUDED.last_updated`, - [ - data.chainId, - data.tokenAddress.toLowerCase(), - data.priceUsd, - data.priceChange24h, - data.volume24h, - data.volume7d, - data.volume30d, - data.marketCapUsd, - data.liquidityUsd, - data.holdersCount, - data.transfers24h, - data.lastUpdated, - ] - ); + try { + await this.pool.query( + `INSERT INTO token_market_data ( + chain_id, token_address, price_usd, price_change_24h, volume_24h, volume_7d, volume_30d, + market_cap_usd, liquidity_usd, holders_count, transfers_24h, last_updated + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + ON CONFLICT (chain_id, token_address) DO UPDATE SET + price_usd = EXCLUDED.price_usd, + price_change_24h = EXCLUDED.price_change_24h, + volume_24h = EXCLUDED.volume_24h, + volume_7d = EXCLUDED.volume_7d, + volume_30d = EXCLUDED.volume_30d, + market_cap_usd = EXCLUDED.market_cap_usd, + liquidity_usd = EXCLUDED.liquidity_usd, + holders_count = EXCLUDED.holders_count, + transfers_24h = EXCLUDED.transfers_24h, + last_updated = EXCLUDED.last_updated`, + [ + data.chainId, + data.tokenAddress.toLowerCase(), + data.priceUsd, + data.priceChange24h, + data.volume24h, + data.volume7d, + data.volume30d, + data.marketCapUsd, + data.liquidityUsd, + data.holdersCount, + data.transfers24h, + data.lastUpdated, + ] + ); + } catch (error) { + if (this.isMissingRelationError(error)) { + return; + } + throw error; + } } async getTopTokensByVolume(chainId: number, limit: number = 50): Promise { - const result = await this.pool.query( - `SELECT chain_id, token_address, price_usd, price_change_24h, volume_24h, volume_7d, volume_30d, - market_cap_usd, liquidity_usd, holders_count, transfers_24h, last_updated - FROM token_market_data - WHERE chain_id = $1 AND volume_24h > 0 - ORDER BY volume_24h DESC - LIMIT $2`, - [chainId, limit] - ); + try { + const result = await this.pool.query( + `SELECT chain_id, token_address, price_usd, price_change_24h, volume_24h, volume_7d, volume_30d, + market_cap_usd, liquidity_usd, holders_count, transfers_24h, last_updated + FROM token_market_data + WHERE chain_id = $1 AND volume_24h > 0 + ORDER BY volume_24h DESC + LIMIT $2`, + [chainId, limit] + ); - return result.rows.map((row) => ({ - chainId: row.chain_id, - tokenAddress: row.token_address, - priceUsd: row.price_usd ? parseFloat(row.price_usd) : undefined, - priceChange24h: row.price_change_24h ? parseFloat(row.price_change_24h) : undefined, - volume24h: parseFloat(row.volume_24h || '0'), - volume7d: parseFloat(row.volume_7d || '0'), - volume30d: parseFloat(row.volume_30d || '0'), - marketCapUsd: row.market_cap_usd ? parseFloat(row.market_cap_usd) : undefined, - liquidityUsd: parseFloat(row.liquidity_usd || '0'), - holdersCount: row.holders_count || 0, - transfers24h: row.transfers_24h || 0, - lastUpdated: row.last_updated, - })); + return result.rows.map((row) => ({ + chainId: row.chain_id, + tokenAddress: row.token_address, + priceUsd: row.price_usd ? parseFloat(row.price_usd) : undefined, + priceChange24h: row.price_change_24h ? parseFloat(row.price_change_24h) : undefined, + volume24h: parseFloat(row.volume_24h || '0'), + volume7d: parseFloat(row.volume_7d || '0'), + volume30d: parseFloat(row.volume_30d || '0'), + marketCapUsd: row.market_cap_usd ? parseFloat(row.market_cap_usd) : undefined, + liquidityUsd: parseFloat(row.liquidity_usd || '0'), + holdersCount: row.holders_count || 0, + transfers24h: row.transfers_24h || 0, + lastUpdated: row.last_updated, + })); + } catch (error) { + if (this.isMissingRelationError(error)) { + return []; + } + throw error; + } } async getTopTokensByLiquidity(chainId: number, limit: number = 50): Promise { - const result = await this.pool.query( - `SELECT chain_id, token_address, price_usd, price_change_24h, volume_24h, volume_7d, volume_30d, - market_cap_usd, liquidity_usd, holders_count, transfers_24h, last_updated - FROM token_market_data - WHERE chain_id = $1 AND liquidity_usd > 0 - ORDER BY liquidity_usd DESC - LIMIT $2`, - [chainId, limit] - ); + try { + const result = await this.pool.query( + `SELECT chain_id, token_address, price_usd, price_change_24h, volume_24h, volume_7d, volume_30d, + market_cap_usd, liquidity_usd, holders_count, transfers_24h, last_updated + FROM token_market_data + WHERE chain_id = $1 AND liquidity_usd > 0 + ORDER BY liquidity_usd DESC + LIMIT $2`, + [chainId, limit] + ); - return result.rows.map((row) => ({ - chainId: row.chain_id, - tokenAddress: row.token_address, - priceUsd: row.price_usd ? parseFloat(row.price_usd) : undefined, - priceChange24h: row.price_change_24h ? parseFloat(row.price_change_24h) : undefined, - volume24h: parseFloat(row.volume_24h || '0'), - volume7d: parseFloat(row.volume_7d || '0'), - volume30d: parseFloat(row.volume_30d || '0'), - marketCapUsd: row.market_cap_usd ? parseFloat(row.market_cap_usd) : undefined, - liquidityUsd: parseFloat(row.liquidity_usd || '0'), - holdersCount: row.holders_count || 0, - transfers24h: row.transfers_24h || 0, - lastUpdated: row.last_updated, - })); + return result.rows.map((row) => ({ + chainId: row.chain_id, + tokenAddress: row.token_address, + priceUsd: row.price_usd ? parseFloat(row.price_usd) : undefined, + priceChange24h: row.price_change_24h ? parseFloat(row.price_change_24h) : undefined, + volume24h: parseFloat(row.volume_24h || '0'), + volume7d: parseFloat(row.volume_7d || '0'), + volume30d: parseFloat(row.volume_30d || '0'), + marketCapUsd: row.market_cap_usd ? parseFloat(row.market_cap_usd) : undefined, + liquidityUsd: parseFloat(row.liquidity_usd || '0'), + holdersCount: row.holders_count || 0, + transfers24h: row.transfers_24h || 0, + lastUpdated: row.last_updated, + })); + } catch (error) { + if (this.isMissingRelationError(error)) { + return []; + } + throw error; + } } } diff --git a/services/token-aggregation/src/database/repositories/token-repo.ts b/services/token-aggregation/src/database/repositories/token-repo.ts index 58ff512..be4b039 100644 --- a/services/token-aggregation/src/database/repositories/token-repo.ts +++ b/services/token-aggregation/src/database/repositories/token-repo.ts @@ -36,116 +36,154 @@ export class TokenRepository { this.pool = getDatabasePool(); } - async getToken(chainId: number, address: string): Promise { - const result = await this.pool.query( - `SELECT chain_id, address, name, symbol, decimals, total_supply, logo_url, website_url, description, verified - FROM tokens - WHERE chain_id = $1 AND address = $2`, - [chainId, address.toLowerCase()] - ); - - if (result.rows.length === 0) { - return null; + private isMissingRelationError(error: unknown): boolean { + if (!error || typeof error !== 'object') { + return false; } - const row = result.rows[0]; - return { - chainId: row.chain_id, - address: row.address, - name: row.name, - symbol: row.symbol, - decimals: row.decimals, - totalSupply: row.total_supply?.toString(), - logoUrl: row.logo_url, - websiteUrl: row.website_url, - description: row.description, - verified: row.verified, - }; + const code = (error as { code?: string }).code; + const message = (error as { message?: string }).message || ''; + return code === '42P01' || (message.includes('relation "') && message.includes('" does not exist')); + } + + async getToken(chainId: number, address: string): Promise { + try { + const result = await this.pool.query( + `SELECT chain_id, address, name, symbol, decimals, total_supply, logo_url, website_url, description, verified + FROM tokens + WHERE chain_id = $1 AND address = $2`, + [chainId, address.toLowerCase()] + ); + + if (result.rows.length === 0) { + return null; + } + + const row = result.rows[0]; + return { + chainId: row.chain_id, + address: row.address, + name: row.name, + symbol: row.symbol, + decimals: row.decimals, + totalSupply: row.total_supply?.toString(), + logoUrl: row.logo_url, + websiteUrl: row.website_url, + description: row.description, + verified: row.verified, + }; + } catch (error) { + if (this.isMissingRelationError(error)) { + return null; + } + throw error; + } } async getTokens(chainId: number, limit: number = 50, offset: number = 0): Promise { - const result = await this.pool.query( - `SELECT chain_id, address, name, symbol, decimals, total_supply, logo_url, website_url, description, verified - FROM tokens - WHERE chain_id = $1 - ORDER BY address - LIMIT $2 OFFSET $3`, - [chainId, limit, offset] - ); + try { + const result = await this.pool.query( + `SELECT chain_id, address, name, symbol, decimals, total_supply, logo_url, website_url, description, verified + FROM tokens + WHERE chain_id = $1 + ORDER BY address + LIMIT $2 OFFSET $3`, + [chainId, limit, offset] + ); - return result.rows.map((row) => ({ - chainId: row.chain_id, - address: row.address, - name: row.name, - symbol: row.symbol, - decimals: row.decimals, - totalSupply: row.total_supply?.toString(), - logoUrl: row.logo_url, - websiteUrl: row.website_url, - description: row.description, - verified: row.verified, - })); + return result.rows.map((row) => ({ + chainId: row.chain_id, + address: row.address, + name: row.name, + symbol: row.symbol, + decimals: row.decimals, + totalSupply: row.total_supply?.toString(), + logoUrl: row.logo_url, + websiteUrl: row.website_url, + description: row.description, + verified: row.verified, + })); + } catch (error) { + if (this.isMissingRelationError(error)) { + return []; + } + throw error; + } } async upsertToken(token: Token): Promise { - await this.pool.query( - `INSERT INTO tokens (chain_id, address, name, symbol, decimals, total_supply, logo_url, website_url, description, verified) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) - ON CONFLICT (chain_id, address) DO UPDATE SET - name = COALESCE(EXCLUDED.name, tokens.name), - symbol = COALESCE(EXCLUDED.symbol, tokens.symbol), - decimals = COALESCE(EXCLUDED.decimals, tokens.decimals), - total_supply = COALESCE(EXCLUDED.total_supply, tokens.total_supply), - logo_url = COALESCE(EXCLUDED.logo_url, tokens.logo_url), - website_url = COALESCE(EXCLUDED.website_url, tokens.website_url), - description = COALESCE(EXCLUDED.description, tokens.description), - verified = COALESCE(EXCLUDED.verified, tokens.verified), - updated_at = NOW()`, - [ - token.chainId, - token.address.toLowerCase(), - token.name, - token.symbol, - token.decimals, - token.totalSupply, - token.logoUrl, - token.websiteUrl, - token.description, - token.verified, - ] - ); + try { + await this.pool.query( + `INSERT INTO tokens (chain_id, address, name, symbol, decimals, total_supply, logo_url, website_url, description, verified) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + ON CONFLICT (chain_id, address) DO UPDATE SET + name = COALESCE(EXCLUDED.name, tokens.name), + symbol = COALESCE(EXCLUDED.symbol, tokens.symbol), + decimals = COALESCE(EXCLUDED.decimals, tokens.decimals), + total_supply = COALESCE(EXCLUDED.total_supply, tokens.total_supply), + logo_url = COALESCE(EXCLUDED.logo_url, tokens.logo_url), + website_url = COALESCE(EXCLUDED.website_url, tokens.website_url), + description = COALESCE(EXCLUDED.description, tokens.description), + verified = COALESCE(EXCLUDED.verified, tokens.verified), + updated_at = NOW()`, + [ + token.chainId, + token.address.toLowerCase(), + token.name, + token.symbol, + token.decimals, + token.totalSupply, + token.logoUrl, + token.websiteUrl, + token.description, + token.verified, + ] + ); + } catch (error) { + if (this.isMissingRelationError(error)) { + return; + } + throw error; + } } async searchTokens(chainId: number, query: string, limit: number = 20): Promise { - const searchPattern = `%${query.toLowerCase()}%`; - const result = await this.pool.query( - `SELECT chain_id, address, name, symbol, decimals, total_supply, logo_url, website_url, description, verified - FROM tokens - WHERE chain_id = $1 - AND (LOWER(address) LIKE $2 OR LOWER(symbol) LIKE $2 OR LOWER(name) LIKE $2) - ORDER BY - CASE - WHEN LOWER(address) = $3 THEN 1 - WHEN LOWER(symbol) = $3 THEN 2 - WHEN LOWER(name) = $3 THEN 3 - ELSE 4 - END, - symbol - LIMIT $4`, - [chainId, searchPattern, query.toLowerCase(), limit] - ); + try { + const searchPattern = `%${query.toLowerCase()}%`; + const result = await this.pool.query( + `SELECT chain_id, address, name, symbol, decimals, total_supply, logo_url, website_url, description, verified + FROM tokens + WHERE chain_id = $1 + AND (LOWER(address) LIKE $2 OR LOWER(symbol) LIKE $2 OR LOWER(name) LIKE $2) + ORDER BY + CASE + WHEN LOWER(address) = $3 THEN 1 + WHEN LOWER(symbol) = $3 THEN 2 + WHEN LOWER(name) = $3 THEN 3 + ELSE 4 + END, + symbol + LIMIT $4`, + [chainId, searchPattern, query.toLowerCase(), limit] + ); - return result.rows.map((row) => ({ - chainId: row.chain_id, - address: row.address, - name: row.name, - symbol: row.symbol, - decimals: row.decimals, - totalSupply: row.total_supply?.toString(), - logoUrl: row.logo_url, - websiteUrl: row.website_url, - description: row.description, - verified: row.verified, - })); + return result.rows.map((row) => ({ + chainId: row.chain_id, + address: row.address, + name: row.name, + symbol: row.symbol, + decimals: row.decimals, + totalSupply: row.total_supply?.toString(), + logoUrl: row.logo_url, + websiteUrl: row.website_url, + description: row.description, + verified: row.verified, + })); + } catch (error) { + if (this.isMissingRelationError(error)) { + return []; + } + throw error; + } } }