chore: sync submodule state (parent ref update)
Made-with: Cursor
This commit is contained in:
53
services/token-aggregation/src/database/client.ts
Normal file
53
services/token-aggregation/src/database/client.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user