158 lines
3.9 KiB
TypeScript
158 lines
3.9 KiB
TypeScript
/**
|
|
* 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<string, number> = {
|
|
"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<string, string> = {
|
|
"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;
|
|
}
|