diff --git a/src/endpoints.ts b/src/endpoints.ts index d15f0eb..175c245 100644 --- a/src/endpoints.ts +++ b/src/endpoints.ts @@ -3,7 +3,8 @@ import { isAddress } from "viem"; import type { GearAPY } from "./apy"; import type { ApyDetails, Fetcher } from "./fetcher"; -import { isSupportedNetwork } from "./utils"; +import type { PointsInfo } from "./points/constants"; +import { isSupportedNetwork, toJSONWithBigint } from "./utils"; interface Response { status: string; @@ -17,6 +18,7 @@ interface OutputDetails { symbol: string; rewards: { apy: Array; + points: Array; }; } @@ -31,23 +33,22 @@ export async function getByChainAndToken(req: any, res: any, fetcher: Fetcher) { if (!checkResp(isTokenValid, res)) { return; } + + const a = fetcher.cache[chainId]?.apyList?.[tokenAddress as Address]; + const p = fetcher.cache[chainId]?.pointsList?.[tokenAddress as Address]; + const data: OutputDetails = { chainId: chainId, address: tokenAddress.toLowerCase(), - symbol: "", + symbol: a?.symbol || p?.symbol || "", rewards: { - apy: [], + apy: a.apys || [], + points: p ? [p] : [], }, }; - const d = fetcher.cache[chainId]?.tokens?.[tokenAddress as Address]; - if (d) { - data.rewards.apy = d.apys; - data.symbol = d.symbol; - } - res.set({ "Content-Type": "application/json" }); - res.send(JSON.stringify({ data: data, status: "ok" } as Response)); + res.send(toJSONWithBigint({ data: data, status: "ok" } as Response)); // } @@ -56,21 +57,45 @@ export async function getAll(req: any, res: any, fetcher: Fetcher) { if (!checkResp(isChainIdValid, res)) { return; } - const data: Array = []; - Object.entries(fetcher.cache[chainId]?.tokens).forEach(([token, apy]) => { - data.push({ + const data = Object.entries(fetcher.cache[chainId]?.apyList || {}).reduce< + Record + >((acc, [t, a]) => { + acc[t as Address] = { chainId: chainId, - address: token, - symbol: apy.symbol, + address: t, + symbol: a.symbol, rewards: { - apy: apy.apys, + apy: a.apys, + points: [], }, - }); + }; + + return acc; + }, {}); + + Object.entries(fetcher.cache[chainId]?.pointsList || {}).forEach(([t, p]) => { + const token = t as Address; + + if (data[token]) { + data[token].rewards.points.push(p); + } else { + data[token] = { + chainId: chainId, + address: t, + symbol: p.symbol, + rewards: { + apy: [], + points: [p], + }, + }; + } }); res.set({ "Content-Type": "application/json" }); - res.send(JSON.stringify({ data: data, status: "ok" } as Response)); + res.send( + toJSONWithBigint({ data: Object.values(data), status: "ok" } as Response), + ); } export async function getRewardList(req: any, res: any, fetcher: Fetcher) { @@ -83,27 +108,30 @@ export async function getRewardList(req: any, res: any, fetcher: Fetcher) { res, ); } - const [isTokenList, tokenList] = checkTokenList(JSON.stringify(req.body)); + const [isTokenList, tokenList] = checkTokenList(toJSONWithBigint(req.body)); if (!checkResp(isTokenList, res)) { return; } const data: Array = []; - for (const entry of tokenList) { - const apys = - fetcher.cache[entry.chain_id]?.tokens?.[entry.token_address as Address]; + for (const t of tokenList) { + const a = fetcher.cache[t.chain_id]?.apyList?.[t.token_address as Address]; + const p = + fetcher.cache[t.chain_id]?.pointsList?.[t.token_address as Address]; + data.push({ - chainId: entry.chain_id, - address: entry.token_address.toLowerCase(), - symbol: apys.symbol, + chainId: t.chain_id, + address: t.token_address.toLowerCase(), + symbol: a.symbol, rewards: { - apy: apys.apys, + apy: a.apys || [], + points: p ? [p] : [], }, }); } res.set({ "Content-Type": "application/json" }); - res.send(JSON.stringify({ data: data, status: "ok" } as Response)); + res.send(toJSONWithBigint({ data: data, status: "ok" } as Response)); } export async function getGearAPY(req: any, res: any, fetcher: Fetcher) { @@ -114,7 +142,7 @@ export async function getGearAPY(req: any, res: any, fetcher: Fetcher) { res.set({ "Content-Type": "application/json" }); res.send( - JSON.stringify({ + toJSONWithBigint({ data: fetcher.cache[chainId]?.gear, status: "ok", } as Response), @@ -157,13 +185,13 @@ function checkTokenAddress(data: any): [Response, string] { "", ]; } - return [{ status: "ok" }, (notUndefined as Address).toString()]; + return [{ status: "ok" }, notUndefined.toString()]; } export function checkResp(res: Response, out: any): boolean { if (res.status === "error") { out.set({ "Content-Type": "application/json" }); - out.send(JSON.stringify(res)); + out.send(toJSONWithBigint(res)); return false; } return true; diff --git a/src/fetcher.ts b/src/fetcher.ts index 0fd34c1..af976b7 100644 --- a/src/fetcher.ts +++ b/src/fetcher.ts @@ -12,6 +12,8 @@ import { getAPYYearn, getGearAPY, } from "./apy"; +import { getPoints } from "./points"; +import type { PointsInfo } from "./points/constants"; import type { Apy, APYResult, NetworkType, TokenAPY } from "./utils"; import { getChainId, supportedChains } from "./utils"; @@ -19,13 +21,15 @@ export type ApyDetails = Apy & { lastUpdated: string }; type TokenDetails = TokenAPY; interface NetworkState { - tokens: Record; + apyList: Record; + pointsList: Record; gear: GearAPY; } function log( network: NetworkType, allProtocolAPYs: Array>, + pointsList: PromiseSettledResult>, gearAPY: PromiseSettledResult, ) { const list = allProtocolAPYs.map(apyRes => { @@ -52,6 +56,16 @@ function log( } else { console.log(`Gear error: ${gearAPY.reason}`); } + + if (pointsList.status === "fulfilled") { + console.log( + `Fetched points for ${Object.values(pointsList.value) + .map(p => p.symbol) + .join(", ")} for ${network}`, + ); + } else { + console.log(`Points error: ${pointsList.reason}`); + } } export class Fetcher { @@ -62,8 +76,11 @@ export class Fetcher { } private async getNetworkState(network: NetworkType): Promise { - const [gearAPY, ...allProtocolAPYs] = await Promise.allSettled([ + const [gearAPY, points, ...allProtocolAPYs] = await Promise.allSettled([ getGearAPY(network), + + getPoints(network), + getAPYCurve(network), getAPYEthena(network), getAPYLama(network), @@ -72,39 +89,46 @@ export class Fetcher { getAPYYearn(network), getAPYConstant(network), ]); - log(network, allProtocolAPYs, gearAPY); + log(network, allProtocolAPYs, points, gearAPY); - const result: Record = {}; const time = moment().utc().format(); - allProtocolAPYs.forEach(apyRes => { - if (apyRes.status === "fulfilled") { - Object.entries(apyRes.value).forEach(([addr, tokenAPY]) => { - const address = addr.toLowerCase() as Address; - - const apyList = tokenAPY?.apys.map( - ({ reward, ...rest }): ApyDetails => ({ - ...rest, - lastUpdated: time, - reward: reward.toLowerCase() as Address, - }), - ); - - result[address] = { - ...tokenAPY, - address, - apys: [...(result[address]?.apys || []), ...apyList], - }; - }); - } - }); + const apyList = allProtocolAPYs.reduce>( + (acc, apyRes) => { + if (apyRes.status === "fulfilled") { + Object.entries(apyRes.value).forEach(([addr, tokenAPY]) => { + const address = addr.toLowerCase() as Address; + + const apyList = tokenAPY?.apys.map( + ({ reward, ...rest }): ApyDetails => ({ + ...rest, + lastUpdated: time, + reward: reward.toLowerCase() as Address, + }), + ); + + acc[address] = { + ...tokenAPY, + address, + apys: [...(acc[address]?.apys || []), ...apyList], + }; + }); + } + + return acc; + }, + {}, + ); + + const pointsList = points.status === "fulfilled" ? points.value : {}; return { gear: gearAPY.status === "fulfilled" ? gearAPY.value : { base: 0, crv: 0, gear: 0 }, - tokens: result, + apyList, + pointsList, }; } diff --git a/src/points/constants.ts b/src/points/constants.ts new file mode 100644 index 0000000..329b52e --- /dev/null +++ b/src/points/constants.ts @@ -0,0 +1,299 @@ +import type { Address } from "viem"; + +import type { NetworkType } from "../utils"; + +type PointsType = + | "eigenlayer" + | "renzo" + | "etherfi" + | "kelp" + | "swell" + | "puffer" + | "ethena" + | "symbiotic" + | "mellow" + | "lombard" + | "babylon" + | "veda" + | "karak" + | "pumpBTC"; + +interface PointsReward { + name: string; + units: string; + multiplier: number | "soon"; + type: PointsType; +} + +interface DebtReward extends PointsReward { + cm: Address; +} + +type CommonReward = + CM extends undefined ? PointsReward : DebtReward; + +const REWARDS_BASE_INFO = { + eigenlayer: (multiplier: PointsReward["multiplier"]): PointsReward => ({ + name: "Eigenlayer", + units: "points", + multiplier, + type: "eigenlayer", + }), + renzo: (multiplier: PointsReward["multiplier"]): PointsReward => ({ + name: "Renzo", + units: "points", + multiplier, + type: "renzo", + }), + etherfi: (multiplier: PointsReward["multiplier"]): PointsReward => ({ + name: "Ether.fi", + units: "points", + multiplier, + type: "etherfi", + }), + kelp: (multiplier: PointsReward["multiplier"]): PointsReward => ({ + name: "Kelp", + units: "Miles", + multiplier, + type: "kelp", + }), + swell: (multiplier: PointsReward["multiplier"]): PointsReward => ({ + name: "Swell", + units: "points", + multiplier, + type: "swell", + }), + puffer: (multiplier: PointsReward["multiplier"]): PointsReward => ({ + name: "Puffer", + units: "points", + multiplier, + type: "puffer", + }), + ethena: ( + multiplier: PointsReward["multiplier"], + cm?: CM, + ): CommonReward => { + return { + name: "Ethena", + units: "sats", + multiplier, + type: "ethena", + ...(cm ? { cm } : {}), + } as CommonReward; + }, + + symbiotic: (multiplier: PointsReward["multiplier"]): PointsReward => ({ + name: "Symbiotic", + units: "points", + multiplier, + type: "symbiotic", + }), + mellow: (multiplier: PointsReward["multiplier"]): PointsReward => ({ + name: "Mellow", + units: "points", + multiplier, + type: "mellow", + }), + + lombard: (multiplier: PointsReward["multiplier"]): PointsReward => ({ + name: "Lombard", + units: "points", + multiplier, + type: "lombard", + }), + babylon: (multiplier: PointsReward["multiplier"]): PointsReward => ({ + name: "Babylon", + units: "points", + multiplier, + type: "babylon", + }), + + veda: (multiplier: PointsReward["multiplier"]): PointsReward => ({ + name: "Veda", + units: "points", + multiplier, + type: "veda", + }), + karak: (multiplier: PointsReward["multiplier"]): PointsReward => ({ + name: "Karak", + units: "points", + multiplier, + type: "karak", + }), + + pumpBTC: (multiplier: PointsReward["multiplier"]): PointsReward => ({ + name: "Pump BTC", + units: "points", + multiplier, + type: "pumpBTC", + }), +}; + +export interface PointsInfo { + symbol: string; + address: Address; + rewards: Array; + debtRewards?: Array; +} + +export const POINTS_INFO_BY_NETWORK: Record> = { + Mainnet: [ + { + address: "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee", + symbol: "weETH", + rewards: [ + REWARDS_BASE_INFO.eigenlayer(100), + REWARDS_BASE_INFO.etherfi(200), + ], + }, + { + address: "0xbf5495Efe5DB9ce00f80364C8B423567e58d2110", + symbol: "ezETH", + rewards: [ + REWARDS_BASE_INFO.eigenlayer(100), + REWARDS_BASE_INFO.renzo(300), + ], + }, + { + address: "0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7", + symbol: "rsETH", + rewards: [REWARDS_BASE_INFO.eigenlayer(100), REWARDS_BASE_INFO.kelp(200)], + }, + { + address: "0xFAe103DC9cf190eD75350761e95403b7b8aFa6c0", + symbol: "rswETH", + rewards: [ + REWARDS_BASE_INFO.eigenlayer(100), + REWARDS_BASE_INFO.swell(450), + ], + }, + { + address: "0xD9A442856C234a39a81a089C06451EBAa4306a72", + symbol: "pufETH", + rewards: [ + REWARDS_BASE_INFO.eigenlayer(100), + REWARDS_BASE_INFO.puffer(100), + ], + }, + + { + address: "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497", + symbol: "sUSDe", + rewards: [REWARDS_BASE_INFO.ethena(500)], + debtRewards: [ + REWARDS_BASE_INFO.ethena( + 500, + "0x58c8e983d9479b69b64970f79e8965ea347189c9", + ), + ], + }, + { + address: "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3", + symbol: "USDe", + rewards: [REWARDS_BASE_INFO.ethena(2000)], + debtRewards: [ + REWARDS_BASE_INFO.ethena( + 500, + "0x58c8e983d9479b69b64970f79e8965ea347189c9", + ), + ], + }, + + { + address: "0x8c9532a60E0E7C6BbD2B2c1303F63aCE1c3E9811", + symbol: "pzETH", + rewards: [ + REWARDS_BASE_INFO.renzo(300), + REWARDS_BASE_INFO.symbiotic(100), + REWARDS_BASE_INFO.mellow(200), + ], + }, + { + address: "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a", + symbol: "rstETH", + rewards: [ + REWARDS_BASE_INFO.symbiotic(100), + REWARDS_BASE_INFO.mellow(200), + ], + }, + { + address: "0xBEEF69Ac7870777598A04B2bd4771c71212E6aBc", + symbol: "steakLRT", + rewards: [ + REWARDS_BASE_INFO.symbiotic(100), + REWARDS_BASE_INFO.mellow(200), + ], + }, + { + address: "0x5fD13359Ba15A84B76f7F87568309040176167cd", + symbol: "amphrETH", + rewards: [ + REWARDS_BASE_INFO.symbiotic(100), + REWARDS_BASE_INFO.mellow(200), + ], + }, + + { + address: "0x8236a87084f8B84306f72007F36F2618A5634494", + symbol: "LBTC", + rewards: [REWARDS_BASE_INFO.babylon(100), REWARDS_BASE_INFO.lombard(300)], + }, + { + address: "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a", + symbol: "Re7LRT", + rewards: [ + REWARDS_BASE_INFO.symbiotic(100), + REWARDS_BASE_INFO.mellow(200), + ], + }, + + { + address: "0x657e8C867D8B37dCC18fA4Caead9C45EB088C642", + symbol: "eBTC", + rewards: [ + REWARDS_BASE_INFO.babylon(100), + REWARDS_BASE_INFO.symbiotic(100), + REWARDS_BASE_INFO.etherfi(200), + REWARDS_BASE_INFO.lombard(400), + REWARDS_BASE_INFO.veda(300), + REWARDS_BASE_INFO.karak(200), + ], + }, + + { + address: "0xF469fBD2abcd6B9de8E169d128226C0Fc90a012e", + symbol: "pumpBTC", + rewards: [REWARDS_BASE_INFO.babylon(100), REWARDS_BASE_INFO.pumpBTC(200)], + }, + ], + Arbitrum: [ + { + address: "0x2416092f143378750bb29b79eD961ab195CcEea5", + symbol: "ezETH", + rewards: [ + REWARDS_BASE_INFO.eigenlayer(100), + REWARDS_BASE_INFO.renzo(300), + ], + }, + { + address: "0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34", + symbol: "USDe", + rewards: [REWARDS_BASE_INFO.ethena(2000)], + }, + { + address: "0x4186BFC76E2E237523CBC30FD220FE055156b41F", + symbol: "rsETH", + rewards: [REWARDS_BASE_INFO.eigenlayer(100), REWARDS_BASE_INFO.kelp(300)], + }, + ], + Optimism: [ + { + address: "0x2416092f143378750bb29b79eD961ab195CcEea5", + symbol: "ezETH", + rewards: [ + REWARDS_BASE_INFO.eigenlayer(100), + REWARDS_BASE_INFO.renzo(300), + ], + }, + ], +}; diff --git a/src/points/index.ts b/src/points/index.ts new file mode 100644 index 0000000..1ce67fa --- /dev/null +++ b/src/points/index.ts @@ -0,0 +1 @@ +export * from "./points"; diff --git a/src/points/points.ts b/src/points/points.ts new file mode 100644 index 0000000..bcf3a21 --- /dev/null +++ b/src/points/points.ts @@ -0,0 +1,31 @@ +import type { Address } from "viem"; + +import type { PointsHandler, PointsResult } from "../utils"; +import { POINTS_INFO_BY_NETWORK } from "./constants"; + +const getPoints: PointsHandler = async network => { + const points = POINTS_INFO_BY_NETWORK[network]; + + const result = points.reduce((acc, p) => { + const address = p.address.toLowerCase() as Address; + + acc[address] = { + ...p, + address, + ...(p.debtRewards + ? { + debtRewards: p.debtRewards.map(r => ({ + ...r, + cm: r.cm.toLowerCase() as Address, + })), + } + : {}), + }; + + return acc; + }, {}); + + return result; +}; + +export { getPoints }; diff --git a/src/utils/index.ts b/src/utils/index.ts index bae3478..a98b609 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,7 @@ import type { Address } from "viem"; +import type { PointsInfo } from "../points/constants"; + export interface Apy { reward: Address; symbol: string; @@ -16,9 +18,11 @@ export interface TokenAPY { } export type APYResult = Record; - export type APYHandler = (network: NetworkType) => Promise; +export type PointsResult = Record; +export type PointsHandler = (network: NetworkType) => Promise; + export const supportedChains = ["Mainnet", "Arbitrum", "Optimism"] as const; export type NetworkType = (typeof supportedChains)[number]; const CHAINS = { @@ -34,3 +38,11 @@ export function getChainId(network: NetworkType) { export function isSupportedNetwork(chainId: number) { return Object.values(CHAINS).includes(chainId); } + +export function toJSONWithBigint(o: any) { + const r = JSON.stringify(o, (_, v) => + typeof v === "bigint" ? v.toString() : v, + ); + + return r; +}