import axios, { AxiosInstance } from "axios"
import { ethers } from "ethers"
import type { PriceFeedData, FeedMetadata } from "../types/feeds.js"
import { getNetworkConfig } from "../resources/network-configs.js"
export type ConfigType = {
chainlinkApiKey?: string
networks?: Record<string, { rpcUrl: string; chainId: number }>
defaultNetwork: string
}
export class ChainlinkApiClient {
private httpClient: AxiosInstance
private providers: Map<string, ethers.JsonRpcProvider> = new Map()
constructor(private config: ConfigType) {
this.httpClient = axios.create({
timeout: 30000,
headers: {
"User-Agent": "Chainlink-MCP-Server/1.0.0",
...(config.chainlinkApiKey && {
"Authorization": `Bearer ${config.chainlinkApiKey}`
})
}
})
}
/**
* Get price feed data for a trading pair
*/
async getPriceFeed(pair: string, network: string = this.config.defaultNetwork): Promise<PriceFeedData> {
try {
const networkConfig = this.getNetworkConfig(network)
const provider = this.getProvider(network, networkConfig.rpcUrl)
// Get feed address for the pair
const feedAddress = await this.getFeedAddress(pair, network)
if (!feedAddress) {
throw new Error(`No feed found for pair ${pair} on ${network}`)
}
// Create aggregator contract instance
const aggregatorAbi = [
"function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)",
"function decimals() external view returns (uint8)"
]
const contract = new ethers.Contract(feedAddress, aggregatorAbi, provider)
// Get latest round data and decimals
const [roundData, decimals] = await Promise.all([
contract.latestRoundData(),
contract.decimals()
])
return {
pair,
price: ethers.formatUnits(roundData.answer, decimals),
decimals: Number(decimals),
updatedAt: new Date(Number(roundData.updatedAt) * 1000).toISOString(),
roundId: roundData.roundId.toString(),
network
}
} catch (error) {
throw new Error(`Failed to get price feed for ${pair}: ${error instanceof Error ? error.message : String(error)}`)
}
}
/**
* Get feed address for a trading pair
*/
private async getFeedAddress(pair: string, network: string): Promise<string | null> {
// This would typically query a registry or use a predefined mapping
// For now, we'll use a simplified mapping
const feedAddresses = await this.getKnownFeedAddresses(network)
return feedAddresses[pair.toUpperCase()] || null
}
/**
* Get known feed addresses for a network
*/
private async getKnownFeedAddresses(network: string): Promise<Record<string, string>> {
// This would be expanded with actual feed addresses from Chainlink
const mainnetFeeds: Record<string, string> = {
"ETH/USD": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419",
"BTC/USD": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c",
"LINK/USD": "0x2c1d072e956AFFC0D435Cb7AC38EF18d24d9127c",
"USDC/USD": "0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6"
}
const sepoliaFeeds: Record<string, string> = {
"ETH/USD": "0x694AA1769357215DE4FAC081bf1f309aDC325306",
"BTC/USD": "0x1b44F3514812d835EB1BDB0acB33d3fA3351Ee43"
}
switch (network.toLowerCase()) {
case "ethereum":
case "mainnet":
return mainnetFeeds
case "sepolia":
return sepoliaFeeds
default:
return {}
}
}
/**
* Get network configuration
*/
private getNetworkConfig(network: string) {
// Check custom networks first
if (this.config.networks?.[network]) {
return this.config.networks[network]
}
// Fall back to default network configs
return getNetworkConfig(network)
}
/**
* Get or create provider for network
*/
private getProvider(network: string, rpcUrl: string): ethers.JsonRpcProvider {
if (!this.providers.has(network)) {
this.providers.set(network, new ethers.JsonRpcProvider(rpcUrl))
}
return this.providers.get(network)!
}
/**
* Make HTTP request to external APIs
*/
async makeRequest(url: string, params?: any): Promise<any> {
try {
const response = await this.httpClient.get(url, { params })
return response.data
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(`HTTP request failed: ${error.response?.status} ${error.response?.statusText}`)
}
throw error
}
}
/**
* Get available feeds for a network
*/
async getAvailableFeeds(network: string = this.config.defaultNetwork): Promise<FeedMetadata[]> {
const feedAddresses = await this.getKnownFeedAddresses(network)
return Object.entries(feedAddresses).map(([pair, address]) => ({
address,
pair,
decimals: 8, // Most Chainlink feeds use 8 decimals
description: `${pair} Price Feed`,
category: this.getCategoryFromPair(pair),
network,
heartbeat: 3600, // Default heartbeat
threshold: 0.5, // Default threshold
status: "active" as const
}))
}
/**
* Determine category from trading pair
*/
private getCategoryFromPair(pair: string): string {
const [base] = pair.split("/")
const cryptoAssets = ["BTC", "ETH", "LINK", "ADA", "DOT", "USDC", "USDT", "DAI"]
const forexPairs = ["EUR", "GBP", "JPY", "CHF", "AUD", "CAD"]
if (cryptoAssets.includes(base)) return "crypto"
if (forexPairs.includes(base)) return "forex"
return "commodities"
}
}