Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,26 @@ import { getEnabledStakingPools } from "../config/staking-pools";
import { getContract } from "../../../providers/chain.provider";
import { GAUGE_ABI, POOL_ABI } from "../../../utils/abi";
import { aerodromeService } from "../../../shared/services/aerodrome.service";
import { createCache, getCached, setCache } from "../../../shared/cache";

type DexScreenerMetrics = {
feeAPR: string | null;
tvlUsd: number | null;
};

/** Fetch fee-based APR and real TVL from DexScreener. */
// Granular caches for APR/TVL and gauge data
const dexMetricsCache = createCache<DexScreenerMetrics>();
const DEX_METRICS_TTL = 30_000; // 30s for APR + TVL

const gaugeDataCache = createCache<{ rewardRate: bigint; totalStaked: bigint }>();
const GAUGE_DATA_TTL = 60_000; // 60s for gauge rewards

/** Fetch fee-based APR and real TVL from DexScreener (cached 30s). */
async function fetchDexScreenerMetrics(poolAddress: string, feeRate: number): Promise<DexScreenerMetrics> {
const cacheKey = `dex:${poolAddress.toLowerCase()}:${feeRate}`;
const cached = getCached(dexMetricsCache, cacheKey);
if (cached) return cached;

try {
const res = await fetch(`https://api.dexscreener.com/latest/dex/pairs/base/${poolAddress}`, {
signal: AbortSignal.timeout(8000),
Expand All @@ -22,12 +34,16 @@ async function fetchDexScreenerMetrics(poolAddress: string, feeRate: number): Pr
const vol24h = pair?.volume?.h24 ?? null;

if (!tvlUsd || tvlUsd <= 0 || !vol24h || vol24h <= 0) {
return { feeAPR: null, tvlUsd };
const result: DexScreenerMetrics = { feeAPR: null, tvlUsd };
setCache(dexMetricsCache, cacheKey, result, DEX_METRICS_TTL);
return result;
}

const feeAPR = (vol24h * feeRate * 365) / tvlUsd * 100;
console.log(`[APR-DEXSCREENER] vol24h=$${vol24h.toFixed(0)}, tvl=$${tvlUsd.toFixed(0)}, feeRate=${feeRate}, feeAPR=${feeAPR.toFixed(2)}%`);
return { feeAPR: `${feeAPR.toFixed(2)}%`, tvlUsd };
const result: DexScreenerMetrics = { feeAPR: `${feeAPR.toFixed(2)}%`, tvlUsd };
setCache(dexMetricsCache, cacheKey, result, DEX_METRICS_TTL);
return result;
} catch (e) {
console.error(`[APR-DEXSCREENER] Failed:`, e instanceof Error ? e.message : e);
return { feeAPR: null, tvlUsd: null };
Expand Down Expand Up @@ -68,7 +84,7 @@ export interface GetProtocolInfoResponse {
}

let cache: { data: GetProtocolInfoResponse; expiresAt: number } | null = null;
const CACHE_TTL = 10 * 60 * 1000; // 10 minutes
const CACHE_TTL = 30_000; // 30s — individual caches (dex 30s, gauge 60s) handle per-resource freshness

export async function executeGetProtocolInfo(): Promise<GetProtocolInfoResponse> {
if (cache && Date.now() < cache.expiresAt) {
Expand All @@ -88,15 +104,25 @@ export async function executeGetProtocolInfo(): Promise<GetProtocolInfoResponse>
);
console.log(`[PROTOCOL-INFO] poolAddress=${poolAddress}, gaugeAddress=${gaugeAddress}`);

const gauge = getContract(gaugeAddress, GAUGE_ABI, "base");
const feeRate = pool.stable ? 0.0001 : 0.003;

// DexScreener is the critical path (APR + TVL). Gauge calls are best-effort — no retries.
const [dexMetrics, rewardRate, totalStaked] = await Promise.all([
fetchDexScreenerMetrics(poolAddress, feeRate),
safeGaugeCall(gauge, "rewardRate"),
safeGaugeCall(gauge, "totalSupply"),
]);
// Gauge data cached 60s; DexScreener cached 30s.
const gaugeCacheKey = `gauge:${gaugeAddress.toLowerCase()}`;
let gaugeData = getCached(gaugeDataCache, gaugeCacheKey);
const dexPromise = fetchDexScreenerMetrics(poolAddress, feeRate);

if (!gaugeData) {
const gauge = getContract(gaugeAddress, GAUGE_ABI, "base");
const [rewardRate, totalStaked] = await Promise.all([
safeGaugeCall(gauge, "rewardRate"),
safeGaugeCall(gauge, "totalSupply"),
]);
gaugeData = { rewardRate, totalStaked };
setCache(gaugeDataCache, gaugeCacheKey, gaugeData, GAUGE_DATA_TTL);
}

const dexMetrics = await dexPromise;
const { rewardRate, totalStaked } = gaugeData;

let estimatedAPR = "0";
let aprSource = "unavailable";
Expand Down
35 changes: 35 additions & 0 deletions backend/src/shared/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Simple in-memory TTL cache backed by a Map.
*
* Usage:
* const myCache = createCache<MyType>();
* setCache(myCache, "key", value, 30_000); // 30s TTL
* const hit = getCached(myCache, "key"); // MyType | null
*/

export interface CacheEntry<T> {
value: T;
expiresAt: number;
}

export type TTLCache<T = unknown> = Map<string, CacheEntry<T>>;

/** Create a new typed cache instance. */
export function createCache<T = unknown>(): TTLCache<T> {
return new Map();
}

/** Retrieve a cached value. Returns `null` when missing or expired. */
export function getCached<T>(cache: TTLCache<T>, key: string): T | null {
const entry = cache.get(key);
if (!entry || Date.now() >= entry.expiresAt) {
cache.delete(key);
return null;
}
return entry.value;
}

/** Store a value in the cache with a TTL in milliseconds. */
export function setCache<T>(cache: TTLCache<T>, key: string, value: T, ttlMs: number): void {
cache.set(key, { value, expiresAt: Date.now() + ttlMs });
}
37 changes: 13 additions & 24 deletions backend/src/shared/services/aerodrome.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ethers } from "ethers";
import { getContract } from "../../providers/chain.provider";
import { getProtocolConfig, getUserAdapterAddress } from "../../config/protocols";
import { AppError } from "../errorCodes";
import { createCache, getCached, setCache, type TTLCache } from "../cache";
import {
AERODROME_ROUTER_ABI,
AERODROME_FACTORY_ABI,
Expand All @@ -23,22 +24,9 @@ interface Route {
}

const BALANCE_CACHE_TTL_MS = 90_000;
const walletBalanceCache = new Map<string, { value: string; expiresAt: number }>();

const poolInfoCache = new Map<string, { value: unknown; expiresAt: number }>();

function getCached(cache: Map<string, { value: unknown; expiresAt: number }>, key: string): unknown | null {
const entry = cache.get(key);
if (!entry || Date.now() >= entry.expiresAt) {
cache.delete(key);
return null;
}
return entry.value;
}

function setCache(cache: Map<string, { value: unknown; expiresAt: number }>, key: string, value: unknown, ttlMs: number): void {
cache.set(key, { value, expiresAt: Date.now() + ttlMs });
}
const walletBalanceCache = createCache<string>();
const poolInfoCache = createCache<string>();
const gaugeRewardCache = createCache<bigint>();

function resolveTokenAddress(address: string): string {
return address === ETH_ADDRESS ? WETH : address;
Expand Down Expand Up @@ -203,8 +191,14 @@ export class AerodromeService {
}

async getRewardRate(gaugeAddress: string): Promise<bigint> {
const cacheKey = `rewardRate:${gaugeAddress.toLowerCase()}`;
const cached = getCached(gaugeRewardCache, cacheKey);
if (cached !== null) return cached;

const gauge = getContract(gaugeAddress, GAUGE_ABI, CHAIN);
return gauge.rewardRate();
const rate: bigint = await gauge.rewardRate();
setCache(gaugeRewardCache, cacheKey, rate, 60_000); // 60s TTL
return rate;
}

// ========== ALLOWANCE ==========
Expand Down Expand Up @@ -248,17 +242,12 @@ export class AerodromeService {

getWalletBalanceCached(userAddress: string, symbol: string): string | null {
const key = `${userAddress.toLowerCase()}:${symbol.toUpperCase()}`;
const cached = walletBalanceCache.get(key);
if (!cached || Date.now() >= cached.expiresAt) {
walletBalanceCache.delete(key);
return null;
}
return cached.value;
return getCached(walletBalanceCache, key);
}

setWalletBalanceCached(userAddress: string, symbol: string, value: string): void {
const key = `${userAddress.toLowerCase()}:${symbol.toUpperCase()}`;
walletBalanceCache.set(key, { value, expiresAt: Date.now() + BALANCE_CACHE_TTL_MS });
setCache(walletBalanceCache, key, value, BALANCE_CACHE_TTL_MS);
}
}

Expand Down
Loading