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,54 @@
/**
* Base adapter interface for external API integrations
*/
export interface ExternalApiAdapter {
/**
* Check if the chain is supported by this API provider
*/
checkChainSupport(chainId: number): Promise<boolean>;
/**
* Get token metadata by contract address
*/
getTokenByContract(chainId: number, address: string): Promise<TokenMetadata | null>;
/**
* Get market data for a token
*/
getMarketData(chainId: number, address: string): Promise<MarketData | null>;
/**
* Get provider name
*/
getProviderName(): string;
}
export interface TokenMetadata {
id?: string; // Provider-specific ID (e.g., CoinGecko coin ID)
name?: string;
symbol?: string;
description?: string;
logoUrl?: string;
websiteUrl?: string;
socialLinks?: {
twitter?: string;
telegram?: string;
discord?: string;
github?: string;
};
}
export interface MarketData {
priceUsd?: number;
priceChange24h?: number;
volume24h?: number;
marketCapUsd?: number;
liquidityUsd?: number;
lastUpdated?: Date;
}
export interface ApiCacheEntry {
key: string;
data: any;
expiresAt: Date;
}

View File

@@ -0,0 +1,328 @@
import axios, { AxiosInstance } from 'axios';
import { ExternalApiAdapter, TokenMetadata, MarketData } from './base-adapter';
interface CMCDexPair {
pair_address: string;
base: {
address: string;
symbol: string;
};
quote: {
address: string;
symbol: string;
};
dex_id: string;
price: string;
price_usd?: string;
volume_24h?: {
base: string;
quote: string;
usd?: string;
};
liquidity?: {
usd?: string;
};
}
interface CMCDexPairsResponse {
data: CMCDexPair[];
}
interface CMCPairQuote {
pair_address: string;
price: string;
price_usd?: string;
volume_24h?: {
usd?: string;
};
liquidity?: {
usd?: string;
};
}
interface CMCPairQuotesResponse {
data: Record<string, CMCPairQuote>;
}
interface CMCOHLCV {
time_open: string;
time_close: string;
quote: {
open: string;
high: string;
low: string;
close: string;
volume: string;
};
}
interface CMCOHLCVResponse {
data: {
pairs: Array<{
pair_address: string;
timeframes: Record<string, CMCOHLCV[]>;
}>;
};
}
// Chain ID to CMC chain identifier mapping
// Note: CMC uses different identifiers, these may need to be updated
const CHAIN_TO_CMC_ID: Record<number, string> = {
1: '1', // Ethereum
56: '1839', // BSC
137: '3890', // Polygon
43114: '5805', // Avalanche
42161: '42161', // Arbitrum
10: '42170', // Optimism
8453: '8453', // Base
// 138 and 651940 likely not supported
};
export class CoinMarketCapAdapter implements ExternalApiAdapter {
private api: AxiosInstance;
private apiKey?: string;
private cache: Map<string, { data: any; expiresAt: Date }> = new Map();
constructor() {
this.apiKey = process.env.COINMARKETCAP_API_KEY;
if (!this.apiKey) {
console.warn('CoinMarketCap API key not provided. CMC adapter will not function.');
}
this.api = axios.create({
baseURL: 'https://pro-api.coinmarketcap.com',
timeout: 10000,
headers: {
'X-CMC_PRO_API_KEY': this.apiKey || '',
'Accept': 'application/json',
},
});
}
getProviderName(): string {
return 'coinmarketcap';
}
/**
* Check if chain is supported by CoinMarketCap DEX API
*/
async checkChainSupport(chainId: number): Promise<boolean> {
// CMC DEX API support is limited and requires API key
if (!this.apiKey) {
return false;
}
const cmcChainId = CHAIN_TO_CMC_ID[chainId];
if (!cmcChainId) {
return false;
}
// Try to fetch DEX pairs to verify support
try {
const response = await this.api.get('/v4/dex/spot-pairs/latest', {
params: {
chain_id: cmcChainId,
limit: 1,
},
});
return response.status === 200;
} catch (error: any) {
if (error.response?.status === 400 || error.response?.status === 404) {
return false; // Chain not supported
}
console.error(`Error checking CMC chain support for ${chainId}:`, error);
return false;
}
}
/**
* Get token by contract address (CMC doesn't have direct contract lookup in free tier)
*/
async getTokenByContract(chainId: number, address: string): Promise<TokenMetadata | null> {
// CMC DEX API doesn't provide token metadata directly
// Would need CMC Pro API with different endpoints
return null;
}
/**
* Get market data via DEX pairs
*/
async getMarketData(chainId: number, address: string): Promise<MarketData | null> {
if (!this.apiKey) {
return null;
}
const cmcChainId = CHAIN_TO_CMC_ID[chainId];
if (!cmcChainId) {
return null;
}
const cacheKey = `cmc_market_${chainId}_${address.toLowerCase()}`;
const cached = this.cache.get(cacheKey);
if (cached && cached.expiresAt > new Date()) {
return cached.data;
}
try {
// Get DEX pairs for this token
const response = await this.api.get<CMCDexPairsResponse>('/v4/dex/spot-pairs/latest', {
params: {
chain_id: cmcChainId,
base_address: address.toLowerCase(),
limit: 10,
},
});
if (!response.data.data || response.data.data.length === 0) {
return null;
}
// Aggregate data from all pairs
let totalVolume24h = 0;
let totalLiquidity = 0;
let avgPrice = 0;
let priceCount = 0;
response.data.data.forEach((pair) => {
if (pair.price_usd) {
avgPrice += parseFloat(pair.price_usd);
priceCount++;
}
if (pair.volume_24h?.usd) {
totalVolume24h += parseFloat(pair.volume_24h.usd);
}
if (pair.liquidity?.usd) {
totalLiquidity += parseFloat(pair.liquidity.usd);
}
});
const marketData: MarketData = {
priceUsd: priceCount > 0 ? avgPrice / priceCount : undefined,
volume24h: totalVolume24h > 0 ? totalVolume24h : undefined,
liquidityUsd: totalLiquidity > 0 ? totalLiquidity : undefined,
lastUpdated: new Date(),
};
// Cache for 5 minutes
this.cache.set(cacheKey, {
data: marketData,
expiresAt: new Date(Date.now() + 5 * 60 * 1000),
});
return marketData;
} catch (error: any) {
if (error.response?.status === 404 || error.response?.status === 400) {
return null;
}
console.error(`Error fetching CMC market data for ${address} on chain ${chainId}:`, error);
return null;
}
}
/**
* Get DEX pairs for a token
*/
async getDexPairs(chainId: number, tokenAddress: string): Promise<CMCDexPair[]> {
if (!this.apiKey) {
return [];
}
const cmcChainId = CHAIN_TO_CMC_ID[chainId];
if (!cmcChainId) {
return [];
}
try {
const response = await this.api.get<CMCDexPairsResponse>('/v4/dex/spot-pairs/latest', {
params: {
chain_id: cmcChainId,
base_address: tokenAddress.toLowerCase(),
limit: 100,
},
});
return response.data.data || [];
} catch (error) {
console.error(`Error fetching CMC DEX pairs for ${tokenAddress} on chain ${chainId}:`, error);
return [];
}
}
/**
* Get pair quotes
*/
async getPairQuotes(chainId: number, pairAddresses: string[]): Promise<CMCPairQuote[]> {
if (!this.apiKey || pairAddresses.length === 0) {
return [];
}
const cmcChainId = CHAIN_TO_CMC_ID[chainId];
if (!cmcChainId) {
return [];
}
try {
const response = await this.api.get<CMCPairQuotesResponse>('/v4/dex/pairs/quotes/latest', {
params: {
chain_id: cmcChainId,
pair_addresses: pairAddresses.join(','),
},
});
return Object.values(response.data.data || {});
} catch (error) {
console.error(`Error fetching CMC pair quotes for chain ${chainId}:`, error);
return [];
}
}
/**
* Get OHLCV data for pairs
*/
async getOHLCV(
chainId: number,
pairAddress: string,
interval: '5m' | '15m' | '1h' | '4h' | '24h',
from: Date,
to: Date
): Promise<CMCOHLCV[]> {
if (!this.apiKey) {
return [];
}
const cmcChainId = CHAIN_TO_CMC_ID[chainId];
if (!cmcChainId) {
return [];
}
const intervalMap: Record<string, string> = {
'5m': '5m',
'15m': '15m',
'1h': '1h',
'4h': '4h',
'24h': '1d',
};
try {
const response = await this.api.get<CMCOHLCVResponse>('/v4/dex/pairs/ohlcv/historical', {
params: {
chain_id: cmcChainId,
pair_address: pairAddress.toLowerCase(),
interval: intervalMap[interval] || '1h',
time_start: Math.floor(from.getTime() / 1000),
time_end: Math.floor(to.getTime() / 1000),
},
});
const pair = response.data.data.pairs?.[0];
if (!pair) {
return [];
}
return pair.timeframes[intervalMap[interval] || '1h'] || [];
} catch (error) {
console.error(`Error fetching CMC OHLCV for ${pairAddress} on chain ${chainId}:`, error);
return [];
}
}
}

