We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/nirholas/universal-crypto-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
/**
* Price Feeds Module Tests
* Tests for Chainlink price feeds and TWAP oracle
* @author nich
* @github github.com/nirholas
* @license Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import type { Address } from "viem"
// Mock the clients module
vi.mock("@/evm/services/clients.js", () => ({
getPublicClient: vi.fn(() => mockPublicClient)
}))
// Mock public client
const mockPublicClient = {
getChainId: vi.fn().mockResolvedValue(1),
readContract: vi.fn(),
multicall: vi.fn()
}
describe("Price Feeds Module", () => {
beforeEach(() => {
vi.clearAllMocks()
mockPublicClient.getChainId.mockResolvedValue(1)
mockPublicClient.readContract.mockReset()
mockPublicClient.multicall.mockReset()
})
afterEach(() => {
vi.clearAllMocks()
})
// Chainlink feed addresses
const CHAINLINK_FEEDS: Record<string, Address> = {
"ETH/USD": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419",
"BTC/USD": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c",
"USDC/USD": "0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6",
"LINK/USD": "0x2c1d072e956AFFC0D435Cb7AC38EF18d24d9127c"
}
describe("Chainlink Price Feeds", () => {
it("should fetch ETH/USD price from Chainlink", async () => {
const mockRoundData = [
BigInt("110680464442257318219"), // roundId
BigInt(250000000000), // answer ($2500.00)
BigInt(1704067200), // startedAt
BigInt(1704067260), // updatedAt
BigInt("110680464442257318219") // answeredInRound
] as const
mockPublicClient.readContract
.mockResolvedValueOnce(mockRoundData)
.mockResolvedValueOnce(8) // decimals
.mockResolvedValueOnce("ETH / USD") // description
const roundData = await mockPublicClient.readContract({
address: CHAINLINK_FEEDS["ETH/USD"],
functionName: "latestRoundData"
})
const decimals = await mockPublicClient.readContract({
address: CHAINLINK_FEEDS["ETH/USD"],
functionName: "decimals"
})
const price = Number(roundData[1]) / Math.pow(10, Number(decimals))
expect(price).toBe(2500)
})
it("should fetch BTC/USD price from Chainlink", async () => {
mockPublicClient.readContract.mockReset()
const mockRoundData = [
BigInt("110680464442257318220"),
BigInt(4500000000000), // answer ($45000.00)
BigInt(1704067200),
BigInt(1704067260),
BigInt("110680464442257318220")
] as const
mockPublicClient.readContract
.mockResolvedValueOnce(mockRoundData)
.mockResolvedValueOnce(8)
const roundData = await mockPublicClient.readContract({
address: CHAINLINK_FEEDS["BTC/USD"],
functionName: "latestRoundData"
})
const decimals = await mockPublicClient.readContract({
address: CHAINLINK_FEEDS["BTC/USD"],
functionName: "decimals"
})
const price = Number(roundData[1]) / Math.pow(10, Number(decimals))
expect(price).toBe(45000)
})
it("should detect stale price data", async () => {
mockPublicClient.readContract.mockReset()
const staleTimestamp = BigInt(Math.floor(Date.now() / 1000) - 3600 * 24) // 24 hours old
const mockRoundData = [
BigInt("110680464442257318219"),
BigInt(250000000000),
staleTimestamp,
staleTimestamp,
BigInt("110680464442257318219")
] as const
mockPublicClient.readContract.mockResolvedValueOnce(mockRoundData)
const roundData = await mockPublicClient.readContract({
address: CHAINLINK_FEEDS["ETH/USD"],
functionName: "latestRoundData"
})
const updatedAt = Number(roundData[3])
const now = Math.floor(Date.now() / 1000)
const ageInSeconds = now - updatedAt
expect(ageInSeconds).toBeGreaterThan(3600) // More than 1 hour old
})
it("should verify answeredInRound matches roundId", async () => {
const roundId = BigInt("110680464442257318219")
const mockRoundData = [
roundId,
BigInt(250000000000),
BigInt(1704067200),
BigInt(1704067260),
roundId // answeredInRound should match roundId for valid data
] as const
mockPublicClient.readContract.mockResolvedValueOnce(mockRoundData)
const roundData = await mockPublicClient.readContract({
address: CHAINLINK_FEEDS["ETH/USD"],
functionName: "latestRoundData"
})
expect(roundData[0]).toBe(roundData[4]) // roundId === answeredInRound
})
})
describe("Multi-Network Price Feeds", () => {
it("should have correct feed addresses for Ethereum", () => {
mockPublicClient.getChainId.mockResolvedValue(1)
expect(CHAINLINK_FEEDS["ETH/USD"]).toBe("0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419")
expect(CHAINLINK_FEEDS["BTC/USD"]).toBe("0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c")
})
it("should have feed addresses for BSC", () => {
const BSC_FEEDS: Record<string, Address> = {
"BNB/USD": "0x0567F2323251f0Aab15c8dFb1967E4e8A7D42aeE",
"BTC/USD": "0x264990fbd0A4796A3E3d8E37C4d5F87a3aCa5Ebf"
}
expect(BSC_FEEDS["BNB/USD"]).toBeDefined()
})
it("should have feed addresses for Arbitrum", () => {
const ARB_FEEDS: Record<string, Address> = {
"ETH/USD": "0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612",
"ARB/USD": "0xb2A824043730FE05F3DA2efaFa1CBbe83fa548D6"
}
expect(ARB_FEEDS["ARB/USD"]).toBeDefined()
})
})
describe("Batch Price Fetching", () => {
it("should fetch multiple prices in parallel", async () => {
mockPublicClient.multicall.mockResolvedValue([
{ result: [BigInt(1), BigInt(250000000000), BigInt(1), BigInt(1), BigInt(1)], status: "success" },
{ result: [BigInt(1), BigInt(4500000000000), BigInt(1), BigInt(1), BigInt(1)], status: "success" },
{ result: [BigInt(1), BigInt(100000000), BigInt(1), BigInt(1), BigInt(1)], status: "success" }
])
const results = await mockPublicClient.multicall({
contracts: [
{ address: CHAINLINK_FEEDS["ETH/USD"], functionName: "latestRoundData" },
{ address: CHAINLINK_FEEDS["BTC/USD"], functionName: "latestRoundData" },
{ address: CHAINLINK_FEEDS["USDC/USD"], functionName: "latestRoundData" }
]
})
expect(results).toHaveLength(3)
expect(results[0].result[1]).toBe(BigInt(250000000000))
expect(results[1].result[1]).toBe(BigInt(4500000000000))
expect(results[2].result[1]).toBe(BigInt(100000000))
})
})
describe("TWAP Oracle (Uniswap V3)", () => {
it("should calculate TWAP from tick cumulatives", () => {
// Mock tick cumulative data for TWAP calculation
const tickCumulatives = [BigInt(-12345678), BigInt(-12000000)]
const secondsAgos = [1800, 0] // 30 minutes ago to now
// TWAP calculation: (tickCumulative[0] - tickCumulative[1]) / (secondsAgos[0] - secondsAgos[1])
const tickCumulativeDelta = Number(tickCumulatives[1] - tickCumulatives[0])
const timeDelta = secondsAgos[0] - secondsAgos[1]
const averageTick = Math.floor(tickCumulativeDelta / timeDelta)
expect(averageTick).toBeDefined()
})
it("should fetch slot0 for current price", async () => {
mockPublicClient.readContract.mockReset()
const mockSlot0 = {
sqrtPriceX96: BigInt("1234567890123456789012345678"),
tick: -50000,
observationIndex: 100,
observationCardinality: 200,
observationCardinalityNext: 300,
feeProtocol: 0,
unlocked: true
}
mockPublicClient.readContract.mockResolvedValueOnce([
mockSlot0.sqrtPriceX96,
mockSlot0.tick,
mockSlot0.observationIndex,
mockSlot0.observationCardinality,
mockSlot0.observationCardinalityNext,
mockSlot0.feeProtocol,
mockSlot0.unlocked
])
const slot0 = await mockPublicClient.readContract({
address: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640" as Address, // USDC/ETH pool
functionName: "slot0"
})
expect(slot0[0]).toBe(mockSlot0.sqrtPriceX96)
expect(slot0[1]).toBe(mockSlot0.tick)
})
it("should convert sqrtPriceX96 to price", () => {
const sqrtPriceX96 = BigInt("1234567890123456789012345678901234567")
const Q96 = BigInt(2) ** BigInt(96)
// Price = (sqrtPriceX96 / 2^96)^2
const sqrtPrice = Number(sqrtPriceX96) / Number(Q96)
const price = sqrtPrice * sqrtPrice
expect(price).toBeGreaterThan(0)
})
})
describe("Price Feed Availability", () => {
it("should handle unavailable price feed", async () => {
const chainId = 999 // Unknown chain
const availableFeeds: Record<number, Record<string, Address>> = {
1: { "ETH/USD": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419" }
}
const feedAddress = availableFeeds[chainId]?.["ETH/USD"]
expect(feedAddress).toBeUndefined()
})
it("should handle RPC errors gracefully", async () => {
mockPublicClient.readContract.mockReset()
mockPublicClient.readContract.mockRejectedValueOnce(
new Error("RPC request failed")
)
await expect(
mockPublicClient.readContract({
address: CHAINLINK_FEEDS["ETH/USD"],
functionName: "latestRoundData"
})
).rejects.toThrow("RPC request failed")
})
})
describe("Price Data Validation", () => {
it("should detect zero or negative prices", () => {
const validatePrice = (answer: bigint): boolean => {
return answer > 0n
}
expect(validatePrice(BigInt(250000000000))).toBe(true)
expect(validatePrice(BigInt(0))).toBe(false)
expect(validatePrice(BigInt(-1))).toBe(false)
})
it("should handle different decimal configurations", () => {
const formatPrice = (answer: bigint, decimals: number): number => {
return Number(answer) / Math.pow(10, decimals)
}
expect(formatPrice(BigInt(250000000000), 8)).toBe(2500)
expect(formatPrice(BigInt(2500000000000000000000n), 18)).toBe(2500)
expect(formatPrice(BigInt(25000000), 6)).toBeCloseTo(25)
})
})
describe("Edge Cases", () => {
it("should handle maximum price values", () => {
const maxInt256 = BigInt("57896044618658097711785492504343953926634992332820282019728792003956564819967")
// Should not overflow when formatted
const decimals = 8
const price = Number(BigInt(999999999999999)) / Math.pow(10, decimals)
expect(price).toBeGreaterThan(0)
expect(Number.isFinite(price)).toBe(true)
})
it("should handle very small prices", () => {
const answer = BigInt(1) // 0.00000001 with 8 decimals
const decimals = 8
const price = Number(answer) / Math.pow(10, decimals)
expect(price).toBe(0.00000001)
})
it("should handle timestamp edge cases", () => {
const updatedAt = BigInt(0) // Unix epoch
const updateTime = new Date(Number(updatedAt) * 1000)
expect(updateTime.getTime()).toBe(0)
})
})
})