rspace-online/modules/rwallet/lib/yield-positions.ts

156 lines
4.3 KiB
TypeScript

/**
* 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<string | null> {
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<YieldPosition[]> {
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;
}