chore: sync submodule state (parent ref update)

Made-with: Cursor
This commit is contained in:
defiQUG
2026-03-02 12:14:09 -08:00
parent 50ab378da9
commit 5efe36b1e0
1100 changed files with 155024 additions and 8674 deletions

View File

@@ -0,0 +1,53 @@
import { Pool, PoolConfig } from 'pg';
import * as dotenv from 'dotenv';
dotenv.config();
let pool: Pool | null = null;
export interface DatabaseConfig {
connectionString?: string;
host?: string;
port?: number;
database?: string;
user?: string;
password?: string;
min?: number;
max?: number;
}
export function getDatabasePool(): Pool {
if (pool) {
return pool;
}
const config: PoolConfig = {
connectionString: process.env.DATABASE_URL,
min: parseInt(process.env.DATABASE_POOL_MIN || '2', 10),
max: parseInt(process.env.DATABASE_POOL_MAX || '10', 10),
};
// If connectionString is not provided, use individual config
if (!config.connectionString) {
config.host = process.env.DB_HOST || 'localhost';
config.port = parseInt(process.env.DB_PORT || '5432', 10);
config.database = process.env.DB_NAME || 'explorer_db';
config.user = process.env.DB_USER || 'postgres';
config.password = process.env.DB_PASSWORD || '';
}
pool = new Pool(config);
pool.on('error', (err) => {
console.error('Unexpected error on idle database client', err);
});
return pool;
}
export async function closeDatabasePool(): Promise<void> {
if (pool) {
await pool.end();
pool = null;
}
}

View File

