- 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
333 lines
8.2 KiB
TypeScript
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 [];
|
|
}
|
|
}
|
|
}
|