Files
smom-dbis-138/services/token-aggregation/src/adapters/cmc-adapter.ts
defiQUG 1511f33857 chore: update DBIS contracts and integrate EIP-712 helper
- Updated DBIS_ConversionRouter and DBIS_SettlementRouter to utilize IDBIS_EIP712Helper for EIP-712 hashing and signature recovery, improving stack depth management.
- Refactored minting logic in DBIS_GRU_MintController to streamline recipient processing.
- Enhanced BUILD_NOTES.md with updated build instructions and test coverage details.
- Added new functions in DBIS_SignerRegistry for duplicate signer checks and active signer validation.
- Introduced a new submodule, DBIS_EIP712Helper, to encapsulate EIP-712 related functionalities.

Made-with: Cursor
2026-03-04 02:00:09 -08:00

333 lines
8.2 KiB
TypeScript

import axios, { AxiosInstance } from 'axios';
import { ExternalApiAdapter, TokenMetadata, MarketData } from './base-adapter';
import { logger } from '../utils/logger';
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: MarketData; expiresAt: Date }> = new Map();
constructor() {
this.apiKey = process.env.COINMARKETCAP_API_KEY;
if (!this.apiKey) {
logger.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: unknown) {
const err = error as { response?: { status?: number } };
if (err.response?.status === 400 || err.response?.status === 404) {
return false; // Chain not supported
}
logger.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)
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- interface requires (chainId, address)
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: unknown) {
const err = error as { response?: { status?: number } };
if (err.response?.status === 404 || err.response?.status === 400) {
return null;
}
logger.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) {
logger.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) {
logger.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) {
logger.error(`Error fetching CMC OHLCV for ${pairAddress} on chain ${chainId}:`, error);
return [];
}
}
}