View File

@@ -0,0 +1,323 @@
import axios, { AxiosInstance } from 'axios';
import { ExternalApiAdapter, TokenMetadata, MarketData } from './base-adapter';
import { getDatabasePool } from '../database/client';
interface CoinGeckoPlatform {
id: string;
chain_identifier: number;
name: string;
shortname: string;
}
interface CoinGeckoCoin {
id: string;
symbol: string;
name: string;
description?: {
en?: string;
};
image?: {
large?: string;
small?: string;
thumb?: string;
};
links?: {
homepage?: string[];
twitter_screen_name?: string;
telegram_channel_identifier?: string;
subreddit_url?: string;
repos_url?: {
github?: string[];
};
};
market_data?: {
current_price?: {
usd?: number;
};
price_change_percentage_24h?: number;
total_volume?: {
usd?: number;
};
market_cap?: {
usd?: number;
};
};
}
interface CoinGeckoMarket {
id: string;
symbol: string;
name: string;
image?: string;
current_price?: number;
price_change_percentage_24h?: number;
total_volume?: number;
market_cap?: number;
}
interface CoinGeckoTrending {
coins: Array<{
item: {
id: string;
name: string;
symbol: string;
thumb?: string;
score?: number;
};
}>;
}
// Chain ID to CoinGecko platform ID mapping
const CHAIN_TO_PLATFORM: Record<number, string> = {
1: 'ethereum',
56: 'binance-smart-chain',
137: 'polygon-pos',
43114: 'avalanche',
42161: 'arbitrum-one',
10: 'optimistic-ethereum',
8453: 'base',
// Note: 138 and 651940 are likely not supported, will return null gracefully
};
export class CoinGeckoAdapter implements ExternalApiAdapter {
private api: AxiosInstance;
private apiKey?: string;
private supportedPlatforms: Map<number, string> = new Map();
private cache: Map<string, { data: any; expiresAt: Date }> = new Map();
constructor() {
this.apiKey = process.env.COINGECKO_API_KEY;
const baseURL = this.apiKey
? 'https://pro-api.coingecko.com/api/v3'
: 'https://api.coingecko.com/api/v3';
this.api = axios.create({
baseURL,
timeout: 10000,
headers: this.apiKey
? {
'x-cg-pro-api-key': this.apiKey,
}
: {},
});
}
getProviderName(): string {
return 'coingecko';
}
/**
* Check if chain is supported by CoinGecko
*/
async checkChainSupport(chainId: number): Promise<boolean> {
// Check cache first
const cacheKey = `chain_support_${chainId}`;
const cached = this.cache.get(cacheKey);
if (cached && cached.expiresAt > new Date()) {
return cached.data;
}
try {
// Load supported platforms if not already loaded
if (this.supportedPlatforms.size === 0) {
await this.loadSupportedPlatforms();
}
const supported = this.supportedPlatforms.has(chainId);
this.cache.set(cacheKey, {
data: supported,
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hour cache
});
return supported;
} catch (error) {
console.error(`Error checking CoinGecko chain support for ${chainId}:`, error);
return false;
}
}
/**
* Load supported platforms from CoinGecko
*/
private async loadSupportedPlatforms(): Promise<void> {
try {
const response = await this.api.get<CoinGeckoPlatform[]>('/asset_platforms');
response.data.forEach((platform) => {
if (platform.chain_identifier) {
this.supportedPlatforms.set(platform.chain_identifier, platform.id);
}
});
} catch (error) {
console.error('Error loading CoinGecko platforms:', error);
// Fallback to known mappings
Object.entries(CHAIN_TO_PLATFORM).forEach(([chainId, platformId]) => {
this.supportedPlatforms.set(parseInt(chainId, 10), platformId);
});
}
}
/**
* Get token by contract address
*/
async getTokenByContract(chainId: number, address: string): Promise<TokenMetadata | null> {
const platformId = this.supportedPlatforms.get(chainId);
if (!platformId) {
return null; // Chain not supported
}
const cacheKey = `token_${chainId}_${address.toLowerCase()}`;
const cached = this.cache.get(cacheKey);
if (cached && cached.expiresAt > new Date()) {
return cached.data;
}
try {
const response = await this.api.get<CoinGeckoCoin>(
`/coins/${platformId}/contract/${address.toLowerCase()}`
);
const metadata: TokenMetadata = {
id: response.data.id,
name: response.data.name,
symbol: response.data.symbol,
description: response.data.description?.en,
logoUrl: response.data.image?.large || response.data.image?.small,
websiteUrl: response.data.links?.homepage?.[0],
socialLinks: {
twitter: response.data.links?.twitter_screen_name
? `https://twitter.com/${response.data.links.twitter_screen_name}`
: undefined,
telegram: response.data.links?.telegram_channel_identifier
? `https://t.me/${response.data.links.telegram_channel_identifier}`
: undefined,
github: response.data.links?.repos_url?.github?.[0],
},
};
// Cache for 1 hour
this.cache.set(cacheKey, {
data: metadata,
expiresAt: new Date(Date.now() + 60 * 60 * 1000),
});
return metadata;
} catch (error: any) {
if (error.response?.status === 404) {
return null; // Token not found
}
console.error(`Error fetching CoinGecko token ${address} on chain ${chainId}:`, error);
return null;
}
}
/**
* Get market data for a token
*/
async getMarketData(chainId: number, address: string): Promise<MarketData | null> {
const platformId = this.supportedPlatforms.get(chainId);
if (!platformId) {
return null;
}
const cacheKey = `market_${chainId}_${address.toLowerCase()}`;
const cached = this.cache.get(cacheKey);
if (cached && cached.expiresAt > new Date()) {
return cached.data;
}
try {
const response = await this.api.get<CoinGeckoCoin>(
`/coins/${platformId}/contract/${address.toLowerCase()}`
);
const marketData: MarketData = {
priceUsd: response.data.market_data?.current_price?.usd,
priceChange24h: response.data.market_data?.price_change_percentage_24h,
volume24h: response.data.market_data?.total_volume?.usd,
marketCapUsd: response.data.market_data?.market_cap?.usd,
lastUpdated: new Date(),
};
// Cache for 5 minutes
this.cache.set(cacheKey, {
data: marketData,
expiresAt: new Date(Date.now() + 5 * 60 * 1000),
});
return marketData;
} catch (error: any) {
if (error.response?.status === 404) {
return null;
}
console.error(`Error fetching CoinGecko market data for ${address} on chain ${chainId}:`, error);
return null;
}
}
/**
* Get trending tokens
*/
async getTrending(): Promise<Array<{ id: string; name: string; symbol: string; score: number }>> {
const cacheKey = 'trending';
const cached = this.cache.get(cacheKey);
if (cached && cached.expiresAt > new Date()) {
return cached.data;
}
try {
const response = await this.api.get<CoinGeckoTrending>('/search/trending');
const trending = response.data.coins.map((coin) => ({
id: coin.item.id,
name: coin.item.name,
symbol: coin.item.symbol,
score: coin.item.score || 0,
}));
// Cache for 10 minutes
this.cache.set(cacheKey, {
data: trending,
expiresAt: new Date(Date.now() + 10 * 60 * 1000),
});
return trending;
} catch (error) {
console.error('Error fetching CoinGecko trending:', error);
return [];
}
}
/**
* Get market data for multiple tokens
*/
async getMarkets(coinIds: string[]): Promise<CoinGeckoMarket[]> {
if (coinIds.length === 0) return [];
const cacheKey = `markets_${coinIds.join(',')}`;
const cached = this.cache.get(cacheKey);
if (cached && cached.expiresAt > new Date()) {
return cached.data;
}
try {
const response = await this.api.get<CoinGeckoMarket[]>('/coins/markets', {
params: {
vs_currency: 'usd',
ids: coinIds.join(','),
order: 'market_cap_desc',
per_page: coinIds.length,
page: 1,
},
});
// Cache for 5 minutes
this.cache.set(cacheKey, {
data: response.data,
expiresAt: new Date(Date.now() + 5 * 60 * 1000),
});
return response.data;
} catch (error) {
console.error('Error fetching CoinGecko markets:', error);
return [];
}
}
}

