@iqai/mcp-fraxlend
Version:
Mcp server for Fraxlend access
147 lines (144 loc) • 6 kB
JavaScript
import dedent from "dedent";
import { graphql } from "gql.tada";
import { http, createPublicClient, formatEther, formatUnits } from "viem";
import { fraxtal } from "viem/chains";
import { FRAXLEND_ABI } from "../lib/fraxlend.abi.js";
import { client as gqlClient } from "../lib/graphql.js";
const SECONDS_PER_YEAR = 365 * 24 * 60 * 60;
const DEFAULT_RESERVE_FACTOR_BPS = 800; // used only if on-chain read fails
const LENDING_PAIRS_QUERY = graphql(`
query GetLendingPairs {
pairs(first: 100) {
id
name
symbol
address
asset { symbol decimals address }
collateral { symbol decimals address }
dailyHistory(first: 1, orderBy: timestamp, orderDirection: desc) {
interestPerSecond
utilization
totalAssetAmount
totalBorrowAmount
timestamp
}
}
}
`);
const publicClient = createPublicClient({
chain: fraxtal,
transport: http(),
});
export class LendingStatsService {
async getStats() {
// 1) Pull pairs from subgraph
const data = (await gqlClient.request(LENDING_PAIRS_QUERY));
const pairs = data.pairs ?? [];
// 2) Read reserve factor on-chain for each pair via multicall
const addresses = pairs.map((p) => (p.address || p.id));
const reserveByAddr = await this.fetchReserveFactors(addresses);
// 3) Compute metrics (skip pairs with no history)
return pairs.flatMap((pair) => {
const h = pair.dailyHistory?.[0];
if (!h)
return []; // guard: some pairs may have no history yet
const decimals = Number(pair.asset.decimals);
const totalAssets = Number(formatUnits(BigInt(h.totalAssetAmount), decimals));
const totalBorrows = Number(formatUnits(BigInt(h.totalBorrowAmount), decimals));
const utilization = totalAssets > 0 ? totalBorrows / totalAssets : 0;
const borrowAprPct = this.borrowAprPct(h.interestPerSecond);
const reserveFactor = reserveByAddr[(pair.address || pair.id).toLowerCase()] ??
DEFAULT_RESERVE_FACTOR_BPS / 10_000;
const lendApyPct = this.lendApyPct(h.interestPerSecond, utilization, reserveFactor);
return [
{
address: pair.id,
symbol: pair.symbol,
asset: pair.asset,
collateral: pair.collateral,
borrowAprPct, // Borrow APR (linear) %
lendApyPct, // Lend APY (compounded) %
utilizationPct: utilization * 100, // %
totalSupplyRaw: h.totalAssetAmount,
totalBorrowRaw: h.totalBorrowAmount,
decimals,
reserveFactorPct: reserveFactor * 100, // for debugging/visibility
},
];
});
}
async fetchReserveFactors(addresses) {
const out = {};
if (addresses.length === 0)
return out;
const calls = addresses.map((address) => ({
address,
abi: FRAXLEND_ABI,
functionName: "currentRateInfo",
}));
const results = await publicClient.multicall({
contracts: calls,
allowFailure: true,
});
results.forEach((res, i) => {
const addr = addresses[i].toLowerCase();
if (res.status === "success") {
// currentRateInfo returns 5 outputs:
// [lastBlock, feeToProtocolRate, lastTimestamp, ratePerSec, fullUtilizationRate]
const tup = res.result;
if (Array.isArray(tup) && tup.length >= 2) {
const feeToProtocolRate = tup[1];
out[addr] = Number(feeToProtocolRate) / 100_000; // FEE_PRECISION = 1e5
return;
}
}
// fallback if a call fails or result shape unexpected
out[addr] = DEFAULT_RESERVE_FACTOR_BPS / 10_000;
});
return out;
}
// Borrow APR (linear, no compounding) – what the site labels "Borrow APR"
borrowAprPct(interestPerSecond) {
const r = Number(formatEther(BigInt(interestPerSecond))); // 1e18 scaled
return r * SECONDS_PER_YEAR * 100;
}
// Lend APY (compounded) = Borrow APY × Util × (1 - reserve)
lendApyPct(interestPerSecond, util, reserve) {
const r = Number(formatEther(BigInt(interestPerSecond)));
const borrowApy = (1 + r) ** SECONDS_PER_YEAR - 1;
return borrowApy * util * (1 - reserve) * 100;
}
formatStats(stats) {
if (stats.length === 0) {
return dedent `
📊 FraxLend Statistics
No active lending pools found.
`;
}
const formattedStats = stats
.map((pool) => {
const supply = Number(formatUnits(BigInt(pool.totalSupplyRaw), pool.decimals)).toFixed(3);
const borrow = Number(formatUnits(BigInt(pool.totalBorrowRaw), pool.decimals)).toFixed(3);
const util = pool.utilizationPct.toFixed(2);
return dedent `
🏦 ${pool.symbol}
├ 🔗 Address: ${pool.address}
├ 💸 Borrow APR: ${pool.borrowAprPct.toFixed(3)}%
├ 💰 Lend APY: ${pool.lendApyPct.toFixed(3)}%
├ 📊 Utilization: ${util}%
├ 📦 Total Supply: ${supply} ${pool.asset.symbol}
├ 📦 Total Borrow: ${borrow} ${pool.asset.symbol}
├ 🏷️ Asset: ${pool.asset.symbol} (${pool.asset.address})
└ 🏷️ Collateral: ${pool.collateral.symbol} (${pool.collateral.address})
`;
})
.join("\n\n");
return dedent `
📊 FraxLend Pool Statistics
═══════════════════════════
${formattedStats}
Last Updated: ${new Date().toLocaleString()}
`;
}
}
//# sourceMappingURL=lending-stats.js.map