/** * Yield Sandbox — pure client-side compound interest simulator. * No server dependencies. Uses live DeFi Llama rates when available, * falls back to hardcoded mock rates. */ export type SandboxProtocol = "aave-v3" | "morpho-blue"; export type SandboxChain = "1" | "8453"; export type SandboxAsset = "USDC" | "USDT" | "DAI"; export interface YieldPoint { days: number; balance: number; earnings: number; dailyRate: number; } export interface SandboxResult { principal: number; apy: number; isLive: boolean; protocol: string; chainId: string; asset: string; milestones: YieldPoint[]; } export interface ProtocolComparison { protocol: SandboxProtocol; chainId: SandboxChain; label: string; apy: number; isLive: boolean; earnings: number; balance: number; } interface LiveRate { protocol: string; chainId: string; asset: string; apy: number; } // ── Realistic fallback APYs (updated periodically) ── const MOCK_FALLBACK_RATES: Record = { "aave-v3:1:USDC": 3.82, "aave-v3:1:USDT": 3.61, "aave-v3:1:DAI": 3.95, "aave-v3:8453:USDC": 4.12, "aave-v3:8453:DAI": 3.74, "morpho-blue:1:USDC": 5.23, "morpho-blue:1:USDT": 4.87, "morpho-blue:8453:USDC": 5.52, }; const MILESTONE_DAYS = [1, 7, 30, 90, 180, 365, 730, 1825]; const PROTOCOL_LABELS: Record = { "aave-v3:1": "Aave V3 Ethereum", "aave-v3:8453": "Aave V3 Base", "morpho-blue:1": "Morpho Ethereum", "morpho-blue:8453": "Morpho Base", }; // ── Core compound interest ── export function computeYieldAtDays( principal: number, apy: number, days: number, compoundFreq = 365, ): YieldPoint { const r = apy / 100; const t = days / 365; const balance = principal * Math.pow(1 + r / compoundFreq, compoundFreq * t); const earnings = balance - principal; const dailyRate = days > 0 ? earnings / days : 0; return { days, balance, earnings, dailyRate }; } // ── Full milestone simulation ── export function simulateYield(config: { principal: number; apy: number; isLive: boolean; protocol: string; chainId: string; asset: string; compoundFreq?: number; }): SandboxResult { const freq = config.compoundFreq ?? 365; const milestones = MILESTONE_DAYS.map((d) => computeYieldAtDays(config.principal, config.apy, d, freq), ); return { principal: config.principal, apy: config.apy, isLive: config.isLive, protocol: config.protocol, chainId: config.chainId, asset: config.asset, milestones, }; } // ── APY resolution: live > mock ── export function resolveApy( protocol: string, chainId: string, asset: string, liveRates?: LiveRate[], ): { apy: number; isLive: boolean } { if (liveRates?.length) { const match = liveRates.find( (r) => r.protocol === protocol && r.chainId === chainId && r.asset === asset, ); if (match && match.apy > 0) return { apy: match.apy, isLive: true }; } const key = `${protocol}:${chainId}:${asset}`; const fallback = MOCK_FALLBACK_RATES[key]; return fallback != null ? { apy: fallback, isLive: false } : { apy: 0, isLive: false }; } // ── Cross-protocol comparison ── export function buildProtocolComparisons( asset: SandboxAsset, principal: number, days: number, liveRates?: LiveRate[], ): ProtocolComparison[] { const combos: Array<{ protocol: SandboxProtocol; chainId: SandboxChain }> = [ { protocol: "aave-v3", chainId: "1" }, { protocol: "aave-v3", chainId: "8453" }, { protocol: "morpho-blue", chainId: "1" }, { protocol: "morpho-blue", chainId: "8453" }, ]; const results: ProtocolComparison[] = []; for (const { protocol, chainId } of combos) { const { apy, isLive } = resolveApy(protocol, chainId, asset, liveRates); if (apy <= 0) continue; const { balance, earnings } = computeYieldAtDays(principal, apy, days); const label = PROTOCOL_LABELS[`${protocol}:${chainId}`] || `${protocol} ${chainId}`; results.push({ protocol, chainId, label, apy, isLive, earnings, balance }); } results.sort((a, b) => b.apy - a.apy); return results; }