View File

@@ -0,0 +1,329 @@
import axios, { AxiosInstance } from 'axios';
import { ExternalApiAdapter, TokenMetadata, MarketData } from './base-adapter';
interface DexScreenerPair {
chainId: string;
dexId: string;
url: string;
pairAddress: string;
baseToken: {
address: string;
name: string;
symbol: string;
};
quoteToken: {
address: string;
name: string;
symbol: string;
};
priceNative?: string;
priceUsd?: string;
txns?: {
m5?: {
buys?: number;
sells?: number;
};
h1?: {
buys?: number;
sells?: number;
};
h6?: {
buys?: number;
sells?: number;
};
h24?: {
buys?: number;
sells?: number;
};
};
volume?: {
h24?: number;
h6?: number;
h1?: number;
m5?: number;
};
priceChange?: {
m5?: number;
h1?: number;
h6?: number;
h24?: number;
};
liquidity?: {
usd?: number;
base?: number;
quote?: number;
};
fdv?: number;
pairCreatedAt?: number;
}
interface DexScreenerResponse {
schemaVersion: string;
pairs: DexScreenerPair[] | null;
pair?: DexScreenerPair;
}
// Chain ID to DexScreener chain identifier mapping
// DexScreener uses chain identifiers like 'ethereum', 'bsc', etc.
const CHAIN_TO_DEXSCREENER_ID: Record<number, string> = {
1: 'ethereum',
56: 'bsc',
137: 'polygon',
43114: 'avalanche',
42161: 'arbitrum',
10: 'optimism',
8453: 'base',
// Note: 138 and 651940 are likely not supported
};
// Reverse mapping for lookup
const DEXSCREENER_ID_TO_CHAIN: Record<string, number> = {};
Object.entries(CHAIN_TO_DEXSCREENER_ID).forEach(([chainId, dexId]) => {
DEXSCREENER_ID_TO_CHAIN[dexId] = parseInt(chainId, 10);
});
export class DexScreenerAdapter implements ExternalApiAdapter {
private api: AxiosInstance;
private apiKey?: string;
private cache: Map<string, { data: any; expiresAt: Date }> = new Map();
private supportedChains: Set<number> = new Set();
constructor() {
this.apiKey = process.env.DEXSCREENER_API_KEY;
this.api = axios.create({
baseURL: 'https://api.dexscreener.com',
timeout: 10000,
headers: this.apiKey
? {
Authorization: `Bearer ${this.apiKey}`,
}
: {},
});
}
getProviderName(): string {
return 'dexscreener';
}
/**
* Check if chain is supported by DexScreener
*/
async checkChainSupport(chainId: number): Promise<boolean> {
const dexId = CHAIN_TO_DEXSCREENER_ID[chainId];
if (!dexId) {
return false;
}
// Check cache
const cacheKey = `chain_support_${chainId}`;
const cached = this.cache.get(cacheKey);
if (cached && cached.expiresAt > new Date()) {
return cached.data;
}
// Try a test request to verify support
try {
// Use a known token address for testing (e.g., WETH on Ethereum)
const testAddress = chainId === 1 ? '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' : '';
if (!testAddress) {
// For unknown chains, assume not supported unless proven otherwise
return false;
}
const response = await this.api.get<DexScreenerResponse>(
`/token-pairs/v1/${dexId}/${testAddress}`
);
const supported = response.status === 200 && (response.data.pairs?.length ?? 0) > 0;
this.cache.set(cacheKey, {
data: supported,
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hour cache
});
if (supported) {
this.supportedChains.add(chainId);
}
return supported;
} catch (error: any) {
if (error.response?.status === 404 || error.response?.status === 400) {
return false;
}
console.error(`Error checking DexScreener chain support for ${chainId}:`, error);
return false;
}
}
/**
* Get token by contract address (DexScreener doesn't provide token metadata)
*/
async getTokenByContract(chainId: number, address: string): Promise<TokenMetadata | null> {
// DexScreener doesn't provide token metadata, only pair data
return null;
}
/**
* Get market data via token pairs
*/
async getMarketData(chainId: number, address: string): Promise<MarketData | null> {
const dexId = CHAIN_TO_DEXSCREENER_ID[chainId];
if (!dexId) {
return null; // Chain not supported
}
const cacheKey = `market_${chainId}_${address.toLowerCase()}`;
const cached = this.cache.get(cacheKey);
if (cached && cached.expiresAt > new Date()) {
return cached.data;
}
try {
const response = await this.api.get<DexScreenerResponse>(
`/token-pairs/v1/${dexId}/${address.toLowerCase()}`
);
if (!response.data.pairs || response.data.pairs.length === 0) {
return null;
}
// Aggregate data from all pairs
let totalVolume24h = 0;
let totalLiquidity = 0;
let avgPrice = 0;
let priceCount = 0;
let totalTxns24h = 0;
response.data.pairs.forEach((pair) => {
if (pair.priceUsd) {
avgPrice += parseFloat(pair.priceUsd);
priceCount++;
}
if (pair.volume?.h24) {
totalVolume24h += pair.volume.h24;
}
if (pair.liquidity?.usd) {
totalLiquidity += pair.liquidity.usd;
}
if (pair.txns?.h24) {
totalTxns24h += (pair.txns.h24.buys || 0) + (pair.txns.h24.sells || 0);
}
});
const marketData: MarketData = {
priceUsd: priceCount > 0 ? avgPrice / priceCount : undefined,
volume24h: totalVolume24h > 0 ? totalVolume24h : undefined,
liquidityUsd: totalLiquidity > 0 ? totalLiquidity : undefined,
lastUpdated: new Date(),
};
// Cache for 2 minutes (DexScreener updates frequently)
this.cache.set(cacheKey, {
data: marketData,
expiresAt: new Date(Date.now() + 2 * 60 * 1000),
});
return marketData;
} catch (error: any) {
if (error.response?.status === 404) {
return null; // Token not found
}
console.error(`Error fetching DexScreener market data for ${address} on chain ${chainId}:`, error);
return null;
}
}
/**
* Get all pairs for a token
*/
async getTokenPairs(chainId: number, tokenAddress: string): Promise<DexScreenerPair[]> {
const dexId = CHAIN_TO_DEXSCREENER_ID[chainId];
if (!dexId) {
return [];
}
try {
const response = await this.api.get<DexScreenerResponse>(
`/token-pairs/v1/${dexId}/${tokenAddress.toLowerCase()}`
);
return response.data.pairs || [];
} catch (error) {
console.error(`Error fetching DexScreener pairs for ${tokenAddress} on chain ${chainId}:`, error);
return [];
}
}
/**
* Get pair data by address
*/
async getPairData(chainId: number, pairAddress: string): Promise<DexScreenerPair | null> {
const dexId = CHAIN_TO_DEXSCREENER_ID[chainId];
if (!dexId) {
return null;
}
try {
const response = await this.api.get<DexScreenerResponse>(
`/latest/dex/pairs/${dexId}/${pairAddress.toLowerCase()}`
);
return response.data.pair || null;
} catch (error: any) {
if (error.response?.status === 404) {
return null;
}
console.error(`Error fetching DexScreener pair data for ${pairAddress} on chain ${chainId}:`, error);
return null;
}
}
/**
* Get pairs for multiple tokens (up to 30)
*/
async getMultipleTokenPairs(
chainId: number,
tokenAddresses: string[]
): Promise<Record<string, DexScreenerPair[]>> {
const dexId = CHAIN_TO_DEXSCREENER_ID[chainId];
if (!dexId || tokenAddresses.length === 0) {
return {};
}
// DexScreener limits to 30 addresses per request
const chunks = [];
for (let i = 0; i < tokenAddresses.length; i += 30) {
chunks.push(tokenAddresses.slice(i, i + 30));
}
const results: Record<string, DexScreenerPair[]> = {};
for (const chunk of chunks) {
try {
const response = await this.api.get<DexScreenerResponse>(
`/tokens/v1/${dexId}/${chunk.map((addr) => addr.toLowerCase()).join(',')}`
);
if (response.data.pairs) {
// Group pairs by token address
response.data.pairs.forEach((pair) => {
const baseAddr = pair.baseToken.address.toLowerCase();
const quoteAddr = pair.quoteToken.address.toLowerCase();
if (chunk.includes(baseAddr)) {
if (!results[baseAddr]) results[baseAddr] = [];
results[baseAddr].push(pair);
}
if (chunk.includes(quoteAddr)) {
if (!results[quoteAddr]) results[quoteAddr] = [];
results[quoteAddr].push(pair);
}
});
}
} catch (error) {
console.error(`Error fetching DexScreener pairs for chunk on chain ${chainId}:`, error);
}
}
return results;
}
}