fix(token-aggregation): tolerate missing market data tables

This commit is contained in:
defiQUG
2026-03-29 18:25:54 -07:00
parent 07d9ce4876
commit 089da29b9b
2 changed files with 278 additions and 202 deletions

View File

@@ -9,122 +9,160 @@ export class MarketDataRepository {
this.pool = getDatabasePool();
}
async getMarketData(chainId: number, tokenAddress: string): Promise<TokenMarketData | null> {
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<TokenMarketData | null> {
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<void> {
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<TokenMarketData[]> {
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<TokenMarketData[]> {
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;
}
}
}

View File

@@ -36,116 +36,154 @@ export class TokenRepository {
this.pool = getDatabasePool();
}
async getToken(chainId: number, address: string): Promise<Token | null> {
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<Token | null> {
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<Token[]> {
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<void> {
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<Token[]> {
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;
}
}
}