fix(token-aggregation): tolerate missing market data tables
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user