/** * Yield position tracking — queries on-chain aToken and vault share balances * for a given address across Aave V3 and Morpho Blue vaults. */ import type { YieldPosition, SupportedYieldChain, StablecoinSymbol } from "./yield-protocols"; import { AAVE_ATOKENS, MORPHO_VAULTS, STABLECOIN_ADDRESSES, STABLECOIN_DECIMALS, encodeBalanceOf, encodeConvertToAssets, YIELD_CHAIN_NAMES, } from "./yield-protocols"; import { getRpcUrl } from "../mod"; import { getYieldRates } from "./yield-rates"; async function rpcCall(rpcUrl: string, to: string, data: string): Promise { try { const res = await fetch(rpcUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_call", params: [{ to, data }, "latest"], }), signal: AbortSignal.timeout(5000), }); const result = (await res.json()).result; if (!result || result === "0x" || result === "0x0") return null; return result; } catch { return null; } } interface PositionQuery { protocol: "aave-v3" | "morpho-blue"; chainId: SupportedYieldChain; asset: StablecoinSymbol; assetAddress: string; vaultAddress: string; vaultName?: string; } function buildPositionQueries(): PositionQuery[] { const queries: PositionQuery[] = []; const chains: SupportedYieldChain[] = ["1", "8453"]; for (const chainId of chains) { // Aave aTokens const aTokens = AAVE_ATOKENS[chainId]; if (aTokens) { for (const [asset, aToken] of Object.entries(aTokens)) { const assetAddr = STABLECOIN_ADDRESSES[chainId]?.[asset as StablecoinSymbol]; if (assetAddr && assetAddr !== "0x0000000000000000000000000000000000000000") { queries.push({ protocol: "aave-v3", chainId, asset: asset as StablecoinSymbol, assetAddress: assetAddr, vaultAddress: aToken, }); } } } // Morpho vaults for (const vault of MORPHO_VAULTS[chainId] || []) { const assetAddr = STABLECOIN_ADDRESSES[chainId]?.[vault.asset]; if (assetAddr && assetAddr !== "0x0000000000000000000000000000000000000000") { queries.push({ protocol: "morpho-blue", chainId, asset: vault.asset, assetAddress: assetAddr, vaultAddress: vault.address, vaultName: vault.name, }); } } } return queries; } export async function getYieldPositions(address: string): Promise { const queries = buildPositionQueries(); const positions: YieldPosition[] = []; // Get current rates for APY enrichment const rates = await getYieldRates(); const rateMap = new Map(rates.map((r) => [`${r.protocol}:${r.chainId}:${r.vaultAddress.toLowerCase()}`, r])); const results = await Promise.allSettled( queries.map(async (q) => { const rpcUrl = getRpcUrl(q.chainId); if (!rpcUrl) return null; const balanceHex = await rpcCall(rpcUrl, q.vaultAddress, encodeBalanceOf(address)); if (!balanceHex) return null; const shares = BigInt(balanceHex); if (shares === 0n) return null; const decimals = STABLECOIN_DECIMALS[q.asset]; let underlying = shares; // For Aave, aToken balance = underlying (rebasing) if (q.protocol === "morpho-blue") { // Convert shares to underlying via convertToAssets const assetsHex = await rpcCall(rpcUrl, q.vaultAddress, encodeConvertToAssets(shares)); if (assetsHex) { underlying = BigInt(assetsHex); } } // Look up APY const rateKey = `${q.protocol}:${q.chainId}:${q.vaultAddress.toLowerCase()}`; const rate = rateMap.get(rateKey); const apy = rate?.apy || 0; const underlyingUSD = Number(underlying) / 10 ** decimals; // stablecoins ≈ $1 const annualEarnings = underlyingUSD * (apy / 100); const dailyEarnings = annualEarnings / 365; return { protocol: q.protocol, chainId: q.chainId, asset: q.asset, assetAddress: q.assetAddress, vaultAddress: q.vaultAddress, shares: shares.toString(), underlying: underlying.toString(), decimals, apy, dailyEarnings, annualEarnings, } satisfies YieldPosition; }), ); for (const r of results) { if (r.status === "fulfilled" && r.value) { positions.push(r.value); } } // Sort by underlying value descending positions.sort((a, b) => { const aVal = Number(BigInt(b.underlying)) / 10 ** b.decimals; const bVal = Number(BigInt(a.underlying)) / 10 ** a.decimals; return aVal - bVal; }); return positions; }