@@ -0,0 +1,399 @@
import { Pool } from 'pg';
import { getDatabasePool } from '../client';
import bcrypt from 'bcrypt';
export interface ApiKey {
id?: number;
provider: 'coingecko' | 'coinmarketcap' | 'dexscreener' | 'custom';
keyName: string;
apiKeyEncrypted: string;
isActive: boolean;
rateLimitPerMinute?: number;
rateLimitPerDay?: number;
lastUsedAt?: Date;
expiresAt?: Date;
createdBy?: string;
createdAt?: Date;
updatedAt?: Date;
}
export interface ApiEndpoint {
id?: number;
chainId: number;
endpointType: 'rpc' | 'explorer' | 'indexer' | 'custom';
endpointName: string;
endpointUrl: string;
isPrimary: boolean;
isActive: boolean;
requiresAuth: boolean;
authType?: 'jwt' | 'api_key' | 'basic' | 'none';
authConfig?: any;
rateLimitPerMinute?: number;
timeoutMs: number;
healthCheckEnabled: boolean;
lastHealthCheck?: Date;
healthCheckStatus?: 'healthy' | 'unhealthy' | 'unknown';
createdBy?: string;
createdAt?: Date;
updatedAt?: Date;
}
export interface DexFactoryConfig {
id?: number;
chainId: number;
dexType: 'uniswap_v2' | 'uniswap_v3' | 'dodo' | 'custom';
factoryAddress: string;
routerAddress?: string;
poolManagerAddress?: string;
startBlock: number;
isActive: boolean;
description?: string;
createdBy?: string;
createdAt?: Date;
updatedAt?: Date;
}
export interface AdminUser {
id?: number;
username: string;
email?: string;
passwordHash: string;
role: 'super_admin' | 'admin' | 'operator' | 'viewer';
isActive: boolean;
lastLogin?: Date;
createdAt?: Date;
updatedAt?: Date;
}
export class AdminRepository {
public pool: Pool;
constructor() {
this.pool = getDatabasePool();
}
// API Keys Management
async createApiKey(apiKey: ApiKey): Promise<ApiKey> {
const result = await this.pool.query(
`INSERT INTO api_keys (
provider, key_name, api_key_encrypted, is_active,
rate_limit_per_minute, rate_limit_per_day, expires_at, created_by
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *`,
[
apiKey.provider,
apiKey.keyName,
apiKey.apiKeyEncrypted,
apiKey.isActive,
apiKey.rateLimitPerMinute,
apiKey.rateLimitPerDay,
apiKey.expiresAt,
apiKey.createdBy,
]
);
return this.mapApiKey(result.rows[0]);
}
async getApiKeys(provider?: string): Promise<ApiKey[]> {
let query = `SELECT * FROM api_keys WHERE is_active = true`;
const params: any[] = [];
if (provider) {
query += ` AND provider = $1`;
params.push(provider);
}
query += ` ORDER BY created_at DESC`;
const result = await this.pool.query(query, params);
return result.rows.map((row) => this.mapApiKey(row));
}
async getApiKey(id: number): Promise<ApiKey | null> {
const result = await this.pool.query(`SELECT * FROM api_keys WHERE id = $1`, [id]);
if (result.rows.length === 0) return null;
return this.mapApiKey(result.rows[0]);
}
async updateApiKey(id: number, updates: Partial<ApiKey>): Promise<void> {
const fields: string[] = [];
const values: any[] = [];
let paramCount = 1;
if (updates.isActive !== undefined) {
fields.push(`is_active = $${paramCount++}`);
values.push(updates.isActive);
}
if (updates.rateLimitPerMinute !== undefined) {
fields.push(`rate_limit_per_minute = $${paramCount++}`);
values.push(updates.rateLimitPerMinute);
}
if (updates.expiresAt !== undefined) {
fields.push(`expires_at = $${paramCount++}`);
values.push(updates.expiresAt);
}
if (fields.length === 0) return;
values.push(id);
await this.pool.query(
`UPDATE api_keys SET ${fields.join(', ')}, updated_at = NOW() WHERE id = $${paramCount}`,
values
);
}
async deleteApiKey(id: number): Promise<void> {
await this.pool.query(`UPDATE api_keys SET is_active = false, updated_at = NOW() WHERE id = $1`, [id]);
}
// API Endpoints Management
async createEndpoint(endpoint: ApiEndpoint): Promise<ApiEndpoint> {
const result = await this.pool.query(
`INSERT INTO api_endpoints (
chain_id, endpoint_type, endpoint_name, endpoint_url,
is_primary, is_active, requires_auth, auth_type, auth_config,
rate_limit_per_minute, timeout_ms, health_check_enabled, created_by
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
RETURNING *`,
[
endpoint.chainId,
endpoint.endpointType,
endpoint.endpointName,
endpoint.endpointUrl,
endpoint.isPrimary,
endpoint.isActive,
endpoint.requiresAuth,
endpoint.authType,
endpoint.authConfig ? JSON.stringify(endpoint.authConfig) : null,
endpoint.rateLimitPerMinute,
endpoint.timeoutMs,
endpoint.healthCheckEnabled,
endpoint.createdBy,
]
);
return this.mapEndpoint(result.rows[0]);
}
async getEndpoints(chainId?: number, endpointType?: string): Promise<ApiEndpoint[]> {
let query = `SELECT * FROM api_endpoints WHERE is_active = true`;
const params: any[] = [];
let paramCount = 1;
if (chainId) {
query += ` AND chain_id = $${paramCount++}`;
params.push(chainId);
}
if (endpointType) {
query += ` AND endpoint_type = $${paramCount++}`;
params.push(endpointType);
}
query += ` ORDER BY chain_id, endpoint_type, is_primary DESC`;
const result = await this.pool.query(query, params);
return result.rows.map((row) => this.mapEndpoint(row));
}
async updateEndpoint(id: number, updates: Partial<ApiEndpoint>): Promise<void> {
const fields: string[] = [];
const values: any[] = [];
let paramCount = 1;
if (updates.endpointUrl !== undefined) {
fields.push(`endpoint_url = $${paramCount++}`);
values.push(updates.endpointUrl);
}
if (updates.isActive !== undefined) {
fields.push(`is_active = $${paramCount++}`);
values.push(updates.isActive);
}
if (updates.isPrimary !== undefined) {
fields.push(`is_primary = $${paramCount++}`);
values.push(updates.isPrimary);
}
if (updates.healthCheckStatus !== undefined) {
fields.push(`health_check_status = $${paramCount++}, last_health_check = NOW()`);
values.push(updates.healthCheckStatus);
}
if (fields.length === 0) return;
values.push(id);
await this.pool.query(
`UPDATE api_endpoints SET ${fields.join(', ')}, updated_at = NOW() WHERE id = $${paramCount}`,
values
);
}
// DEX Factory Management
async createDexFactory(config: DexFactoryConfig): Promise<DexFactoryConfig> {
const result = await this.pool.query(
`INSERT INTO dex_factory_config (
chain_id, dex_type, factory_address, router_address,
pool_manager_address, start_block, is_active, description, created_by
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING *`,
[
config.chainId,
config.dexType,
config.factoryAddress.toLowerCase(),
config.routerAddress?.toLowerCase(),
config.poolManagerAddress?.toLowerCase(),
config.startBlock,
config.isActive,
config.description,
config.createdBy,
]
);
return this.mapDexFactory(result.rows[0]);
}
async getDexFactories(chainId?: number): Promise<DexFactoryConfig[]> {
let query = `SELECT * FROM dex_factory_config WHERE is_active = true`;
const params: any[] = [];
if (chainId) {
query += ` AND chain_id = $1`;
params.push(chainId);
}
query += ` ORDER BY chain_id, dex_type`;
const result = await this.pool.query(query, params);
return result.rows.map((row) => this.mapDexFactory(row));
}
// Admin Users
async createAdminUser(user: Omit<AdminUser, 'id' | 'createdAt' | 'updatedAt'>): Promise<AdminUser> {
const result = await this.pool.query(
`INSERT INTO admin_users (username, email, password_hash, role, is_active)
VALUES ($1, $2, $3, $4, $5)
RETURNING *`,
[user.username, user.email, user.passwordHash, user.role, user.isActive]
);
return this.mapAdminUser(result.rows[0]);
}
async getAdminUserByUsername(username: string): Promise<AdminUser | null> {
const result = await this.pool.query(`SELECT * FROM admin_users WHERE username = $1`, [username]);
if (result.rows.length === 0) return null;
return this.mapAdminUser(result.rows[0]);
}
async verifyPassword(user: AdminUser, password: string): Promise<boolean> {
return bcrypt.compare(password, user.passwordHash);
}
async hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 10);
}
// Audit Log
async createAuditLog(
userId: number | null,
action: string,
resourceType: string,
resourceId: number | null,
oldValues: any,
newValues: any,
ipAddress?: string,
userAgent?: string
): Promise<void> {
await this.pool.query(
`INSERT INTO admin_audit_log (
user_id, action, resource_type, resource_id,
old_values, new_values, ip_address, user_agent
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[
userId,
action,
resourceType,
resourceId,
oldValues ? JSON.stringify(oldValues) : null,
newValues ? JSON.stringify(newValues) : null,
ipAddress,
userAgent,
]
);
}
// Mappers
private mapApiKey(row: any): ApiKey {
return {
id: row.id,
provider: row.provider,
keyName: row.key_name,
apiKeyEncrypted: row.api_key_encrypted,
isActive: row.is_active,
rateLimitPerMinute: row.rate_limit_per_minute,
rateLimitPerDay: row.rate_limit_per_day,
lastUsedAt: row.last_used_at,
expiresAt: row.expires_at,
createdBy: row.created_by,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
private mapEndpoint(row: any): ApiEndpoint {
return {
id: row.id,
chainId: row.chain_id,
endpointType: row.endpoint_type,
endpointName: row.endpoint_name,
endpointUrl: row.endpoint_url,
isPrimary: row.is_primary,
isActive: row.is_active,
requiresAuth: row.requires_auth,
authType: row.auth_type,
authConfig: row.auth_config,
rateLimitPerMinute: row.rate_limit_per_minute,
timeoutMs: row.timeout_ms,
healthCheckEnabled: row.health_check_enabled,
lastHealthCheck: row.last_health_check,
healthCheckStatus: row.health_check_status,
createdBy: row.created_by,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
private mapDexFactory(row: any): DexFactoryConfig {
return {
id: row.id,
chainId: row.chain_id,
dexType: row.dex_type,
factoryAddress: row.factory_address,
routerAddress: row.router_address,
poolManagerAddress: row.pool_manager_address,
startBlock: parseInt(row.start_block, 10),
isActive: row.is_active,
description: row.description,
createdBy: row.created_by,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
private mapAdminUser(row: any): AdminUser {
return {
id: row.id,
username: row.username,
email: row.email,
passwordHash: row.password_hash,
role: row.role,
isActive: row.is_active,
lastLogin: row.last_login,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
}

View File

@@ -0,0 +1,130 @@
import { Pool } from 'pg';
import { getDatabasePool } from '../client';
import { TokenMarketData } from './token-repo';
export class MarketDataRepository {
private pool: Pool;
constructor() {
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;
}
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,
};
}
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,
]
);
}
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]
);
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,
}));
}
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]
);
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,
}));
}
}

View File

@@ -0,0 +1,208 @@
import { Pool } from 'pg';
import { getDatabasePool } from '../client';
export type DexType = 'uniswap_v2' | 'uniswap_v3' | 'dodo' | 'custom';
export interface LiquidityPool {
id?: number;
chainId: number;
poolAddress: string;
token0Address: string;
token1Address: string;
dexType: DexType;
factoryAddress?: string;
routerAddress?: string;
reserve0: string;
reserve1: string;
reserve0Usd: number;
reserve1Usd: number;
totalLiquidityUsd: number;
volume24h: number;
feeTier?: number;
createdAtBlock?: number;
createdAtTimestamp?: Date;
lastUpdated: Date;
}
export interface PoolReserveSnapshot {
chainId: number;
poolAddress: string;
reserve0: string;
reserve1: string;
reserve0Usd?: number;
reserve1Usd?: number;
totalLiquidityUsd?: number;
blockNumber: number;
timestamp: Date;
}
export class PoolRepository {
private pool: Pool;
constructor() {
this.pool = getDatabasePool();
}
async getPool(chainId: number, poolAddress: string): Promise<LiquidityPool | null> {
const result = await this.pool.query(
`SELECT id, chain_id, pool_address, token0_address, token1_address, dex_type,
factory_address, router_address, reserve0, reserve1, reserve0_usd, reserve1_usd,
total_liquidity_usd, volume_24h, fee_tier, created_at_block, created_at_timestamp, last_updated
FROM liquidity_pools
WHERE chain_id = $1 AND pool_address = $2`,
[chainId, poolAddress.toLowerCase()]
);
if (result.rows.length === 0) {
return null;
}
return this.mapRowToPool(result.rows[0]);
}
async getPoolsByChain(chainId: number, limit: number = 500): Promise<LiquidityPool[]> {
const result = await this.pool.query(
`SELECT id, chain_id, pool_address, token0_address, token1_address, dex_type,
factory_address, router_address, reserve0, reserve1, reserve0_usd, reserve1_usd,
total_liquidity_usd, volume_24h, fee_tier, created_at_block, created_at_timestamp, last_updated
FROM liquidity_pools
WHERE chain_id = $1
ORDER BY total_liquidity_usd DESC NULLS LAST
LIMIT $2`,
[chainId, limit]
);
return result.rows.map((row) => this.mapRowToPool(row));
}
async getPoolsByToken(chainId: number, tokenAddress: string): Promise<LiquidityPool[]> {
const result = await this.pool.query(
`SELECT id, chain_id, pool_address, token0_address, token1_address, dex_type,
factory_address, router_address, reserve0, reserve1, reserve0_usd, reserve1_usd,
total_liquidity_usd, volume_24h, fee_tier, created_at_block, created_at_timestamp, last_updated
FROM liquidity_pools
WHERE chain_id = $1 AND (token0_address = $2 OR token1_address = $2)
ORDER BY total_liquidity_usd DESC`,
[chainId, tokenAddress.toLowerCase()]
);
return result.rows.map((row) => this.mapRowToPool(row));
}
async upsertPool(pool: LiquidityPool): Promise<void> {
await this.pool.query(
`INSERT INTO liquidity_pools (
chain_id, pool_address, token0_address, token1_address, dex_type,
factory_address, router_address, reserve0, reserve1, reserve0_usd, reserve1_usd,
total_liquidity_usd, volume_24h, fee_tier, created_at_block, created_at_timestamp, last_updated
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
ON CONFLICT (chain_id, pool_address) DO UPDATE SET
token0_address = EXCLUDED.token0_address,
token1_address = EXCLUDED.token1_address,
dex_type = EXCLUDED.dex_type,
factory_address = EXCLUDED.factory_address,
router_address = EXCLUDED.router_address,
reserve0 = EXCLUDED.reserve0,
reserve1 = EXCLUDED.reserve1,
reserve0_usd = EXCLUDED.reserve0_usd,
reserve1_usd = EXCLUDED.reserve1_usd,
total_liquidity_usd = EXCLUDED.total_liquidity_usd,
volume_24h = EXCLUDED.volume_24h,
fee_tier = EXCLUDED.fee_tier,
last_updated = EXCLUDED.last_updated`,
[
pool.chainId,
pool.poolAddress.toLowerCase(),
pool.token0Address.toLowerCase(),
pool.token1Address.toLowerCase(),
pool.dexType,
pool.factoryAddress?.toLowerCase(),
pool.routerAddress?.toLowerCase(),
pool.reserve0,
pool.reserve1,
pool.reserve0Usd,
pool.reserve1Usd,
pool.totalLiquidityUsd,
pool.volume24h,
pool.feeTier,
pool.createdAtBlock,
pool.createdAtTimestamp,
pool.lastUpdated,
]
);
}
async addReserveSnapshot(snapshot: PoolReserveSnapshot): Promise<void> {
await this.pool.query(
`INSERT INTO pool_reserves_history (
chain_id, pool_address, reserve0, reserve1, reserve0_usd, reserve1_usd,
total_liquidity_usd, block_number, timestamp
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
[
snapshot.chainId,
snapshot.poolAddress.toLowerCase(),
snapshot.reserve0,
snapshot.reserve1,
snapshot.reserve0Usd,
snapshot.reserve1Usd,
snapshot.totalLiquidityUsd,
snapshot.blockNumber,
snapshot.timestamp,
]
);
}
async getReserveHistory(
chainId: number,
poolAddress: string,
from: Date,
to: Date,
limit: number = 1000
): Promise<PoolReserveSnapshot[]> {
const result = await this.pool.query(
`SELECT chain_id, pool_address, reserve0, reserve1, reserve0_usd, reserve1_usd,
total_liquidity_usd, block_number, timestamp
FROM pool_reserves_history
WHERE chain_id = $1 AND pool_address = $2 AND timestamp >= $3 AND timestamp <= $4
ORDER BY timestamp DESC
LIMIT $5`,
[chainId, poolAddress.toLowerCase(), from, to, limit]
);
return result.rows.map((row) => ({
chainId: row.chain_id,
poolAddress: row.pool_address,
reserve0: row.reserve0,
reserve1: row.reserve1,
reserve0Usd: row.reserve0_usd ? parseFloat(row.reserve0_usd) : undefined,
reserve1Usd: row.reserve1_usd ? parseFloat(row.reserve1_usd) : undefined,
totalLiquidityUsd: row.total_liquidity_usd ? parseFloat(row.total_liquidity_usd) : undefined,
blockNumber: parseInt(row.block_number, 10),
timestamp: row.timestamp,
}));
}
private mapRowToPool(row: any): LiquidityPool {
return {
id: row.id,
chainId: row.chain_id,
poolAddress: row.pool_address,
token0Address: row.token0_address,
token1Address: row.token1_address,
dexType: row.dex_type,
factoryAddress: row.factory_address,
routerAddress: row.router_address,
reserve0: row.reserve0,
reserve1: row.reserve1,
reserve0Usd: parseFloat(row.reserve0_usd || '0'),
reserve1Usd: parseFloat(row.reserve1_usd || '0'),
totalLiquidityUsd: parseFloat(row.total_liquidity_usd || '0'),
volume24h: parseFloat(row.volume_24h || '0'),
feeTier: row.fee_tier,
createdAtBlock: row.created_at_block,
createdAtTimestamp: row.created_at_timestamp,
lastUpdated: row.last_updated,
};
}
}

View File

@@ -0,0 +1,151 @@
import { Pool } from 'pg';
import { getDatabasePool } from '../client';
export interface Token {
chainId: number;
address: string;
name?: string;
symbol?: string;
decimals?: number;
totalSupply?: string;
logoUrl?: string;
websiteUrl?: string;
description?: string;
verified?: boolean;
}
export interface TokenMarketData {
chainId: number;
tokenAddress: string;
priceUsd?: number;
priceChange24h?: number;
volume24h: number;
volume7d: number;
volume30d: number;
marketCapUsd?: number;
liquidityUsd: number;
holdersCount: number;
transfers24h: number;
lastUpdated: Date;
}
export class TokenRepository {
private pool: Pool;
constructor() {
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;
}
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,
};
}
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]
);
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,
}));
}
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,
]
);
}
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]
);
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,
}));
}
}