diff --git a/packages/api/src/routes/auxiliary/auxiliaryController.ts b/packages/api/src/routes/auxiliary/auxiliaryController.ts index ac4109f5..f95b3887 100644 --- a/packages/api/src/routes/auxiliary/auxiliaryController.ts +++ b/packages/api/src/routes/auxiliary/auxiliaryController.ts @@ -1,8 +1,6 @@ import { Request, Response } from 'express' -import { cacheStore } from 'route-cache' -import { eigenContracts } from '../../data/address/eigenMainnetContracts' import { handleAndReturnErrorResponse } from '../../schema/errors' -import { fetchStrategyTokenPrices } from '../../utils/tokenPrices' +import { fetchTokenPrices } from '../../utils/tokenPrices' import { getPrismaClient } from '../../utils/prismaClient' /** @@ -13,32 +11,9 @@ import { getPrismaClient } from '../../utils/prismaClient' */ export async function getCachedPrices(req: Request, res: Response) { try { - const CMC_TOKEN_IDS = [ - 8100, 21535, 27566, 23782, 29035, 24277, 28476, 15060, 23177, 8085, 25147, 24760, 2396 - ] - const keysStr = CMC_TOKEN_IDS.join(',') - let cachedPrices = await cacheStore.get(`price_${keysStr}`) + const tokenPrices = await fetchTokenPrices() - if (!cachedPrices) { - cachedPrices = await fetchStrategyTokenPrices() - } - - const priceData = Object.values(cachedPrices).map( - (cachedPrice: { - symbol: string - strategyAddress: string - eth: number - tokenAddress?: string - }) => { - const strategy = eigenContracts.Strategies[cachedPrice.symbol] - return { - ...cachedPrice, - tokenAddress: strategy?.tokenContract || null - } - } - ) - - res.status(200).send(priceData) + res.status(200).send(tokenPrices) } catch (error) { handleAndReturnErrorResponse(req, res, error) } @@ -65,3 +40,22 @@ export async function getLastSyncBlocks(req: Request, res: Response) { handleAndReturnErrorResponse(req, res, error) } } + +/** + * Route to fetch and display all strategies and tokens + * + * @param req + * @param res + */ +export async function getStrategies(req: Request, res: Response) { + try { + const prismaClient = getPrismaClient() + + const strategies = await prismaClient.strategies.findMany() + const tokens = await prismaClient.tokens.findMany() + + res.status(200).send({ strategies, tokens }) + } catch (error) { + handleAndReturnErrorResponse(req, res, error) + } +} diff --git a/packages/api/src/routes/auxiliary/auxiliaryRoutes.ts b/packages/api/src/routes/auxiliary/auxiliaryRoutes.ts index f1b935bd..05a82fcf 100644 --- a/packages/api/src/routes/auxiliary/auxiliaryRoutes.ts +++ b/packages/api/src/routes/auxiliary/auxiliaryRoutes.ts @@ -1,9 +1,10 @@ import express from 'express' -import { getCachedPrices, getLastSyncBlocks } from './auxiliaryController' +import { getCachedPrices, getLastSyncBlocks, getStrategies } from './auxiliaryController' const router = express.Router() router.get('/prices', getCachedPrices) router.get('/sync-status', getLastSyncBlocks) +router.get('/strategies', getStrategies) export default router diff --git a/packages/api/src/routes/avs/avsController.ts b/packages/api/src/routes/avs/avsController.ts index 99a1af84..16c70934 100644 --- a/packages/api/src/routes/avs/avsController.ts +++ b/packages/api/src/routes/avs/avsController.ts @@ -1,7 +1,5 @@ import type { Request, Response } from 'express' -import type { IMap } from '../../schema/generic' import type { Submission } from '../rewards/rewardController' -import { type EigenStrategiesContractAddress, getEigenContracts } from '../../data/address' import { PaginationQuerySchema } from '../../schema/zod/schemas/paginationQuery' import { EthereumAddressSchema } from '../../schema/zod/schemas/base/ethereumAddress' import { WithTvlQuerySchema } from '../../schema/zod/schemas/withTvlQuery' @@ -12,16 +10,15 @@ import { SearchByTextQuerySchema } from '../../schema/zod/schemas/searchByTextQu import { WithRewardsQuerySchema } from '../../schema/zod/schemas/withRewardsQuery' import { getOperatorSearchQuery } from '../operators/operatorController' import { handleAndReturnErrorResponse } from '../../schema/errors' -import { fetchRewardTokenPrices, fetchStrategyTokenPrices } from '../../utils/tokenPrices' -import { getNetwork } from '../../viem/viemClient' -import { holesky } from 'viem/chains' import { getStrategiesWithShareUnderlying, sharesToTVL, - sharesToTVLEth -} from '../strategies/strategiesController' + sharesToTVLStrategies +} from '../../utils/strategyShares' import Prisma from '@prisma/client' import prisma from '../../utils/prismaClient' +import { fetchTokenPrices } from '../../utils/tokenPrices' +import { withOperatorShares } from '../../utils/operatorShares' /** * Function for route /avs @@ -106,7 +103,6 @@ export async function getAllAVS(req: Request, res: Response) { } }) - const strategyTokenPrices = withTvl ? await fetchStrategyTokenPrices() : {} const strategiesWithSharesUnderlying = withTvl ? await getStrategiesWithShareUnderlying() : [] const data = await Promise.all( @@ -121,9 +117,7 @@ export async function getAllAVS(req: Request, res: Response) { totalOperators: avs.totalOperators, totalStakers: avs.totalStakers, shares, - tvl: withTvl - ? sharesToTVL(shares, strategiesWithSharesUnderlying, strategyTokenPrices) - : undefined, + tvl: withTvl ? sharesToTVL(shares, strategiesWithSharesUnderlying) : undefined, operators: undefined, metadataUrl: undefined, isMetadataSynced: undefined, @@ -266,7 +260,6 @@ export async function getAVS(req: Request, res: Response) { (s) => avs.restakeableStrategies.indexOf(s.strategyAddress.toLowerCase()) !== -1 ) - const strategyTokenPrices = withTvl ? await fetchStrategyTokenPrices() : {} const strategiesWithSharesUnderlying = withTvl ? await getStrategiesWithShareUnderlying() : [] res.send({ @@ -275,9 +268,7 @@ export async function getAVS(req: Request, res: Response) { shares, totalOperators: avs.totalOperators, totalStakers: avs.totalStakers, - tvl: withTvl - ? sharesToTVL(shares, strategiesWithSharesUnderlying, strategyTokenPrices) - : undefined, + tvl: withTvl ? sharesToTVL(shares, strategiesWithSharesUnderlying) : undefined, rewards: withRewards ? await calculateAvsApy(avs) : undefined, operators: undefined, metadataUrl: undefined, @@ -361,7 +352,6 @@ export async function getAVSStakers(req: Request, res: Response) { include: { shares: true } }) - const strategyTokenPrices = withTvl ? await fetchStrategyTokenPrices() : {} const strategiesWithSharesUnderlying = withTvl ? await getStrategiesWithShareUnderlying() : [] const stakers = stakersRecords.map((staker) => { @@ -372,9 +362,7 @@ export async function getAVSStakers(req: Request, res: Response) { return { ...staker, shares, - tvl: withTvl - ? sharesToTVL(shares, strategiesWithSharesUnderlying, strategyTokenPrices) - : undefined + tvl: withTvl ? sharesToTVL(shares, strategiesWithSharesUnderlying) : undefined } }) @@ -469,7 +457,6 @@ export async function getAVSOperators(req: Request, res: Response) { }) : avs.operators.length - const strategyTokenPrices = withTvl ? await fetchStrategyTokenPrices() : {} const strategiesWithSharesUnderlying = withTvl ? await getStrategiesWithShareUnderlying() : [] const data = operatorsRecords.map((operator) => { @@ -486,9 +473,7 @@ export async function getAVSOperators(req: Request, res: Response) { restakedStrategies: avsOperator?.restakedStrategies, shares, totalStakers: operator.stakers.length, - tvl: withTvl - ? sharesToTVL(shares, strategiesWithSharesUnderlying, strategyTokenPrices) - : undefined, + tvl: withTvl ? sharesToTVL(shares, strategiesWithSharesUnderlying) : undefined, stakers: undefined, metadataUrl: undefined, isMetadataSynced: undefined, @@ -541,10 +526,7 @@ export async function getAVSRewards(req: Request, res: Response) { throw new Error('AVS not found.') } - const strategyTokenPrices = await fetchStrategyTokenPrices() - const rewardTokenPrices = await fetchRewardTokenPrices() - const eigenContracts = getEigenContracts() - const tokenToStrategyMap = tokenToStrategyAddressMap(eigenContracts.Strategies) + const tokenPrices = await fetchTokenPrices() const result: { address: string @@ -597,33 +579,18 @@ export async function getAVSRewards(req: Request, res: Response) { const rewardTokenAddress = submission.token.toLowerCase() const strategyAddress = submission.strategyAddress.toLowerCase() - const tokenStrategyAddress = tokenToStrategyMap.get(rewardTokenAddress) // Document reward token & rewarded strategy addresses if (!rewardTokens.includes(rewardTokenAddress)) rewardTokens.push(rewardTokenAddress) - if (!rewardStrategies.includes(strategyAddress)) rewardStrategies.push(strategyAddress) - // Normalize reward amount to its ETH price - if (tokenStrategyAddress) { - const tokenPrice = Object.values(strategyTokenPrices).find( - (tp) => tp.strategyAddress.toLowerCase() === tokenStrategyAddress - ) - const amountInEth = amount.mul(new Prisma.Prisma.Decimal(tokenPrice?.eth ?? 0)) + if (rewardTokenAddress) { + const tokenPrice = tokenPrices.find((tp) => tp.address.toLowerCase() === rewardTokenAddress) + const amountInEth = amount + .div(new Prisma.Prisma.Decimal(10).pow(tokenPrice?.decimals ?? 18)) + .mul(new Prisma.Prisma.Decimal(tokenPrice?.ethPrice ?? 0)) + .mul(new Prisma.Prisma.Decimal(10).pow(18)) // 18 decimals currentTotalAmountEth = currentTotalAmountEth.add(amountInEth) - } else { - // Check if it is a reward token which isn't a strategy on EL - for (const [, price] of Object.entries(rewardTokenPrices)) { - if (price && price.tokenAddress.toLowerCase() === rewardTokenAddress) { - const amountInEth = amount.mul(new Prisma.Prisma.Decimal(price?.eth ?? 0)) - currentTotalAmountEth = currentTotalAmountEth.add(amountInEth) - } else { - // Check for special tokens - currentTotalAmountEth = isSpecialToken(rewardTokenAddress) - ? currentTotalAmountEth.add(amount) - : new Prisma.Prisma.Decimal(0) - } - } } currentSubmission.strategies.push({ @@ -638,7 +605,7 @@ export async function getAVSRewards(req: Request, res: Response) { currentSubmission.totalAmount = currentTotalAmount.toString() result.submissions.push(currentSubmission) result.totalSubmissions++ - result.totalRewards += currentTotalAmountEth.toNumber() + result.totalRewards += currentTotalAmountEth.toNumber() // 18 decimals } result.rewardTokens = rewardTokens @@ -683,32 +650,6 @@ export async function invalidateMetadata(req: Request, res: Response) { // --- Helper functions --- -export function withOperatorShares(avsOperators) { - const sharesMap: IMap = new Map() - - avsOperators.map((avsOperator) => { - const shares = avsOperator.operator.shares.filter( - (s) => avsOperator.restakedStrategies.indexOf(s.strategyAddress.toLowerCase()) !== -1 - ) - - shares.map((s) => { - if (!sharesMap.has(s.strategyAddress)) { - sharesMap.set(s.strategyAddress, '0') - } - - sharesMap.set( - s.strategyAddress, - (BigInt(sharesMap.get(s.strategyAddress)) + BigInt(s.shares)).toString() - ) - }) - }) - - return Array.from(sharesMap, ([strategyAddress, shares]) => ({ - strategyAddress, - shares - })) -} - export function getAvsFilterQuery(filterName?: boolean) { const queryWithName = filterName ? { @@ -788,11 +729,7 @@ export function getAvsSearchQuery( // biome-ignore lint/suspicious/noExplicitAny: async function calculateAvsApy(avs: any) { try { - const strategyTokenPrices = await fetchStrategyTokenPrices() - const rewardTokenPrices = await fetchRewardTokenPrices() - const eigenContracts = getEigenContracts() - const tokenToStrategyMap = tokenToStrategyAddressMap(eigenContracts.Strategies) - + const tokenPrices = await fetchTokenPrices() const strategiesWithSharesUnderlying = await getStrategiesWithShareUnderlying() // Get share amounts for each restakeable strategy @@ -801,11 +738,7 @@ async function calculateAvsApy(avs: any) { ) // Fetch the AVS tvl for each strategy - const tvlStrategiesEth = sharesToTVLEth( - shares, - strategiesWithSharesUnderlying, - strategyTokenPrices - ) + const tvlStrategiesEth = sharesToTVLStrategies(shares, strategiesWithSharesUnderlying) // Iterate through each strategy and calculate all its rewards const strategiesApy = avs.restakeableStrategies.map((strategyAddress) => { @@ -824,28 +757,15 @@ async function calculateAvsApy(avs: any) { for (const submission of relevantSubmissions) { let rewardIncrementEth = new Prisma.Prisma.Decimal(0) const rewardTokenAddress = submission.token.toLowerCase() - const tokenStrategyAddress = tokenToStrategyMap.get(rewardTokenAddress) // Normalize reward amount to its ETH price - if (tokenStrategyAddress) { - const tokenPrice = Object.values(strategyTokenPrices).find( - (tp) => tp.strategyAddress.toLowerCase() === tokenStrategyAddress + if (rewardTokenAddress) { + const tokenPrice = tokenPrices.find( + (tp) => tp.address.toLowerCase() === rewardTokenAddress ) - rewardIncrementEth = submission.amount.mul( - new Prisma.Prisma.Decimal(tokenPrice?.eth ?? 0) - ) - } else { - // Check if it is a reward token which isn't a strategy on EL - for (const [, price] of Object.entries(rewardTokenPrices)) { - if (price && price.tokenAddress.toLowerCase() === rewardTokenAddress) { - rewardIncrementEth = submission.amount.mul(new Prisma.Prisma.Decimal(price.eth ?? 0)) - } else { - // Check for special tokens - rewardIncrementEth = isSpecialToken(rewardTokenAddress) - ? submission.amount - : new Prisma.Prisma.Decimal(0) - } - } + rewardIncrementEth = submission.amount + .mul(new Prisma.Prisma.Decimal(tokenPrice?.ethPrice ?? 0)) + .div(new Prisma.Prisma.Decimal(10).pow(tokenPrice?.decimals ?? 18)) // No decimals } // Multiply reward amount in ETH by the strategy weight @@ -853,7 +773,7 @@ async function calculateAvsApy(avs: any) { .mul(submission.multiplier) .div(new Prisma.Prisma.Decimal(10).pow(18)) - totalRewardsEth = totalRewardsEth.add(rewardIncrementEth) + totalRewardsEth = totalRewardsEth.add(rewardIncrementEth) // No decimals totalDuration += submission.duration } @@ -862,8 +782,7 @@ async function calculateAvsApy(avs: any) { } // Annualize the reward basis its duration to find yearly APY - const rewardRate = - totalRewardsEth.div(new Prisma.Prisma.Decimal(10).pow(18)).toNumber() / strategyTvl + const rewardRate = totalRewardsEth.toNumber() / strategyTvl const annualizedRate = rewardRate * ((365 * 24 * 60 * 60) / totalDuration) const apy = annualizedRate * 100 @@ -879,42 +798,3 @@ async function calculateAvsApy(avs: any) { } } catch {} } - -/** - * Return a map of strategy addresses <> token addresses - * - * @param strategies - * @returns - */ -export function tokenToStrategyAddressMap( - strategies: EigenStrategiesContractAddress -): Map { - const map = new Map() - for (const [key, value] of Object.entries(strategies)) { - if (key !== 'Eigen' && value?.tokenContract && value?.strategyContract) { - map.set(value.tokenContract.toLowerCase(), value.strategyContract.toLowerCase()) - } - } - return map -} - -/** - * Returns whether a given token address belongs to a list of special tokens - * - * @param tokenAddress - * @returns - */ -export function isSpecialToken(tokenAddress: string): boolean { - const specialTokens = - getNetwork() === holesky - ? [ - '0x6Cc9397c3B38739daCbfaA68EaD5F5D77Ba5F455', // WETH - '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' - ] - : [ - '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // WETH - '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' - ] - - return specialTokens.includes(tokenAddress.toLowerCase()) -} diff --git a/packages/api/src/routes/metrics/metricController.ts b/packages/api/src/routes/metrics/metricController.ts index e9585ca8..f8bced06 100644 --- a/packages/api/src/routes/metrics/metricController.ts +++ b/packages/api/src/routes/metrics/metricController.ts @@ -1,21 +1,20 @@ import type { Request, Response } from 'express' import type Prisma from '@prisma/client' -import prisma from '../../utils/prismaClient' -import { type EigenStrategiesContractAddress, getEigenContracts } from '../../data/address' +import prisma, { getPrismaClient } from '../../utils/prismaClient' import { EigenExplorerApiError, handleAndReturnErrorResponse } from '../../schema/errors' import { getAvsFilterQuery } from '../avs/avsController' import { HistoricalCountSchema } from '../../schema/zod/schemas/historicalCountQuery' import { EthereumAddressSchema } from '../../schema/zod/schemas/base/ethereumAddress' -import { fetchStrategyTokenPrices } from '../../utils/tokenPrices' import { getContract } from 'viem' import { strategyAbi } from '../../data/abi/strategy' import { getViemClient } from '../../viem/viemClient' -import { getStrategiesWithShareUnderlying } from '../strategies/strategiesController' +import { getStrategiesWithShareUnderlying } from '../../utils/strategyShares' import { type CirculatingSupplyWithChange, fetchEthCirculatingSupply } from '../../utils/ethCirculatingSupply' import { WithChangeQuerySchema } from '../../schema/zod/schemas/withChangeQuery' +import { fetchTokenPrices } from '../../utils/tokenPrices' const beaconAddress = '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' @@ -273,8 +272,9 @@ export async function getTvlRestakingByStrategy(req: Request, res: Response) { const { strategy } = req.params const { withChange } = queryCheck.data - const strategies = Object.keys(getEigenContracts().Strategies) - const foundStrategy = strategies.find((s) => s.toLowerCase() === strategy.toLowerCase()) + const foundStrategy = await prisma.strategies.findUnique({ + where: { address: strategy.toLowerCase() } + }) if (!foundStrategy) { throw new EigenExplorerApiError({ @@ -284,7 +284,8 @@ export async function getTvlRestakingByStrategy(req: Request, res: Response) { } const tvlResponse = await doGetTvlStrategy( - getEigenContracts().Strategies[foundStrategy].strategyContract, + foundStrategy.address as `0x${string}`, + foundStrategy.underlyingToken as `0x${string}`, withChange ) @@ -789,51 +790,45 @@ async function doGetTvl(withChange: boolean) { let tvlRestaking: TvlWithoutChange = 0 const ethPrices = withChange ? await fetchCurrentEthPrices() : undefined - const strategyKeys = Object.keys(getEigenContracts().Strategies) - const strategiesContracts = strategyKeys.map((s) => - getContract({ - address: getEigenContracts().Strategies[s].strategyContract, - abi: strategyAbi, - client: getViemClient() - }) - ) - - const tvlStrategies = {} - const tvlStrategiesEth: Map = new Map( - strategyKeys.map((sk) => [sk as keyof EigenStrategiesContractAddress, 0]) - ) + const tvlStrategies: Map = new Map() + const tvlStrategiesEth: Map = new Map() try { + const strategiesWithSharesUnderlying = await getStrategiesWithShareUnderlying() + const totalShares = await Promise.all( - strategiesContracts.map(async (sc, i) => ({ - strategyKey: strategyKeys[i], - strategyAddress: sc.address.toLowerCase(), - shares: (await sc.read.totalShares()) as string - })) - ) + strategiesWithSharesUnderlying + .filter((s) => s.strategyAddress.toLowerCase() !== beaconAddress.toLowerCase()) + .map(async (su) => { + const strategyContract = getContract({ + address: su.strategyAddress as `0x${string}`, + abi: strategyAbi, + client: getViemClient() + }) - const strategiesWithSharesUnderlying = await getStrategiesWithShareUnderlying() - const strategyTokenPrices = await fetchStrategyTokenPrices() + return { + strategyAddress: strategyContract.address, + shares: (await strategyContract.read.totalShares()) as string + } + }) + ) - totalShares.map((s) => { - const strategyTokenPrice = Object.values(strategyTokenPrices).find( - (stp) => stp.strategyAddress.toLowerCase() === s.strategyAddress.toLowerCase() - ) - const sharesUnderlying = strategiesWithSharesUnderlying.find( - (su) => su.strategyAddress.toLowerCase() === s.strategyAddress.toLowerCase() + strategiesWithSharesUnderlying.map((s) => { + const foundTotalShares = totalShares.find( + (ts) => ts.strategyAddress.toLowerCase() === s.strategyAddress.toLowerCase() ) - if (sharesUnderlying) { + if (foundTotalShares) { const strategyShares = - Number((BigInt(s.shares) * BigInt(sharesUnderlying.sharesToUnderlying)) / BigInt(1e18)) / + Number((BigInt(foundTotalShares.shares) * BigInt(s.sharesToUnderlying)) / BigInt(1e18)) / 1e18 - tvlStrategies[s.strategyKey] = strategyShares + tvlStrategies[s.symbol] = strategyShares - if (strategyTokenPrice) { - const strategyTvl = strategyShares * strategyTokenPrice.eth + if (s.ethPrice) { + const strategyTvl = strategyShares * s.ethPrice - tvlStrategiesEth.set(s.strategyKey as keyof EigenStrategiesContractAddress, strategyTvl) + tvlStrategiesEth.set(s.symbol, strategyTvl) tvlRestaking += strategyTvl } @@ -857,16 +852,20 @@ async function doGetTvl(withChange: boolean) { * @param withChange * @returns */ -async function doGetTvlStrategy(strategy: `0x${string}`, withChange: boolean) { +async function doGetTvlStrategy( + strategy: `0x${string}`, + underlyingToken: `0x${string}`, + withChange: boolean +) { let tvl = 0 let tvlEth = 0 const ethPrices = withChange ? await fetchCurrentEthPrices() : undefined try { - const strategyTokenPrices = await fetchStrategyTokenPrices() - const strategyTokenPrice = Object.values(strategyTokenPrices).find( - (stp) => stp.strategyAddress.toLowerCase() === strategy.toLowerCase() + const tokenPrices = await fetchTokenPrices() + const strategyTokenPrice = tokenPrices.find( + (tp) => tp.address.toLowerCase() === underlyingToken.toLowerCase() ) const contract = getContract({ @@ -879,7 +878,7 @@ async function doGetTvlStrategy(strategy: `0x${string}`, withChange: boolean) { Number(await contract.read.sharesToUnderlyingView([await contract.read.totalShares()])) / 1e18 if (strategyTokenPrice) { - tvlEth = tvl * strategyTokenPrice.eth + tvlEth = tvl * strategyTokenPrice.ethPrice } } catch (error) {} @@ -2268,13 +2267,17 @@ function resetTime(date: Date) { * @returns */ async function fetchCurrentEthPrices(): Promise> { - const ethPrices = await fetchStrategyTokenPrices() + const prismaClient = getPrismaClient() + const strategies = await prismaClient.strategies.findMany() + const tokenPrices = await fetchTokenPrices() const strategyPriceMap = new Map() - for (const [_, tokenPrice] of Object.entries(ethPrices)) { - if (tokenPrice) { - strategyPriceMap.set(tokenPrice.strategyAddress.toLowerCase(), tokenPrice.eth) - } + for (const strategy of strategies) { + const foundTokenPrice = tokenPrices.find( + (tp) => tp.address.toLowerCase() === strategy.underlyingToken.toLowerCase() + ) + + strategyPriceMap.set(strategy.address.toLowerCase(), foundTokenPrice?.ethPrice || 0) } strategyPriceMap.set('0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0', 1) diff --git a/packages/api/src/routes/operators/operatorController.ts b/packages/api/src/routes/operators/operatorController.ts index 49673078..5d76443f 100644 --- a/packages/api/src/routes/operators/operatorController.ts +++ b/packages/api/src/routes/operators/operatorController.ts @@ -1,5 +1,4 @@ import type { Request, Response } from 'express' -import { type EigenStrategiesContractAddress, getEigenContracts } from '../../data/address' import { PaginationQuerySchema } from '../../schema/zod/schemas/paginationQuery' import { EthereumAddressSchema } from '../../schema/zod/schemas/base/ethereumAddress' import { WithTvlQuerySchema } from '../../schema/zod/schemas/withTvlQuery' @@ -8,17 +7,15 @@ import { SortByQuerySchema } from '../../schema/zod/schemas/sortByQuery' import { SearchByTextQuerySchema } from '../../schema/zod/schemas/searchByTextQuery' import { WithRewardsQuerySchema } from '../../schema/zod/schemas/withRewardsQuery' import { handleAndReturnErrorResponse } from '../../schema/errors' -import { fetchRewardTokenPrices, fetchStrategyTokenPrices } from '../../utils/tokenPrices' import { getStrategiesWithShareUnderlying, sharesToTVL, - sharesToTVLEth -} from '../strategies/strategiesController' -import { withOperatorShares } from '../avs/avsController' -import { getNetwork } from '../../viem/viemClient' -import { holesky } from 'viem/chains' + sharesToTVLStrategies +} from '../../utils/strategyShares' +import { withOperatorShares } from '../../utils/operatorShares' import Prisma from '@prisma/client' import prisma from '../../utils/prismaClient' +import { fetchTokenPrices } from '../../utils/tokenPrices' /** * Function for route /operators @@ -90,7 +87,6 @@ export async function getAllOperators(req: Request, res: Response) { } }) - const strategyTokenPrices = withTvl ? await fetchStrategyTokenPrices() : {} const strategiesWithSharesUnderlying = withTvl ? await getStrategiesWithShareUnderlying() : [] const operators = operatorRecords.map((operator) => ({ @@ -98,9 +94,7 @@ export async function getAllOperators(req: Request, res: Response) { avsRegistrations: operator.avs, totalStakers: operator.totalStakers, totalAvs: operator.totalAvs, - tvl: withTvl - ? sharesToTVL(operator.shares, strategiesWithSharesUnderlying, strategyTokenPrices) - : undefined, + tvl: withTvl ? sharesToTVL(operator.shares, strategiesWithSharesUnderlying) : undefined, metadataUrl: undefined, isMetadataSynced: undefined, avs: undefined, @@ -212,7 +206,6 @@ export async function getOperator(req: Request, res: Response) { ...(withAvsData && registration.avs ? registration.avs : {}) })) - const strategyTokenPrices = withTvl ? await fetchStrategyTokenPrices() : {} const strategiesWithSharesUnderlying = withTvl ? await getStrategiesWithShareUnderlying() : [] res.send({ @@ -220,9 +213,7 @@ export async function getOperator(req: Request, res: Response) { avsRegistrations, totalStakers: operator.totalStakers, totalAvs: operator.totalAvs, - tvl: withTvl - ? sharesToTVL(operator.shares, strategiesWithSharesUnderlying, strategyTokenPrices) - : undefined, + tvl: withTvl ? sharesToTVL(operator.shares, strategiesWithSharesUnderlying) : undefined, rewards: withRewards ? await calculateOperatorApy(operator) : undefined, stakers: undefined, metadataUrl: undefined, @@ -471,11 +462,7 @@ async function calculateOperatorApy(operator: any) { let operatorEarningsEth = new Prisma.Prisma.Decimal(0) - const strategyTokenPrices = await fetchStrategyTokenPrices() - const rewardTokenPrices = await fetchRewardTokenPrices() - const eigenContracts = getEigenContracts() - const tokenToStrategyMap = tokenToStrategyAddressMap(eigenContracts.Strategies) - + const tokenPrices = await fetchTokenPrices() const strategiesWithSharesUnderlying = await getStrategiesWithShareUnderlying() // Calc aggregate APY for each AVS basis the opted-in strategies @@ -488,11 +475,7 @@ async function calculateOperatorApy(operator: any) { ) // Fetch the AVS tvl for each strategy - const tvlStrategiesEth = sharesToTVLEth( - shares, - strategiesWithSharesUnderlying, - strategyTokenPrices - ) + const tvlStrategiesEth = sharesToTVLStrategies(shares, strategiesWithSharesUnderlying) // Iterate through each strategy and calculate all its rewards for (const strategyAddress of optedStrategyAddresses) { @@ -510,30 +493,14 @@ async function calculateOperatorApy(operator: any) { for (const submission of relevantSubmissions) { let rewardIncrementEth = new Prisma.Prisma.Decimal(0) const rewardTokenAddress = submission.token.toLowerCase() - const tokenStrategyAddress = tokenToStrategyMap.get(rewardTokenAddress) - // Normalize reward amount to its ETH price - if (tokenStrategyAddress) { - const tokenPrice = Object.values(strategyTokenPrices).find( - (tp) => tp.strategyAddress.toLowerCase() === tokenStrategyAddress - ) - rewardIncrementEth = submission.amount.mul( - new Prisma.Prisma.Decimal(tokenPrice?.eth ?? 0) + if (rewardTokenAddress) { + const tokenPrice = tokenPrices.find( + (tp) => tp.address.toLowerCase() === rewardTokenAddress ) - } else { - // Check if it is a reward token which isn't a strategy on EL - for (const [, price] of Object.entries(rewardTokenPrices)) { - if (price && price.tokenAddress.toLowerCase() === rewardTokenAddress) { - rewardIncrementEth = submission.amount.mul( - new Prisma.Prisma.Decimal(price.eth ?? 0) - ) - } else { - // Check for special tokens - rewardIncrementEth = isSpecialToken(rewardTokenAddress) - ? submission.amount - : new Prisma.Prisma.Decimal(0) - } - } + rewardIncrementEth = submission.amount + .mul(new Prisma.Prisma.Decimal(tokenPrice?.ethPrice ?? 0)) + .div(new Prisma.Prisma.Decimal(10).pow(tokenPrice?.decimals ?? 18)) // No decimals } // Multiply reward amount in ETH by the strategy weight @@ -542,18 +509,20 @@ async function calculateOperatorApy(operator: any) { .div(new Prisma.Prisma.Decimal(10).pow(18)) // Operator takes 10% in commission - const operatorFeesEth = rewardIncrementEth.mul(10).div(100) - operatorEarningsEth = operatorEarningsEth.add(operatorFeesEth) + const operatorFeesEth = rewardIncrementEth.mul(10).div(100) // No decimals + + operatorEarningsEth = operatorEarningsEth.add( + operatorFeesEth.mul(new Prisma.Prisma.Decimal(10).pow(18)) + ) // 18 decimals - totalRewardsEth = totalRewardsEth.add(rewardIncrementEth).sub(operatorFeesEth) + totalRewardsEth = totalRewardsEth.add(rewardIncrementEth).sub(operatorFeesEth) // No decimals totalDuration += submission.duration } if (totalDuration === 0) continue // Annualize the reward basis its duration to find yearly APY - const rewardRate = - totalRewardsEth.div(new Prisma.Prisma.Decimal(10).pow(18)).toNumber() / strategyTvl + const rewardRate = totalRewardsEth.toNumber() / strategyTvl // No decimals const annualizedRate = rewardRate * ((365 * 24 * 60 * 60) / totalDuration) const apy = annualizedRate * 100 aggregateApy += apy @@ -586,42 +555,3 @@ async function calculateOperatorApy(operator: any) { return response } catch {} } - -/** - * Return a map of strategy addresses <> token addresses - * - * @param strategies - * @returns - */ -export function tokenToStrategyAddressMap( - strategies: EigenStrategiesContractAddress -): Map { - const map = new Map() - for (const [key, value] of Object.entries(strategies)) { - if (key !== 'Eigen' && value?.tokenContract && value?.strategyContract) { - map.set(value.tokenContract.toLowerCase(), value.strategyContract.toLowerCase()) - } - } - return map -} - -/** - * Returns whether a given token address belongs to a list of special tokens - * - * @param tokenAddress - * @returns - */ -export function isSpecialToken(tokenAddress: string): boolean { - const specialTokens = - getNetwork() === holesky - ? [ - '0x6Cc9397c3B38739daCbfaA68EaD5F5D77Ba5F455', // WETH - '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' - ] - : [ - '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // WETH - '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' - ] - - return specialTokens.includes(tokenAddress.toLowerCase()) -} diff --git a/packages/api/src/routes/stakers/stakerController.ts b/packages/api/src/routes/stakers/stakerController.ts index b8afdaf0..8896c2f5 100644 --- a/packages/api/src/routes/stakers/stakerController.ts +++ b/packages/api/src/routes/stakers/stakerController.ts @@ -4,8 +4,7 @@ import { handleAndReturnErrorResponse } from '../../schema/errors' import { PaginationQuerySchema } from '../../schema/zod/schemas/paginationQuery' import { WithTvlQuerySchema } from '../../schema/zod/schemas/withTvlQuery' import { getViemClient } from '../../viem/viemClient' -import { fetchStrategyTokenPrices } from '../../utils/tokenPrices' -import { getStrategiesWithShareUnderlying, sharesToTVL } from '../strategies/strategiesController' +import { getStrategiesWithShareUnderlying, sharesToTVL } from '../../utils/strategyShares' import { EthereumAddressSchema } from '../../schema/zod/schemas/base/ethereumAddress' import { UpdatedSinceQuerySchema } from '../../schema/zod/schemas/updatedSinceQuery' @@ -43,14 +42,11 @@ export async function getAllStakers(req: Request, res: Response) { } }) - const strategyTokenPrices = withTvl ? await fetchStrategyTokenPrices() : {} const strategiesWithSharesUnderlying = withTvl ? await getStrategiesWithShareUnderlying() : [] const stakers = stakersRecords.map((staker) => ({ ...staker, - tvl: withTvl - ? sharesToTVL(staker.shares, strategiesWithSharesUnderlying, strategyTokenPrices) - : undefined + tvl: withTvl ? sharesToTVL(staker.shares, strategiesWithSharesUnderlying) : undefined })) res.send({ @@ -98,14 +94,11 @@ export async function getStaker(req: Request, res: Response) { } }) - const strategyTokenPrices = withTvl ? await fetchStrategyTokenPrices() : {} const strategiesWithSharesUnderlying = withTvl ? await getStrategiesWithShareUnderlying() : [] res.send({ ...staker, - tvl: withTvl - ? sharesToTVL(staker.shares, strategiesWithSharesUnderlying, strategyTokenPrices) - : undefined + tvl: withTvl ? sharesToTVL(staker.shares, strategiesWithSharesUnderlying) : undefined }) } catch (error) { handleAndReturnErrorResponse(req, res, error) diff --git a/packages/api/src/routes/strategies/strategiesController.ts b/packages/api/src/routes/strategies/strategiesController.ts index 95f65a1c..c857ce25 100644 --- a/packages/api/src/routes/strategies/strategiesController.ts +++ b/packages/api/src/routes/strategies/strategiesController.ts @@ -1,12 +1,7 @@ import type { Request, Response } from 'express' -import type { TokenPrices } from '../../utils/tokenPrices' -import { type EigenStrategiesContractAddress, getEigenContracts } from '../../data/address' import { formatEther } from 'viem' import { eigenLayerMainnetStrategyContracts } from '../../data/address/eigenMainnetContracts' import { getViemClient } from '../../viem/viemClient' -import { serviceManagerUIAbi } from '../../data/abi/serviceManagerUIAbi' -import { getPrismaClient } from '../../utils/prismaClient' -import Prisma from '@prisma/client' // ABI path for dynamic imports const abiPath = { @@ -145,178 +140,3 @@ export async function getTotalTvl(req: Request, res: Response) { res.status(500).send('An error occurred while fetching data.') } } - -export async function getStrategiesWithShareUnderlying(): Promise< - { - strategyAddress: string - sharesToUnderlying: number - }[] -> { - const prismaClient = getPrismaClient() - const strategies = await prismaClient.strategies.findMany() - - return strategies.map((s) => ({ - strategyAddress: s.address, - sharesToUnderlying: BigInt(s.sharesToUnderlying) as unknown as number - })) -} - -export function sharesToTVL( - shares: { - strategyAddress: string - shares: string - }[], - strategiesWithSharesUnderlying: { - strategyAddress: string - sharesToUnderlying: number - }[], - strategyTokenPrices: TokenPrices -) { - const beaconAddress = '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' - - const beaconStrategy = shares.find((s) => s.strategyAddress.toLowerCase() === beaconAddress) - const restakingStrategies = shares.filter( - (s) => s.strategyAddress.toLowerCase() !== beaconAddress - ) - - const tvlBeaconChain = beaconStrategy ? Number(beaconStrategy.shares) / 1e18 : 0 - - const strategyKeys = Object.keys(getEigenContracts().Strategies) - const strategies = Object.values(getEigenContracts().Strategies) - - let tvlRestaking = 0 - const tvlStrategies: Map = new Map( - strategyKeys.map((sk) => [sk as keyof EigenStrategiesContractAddress, 0]) - ) - const tvlStrategiesEth: Map = new Map( - strategyKeys.map((sk) => [sk as keyof EigenStrategiesContractAddress, 0]) - ) - - restakingStrategies.map((s) => { - const foundStrategyIndex = strategies.findIndex( - (si) => si.strategyContract.toLowerCase() === s.strategyAddress.toLowerCase() - ) - - const strategyTokenPrice = Object.values(strategyTokenPrices).find( - (stp) => stp.strategyAddress.toLowerCase() === s.strategyAddress.toLowerCase() - ) - const sharesUnderlying = strategiesWithSharesUnderlying.find( - (su) => su.strategyAddress.toLowerCase() === s.strategyAddress.toLowerCase() - ) - - if (foundStrategyIndex !== -1 && sharesUnderlying) { - const strategyShares = - Number((BigInt(s.shares) * BigInt(sharesUnderlying.sharesToUnderlying)) / BigInt(1e18)) / - 1e18 - - tvlStrategies.set( - strategyKeys[foundStrategyIndex] as keyof EigenStrategiesContractAddress, - strategyShares - ) - - if (strategyTokenPrice) { - const strategyTvl = strategyShares * strategyTokenPrice.eth - - tvlStrategiesEth.set( - strategyKeys[foundStrategyIndex] as keyof EigenStrategiesContractAddress, - strategyTvl - ) - - tvlRestaking = tvlRestaking + strategyTvl - } - } - }) - - return { - tvl: tvlBeaconChain + tvlRestaking, - tvlBeaconChain, - tvlWETH: tvlStrategies.has('WETH') ? tvlStrategies.get('WETH') : 0, - tvlRestaking, - tvlStrategies: Object.fromEntries(tvlStrategies.entries()), - tvlStrategiesEth: Object.fromEntries(tvlStrategiesEth.entries()) - } -} - -/** - * Return the Tvl in Eth of a given set of shares across strategies - * - * @param shares - * @param strategiesWithSharesUnderlying - * @param strategyTokenPrices - * @returns - */ -export function sharesToTVLEth( - shares: { - strategyAddress: string - shares: string - }[], - strategiesWithSharesUnderlying: { - strategyAddress: string - sharesToUnderlying: number - }[], - strategyTokenPrices: TokenPrices -): { [strategyAddress: string]: number } { - const beaconAddress = '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' - - const beaconStrategy = shares.find((s) => s.strategyAddress.toLowerCase() === beaconAddress) - - const tvlBeaconChain = beaconStrategy ? Number(beaconStrategy.shares) / 1e18 : 0 - - const strategies = getEigenContracts().Strategies - const addressToKey = Object.entries(strategies).reduce((acc, [key, value]) => { - acc[value.strategyContract.toLowerCase()] = key - return acc - }, {} as Record) - - const tvlStrategiesEth: { [strategyAddress: string]: number } = { - [beaconAddress]: tvlBeaconChain - } - - for (const share of shares) { - const strategyAddress = share.strategyAddress.toLowerCase() - const isBeaconStrategy = strategyAddress.toLowerCase() === beaconAddress - - const sharesUnderlying = strategiesWithSharesUnderlying.find( - (su) => su.strategyAddress.toLowerCase() === strategyAddress - ) - - const strategyTokenPrice = isBeaconStrategy - ? { eth: 1 } - : Object.values(strategyTokenPrices).find( - (stp) => stp.strategyAddress.toLowerCase() === strategyAddress - ) - - if (sharesUnderlying && strategyTokenPrice) { - const strategyShares = - new Prisma.Prisma.Decimal(share.shares) - .mul(new Prisma.Prisma.Decimal(sharesUnderlying.sharesToUnderlying.toString())) - .div(new Prisma.Prisma.Decimal(10).pow(18)) - .toNumber() / 1e18 - - const strategyTvl = strategyShares * strategyTokenPrice.eth - - const strategyKey = addressToKey[strategyAddress] - if (strategyKey) { - tvlStrategiesEth[strategyAddress] = (tvlStrategiesEth[strategyAddress] || 0) + strategyTvl - } - } - } - - return tvlStrategiesEth -} - -export async function getRestakeableStrategies(avsAddress: string): Promise { - try { - const viemClient = getViemClient() - - const strategies = (await viemClient.readContract({ - address: avsAddress as `0x${string}`, - abi: serviceManagerUIAbi, - functionName: 'getRestakeableStrategies' - })) as string[] - - return strategies.map((s) => s.toLowerCase()) - } catch (error) {} - - return [] -} diff --git a/packages/api/src/utils/operatorShares.ts b/packages/api/src/utils/operatorShares.ts new file mode 100644 index 00000000..d2012d0e --- /dev/null +++ b/packages/api/src/utils/operatorShares.ts @@ -0,0 +1,29 @@ +import { holesky } from 'viem/chains' +import { IMap } from '../schema/generic' +import { getNetwork } from '../viem/viemClient' + +export function withOperatorShares(avsOperators) { + const sharesMap: IMap = new Map() + + avsOperators.map((avsOperator) => { + const shares = avsOperator.operator.shares.filter( + (s) => avsOperator.restakedStrategies.indexOf(s.strategyAddress.toLowerCase()) !== -1 + ) + + shares.map((s) => { + if (!sharesMap.has(s.strategyAddress)) { + sharesMap.set(s.strategyAddress, '0') + } + + sharesMap.set( + s.strategyAddress, + (BigInt(sharesMap.get(s.strategyAddress)) + BigInt(s.shares)).toString() + ) + }) + }) + + return Array.from(sharesMap, ([strategyAddress, shares]) => ({ + strategyAddress, + shares + })) +} diff --git a/packages/api/src/utils/strategyShares.ts b/packages/api/src/utils/strategyShares.ts new file mode 100644 index 00000000..e87fc31e --- /dev/null +++ b/packages/api/src/utils/strategyShares.ts @@ -0,0 +1,159 @@ +import prisma from '@prisma/client' +import { getPrismaClient } from './prismaClient' +import { fetchTokenPrices } from './tokenPrices' +import { getViemClient } from '../viem/viemClient' +import { serviceManagerUIAbi } from '../data/abi/serviceManagerUIAbi' + +export interface StrategyWithShareUnderlying { + symbol: string + strategyAddress: string + tokenAddress: string + sharesToUnderlying: number + ethPrice: number +} + +/** + * Get the strategies with their shares underlying and their token prices + * + * @returns + */ +export async function getStrategiesWithShareUnderlying(): Promise { + const prismaClient = getPrismaClient() + const strategies = await prismaClient.strategies.findMany() + const tokenPrices = await fetchTokenPrices() + + return strategies.map((s) => { + const foundTokenPrice = tokenPrices.find( + (tp) => tp.address.toLowerCase() === s.underlyingToken.toLowerCase() + ) + + return { + symbol: s.symbol, + strategyAddress: s.address, + tokenAddress: s.underlyingToken, + sharesToUnderlying: BigInt(s.sharesToUnderlying) as unknown as number, + ethPrice: foundTokenPrice?.ethPrice || 0 + } + }) +} + +/** + * Return the Tvl of a given set of shares across strategies + * + * @param shares + * @param strategiesWithSharesUnderlying + * @returns + */ +export function sharesToTVL( + shares: { + strategyAddress: string + shares: string + }[], + strategiesWithSharesUnderlying: StrategyWithShareUnderlying[] +) { + const beaconAddress = '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' + + const beaconStrategy = shares.find((s) => s.strategyAddress.toLowerCase() === beaconAddress) + const restakingStrategies = shares.filter( + (s) => s.strategyAddress.toLowerCase() !== beaconAddress + ) + + const tvlBeaconChain = beaconStrategy ? Number(beaconStrategy.shares) / 1e18 : 0 + + let tvlRestaking = 0 + const tvlStrategies: Map = new Map() + const tvlStrategiesEth: Map = new Map() + + restakingStrategies.map((s) => { + const sharesUnderlying = strategiesWithSharesUnderlying.find( + (su) => su.strategyAddress.toLowerCase() === s.strategyAddress.toLowerCase() + ) + + if (sharesUnderlying) { + const strategyShares = + Number((BigInt(s.shares) * BigInt(sharesUnderlying.sharesToUnderlying)) / BigInt(1e18)) / + 1e18 + + tvlStrategies.set(sharesUnderlying.symbol, strategyShares) + + // Add the TVL in ETH, if 0, it will not be added to the total + if (sharesUnderlying.ethPrice) { + const strategyTvl = strategyShares * sharesUnderlying.ethPrice + + tvlStrategiesEth.set(sharesUnderlying.symbol, strategyTvl) + + tvlRestaking = tvlRestaking + strategyTvl + } + } + }) + + return { + tvl: tvlBeaconChain + tvlRestaking, + tvlBeaconChain, + tvlWETH: tvlStrategies.has('WETH') ? tvlStrategies.get('WETH') : 0, + tvlRestaking, + tvlStrategies: Object.fromEntries(tvlStrategies.entries()), + tvlStrategiesEth: Object.fromEntries(tvlStrategiesEth.entries()) + } +} + +/** + * Return the Tvl in Eth of a given set of shares across strategies + * + * @param shares + * @param strategiesWithSharesUnderlying + * @returns + */ +export function sharesToTVLStrategies( + shares: { + strategyAddress: string + shares: string + }[], + strategiesWithSharesUnderlying: StrategyWithShareUnderlying[] +): { [strategyAddress: string]: number } { + const tvlStrategiesEth: { [strategyAddress: string]: number } = {} + + for (const share of shares) { + const strategyAddress = share.strategyAddress.toLowerCase() + + const sharesUnderlying = strategiesWithSharesUnderlying.find( + (su) => su.strategyAddress.toLowerCase() === strategyAddress + ) + + if (sharesUnderlying) { + const strategyShares = + new prisma.Prisma.Decimal(share.shares) + .mul(new prisma.Prisma.Decimal(sharesUnderlying.sharesToUnderlying.toString())) + .div(new prisma.Prisma.Decimal(10).pow(18)) + .toNumber() / 1e18 + + const strategyTokenPrice = sharesUnderlying.ethPrice || 0 + const strategyTvl = strategyShares * strategyTokenPrice + tvlStrategiesEth[strategyAddress] = (tvlStrategiesEth[strategyAddress] || 0) + strategyTvl + } + } + + return tvlStrategiesEth +} + +/** + * Get the restakeable strategies for a given avs + * + * @param avsAddress + * @returns + */ +export async function getRestakeableStrategies(avsAddress: string): Promise { + try { + const viemClient = getViemClient() + + const strategies = (await viemClient.readContract({ + address: avsAddress as `0x${string}`, + abi: serviceManagerUIAbi, + functionName: 'getRestakeableStrategies' + })) as string[] + + return strategies.map((s) => s.toLowerCase()) + } catch (error) {} + + return [] +} diff --git a/packages/api/src/utils/tokenPrices.ts b/packages/api/src/utils/tokenPrices.ts index 639d22dd..11b609b2 100644 --- a/packages/api/src/utils/tokenPrices.ts +++ b/packages/api/src/utils/tokenPrices.ts @@ -1,47 +1,24 @@ -import { - type EigenStrategiesContractAddress, - type RewardsTokensContractAddress, - getEigenContracts -} from '../data/address' - import { cacheStore } from 'route-cache' - -type Tokens = keyof EigenStrategiesContractAddress -type RewardTokens = keyof RewardsTokensContractAddress +import { getPrismaClient } from './prismaClient' type TokenPrice = { + id: number + address: string symbol: string - strategyAddress: string - eth: number - usd?: number -} - -type RewardTokenPrice = { - symbol: string - tokenAddress: string - eth: number - usd?: number + ethPrice: number + decimals: number } -export type TokenPrices = { - [key in Tokens]?: TokenPrice -} - -export type RewardTokenPrices = { - [key in RewardTokens]?: RewardTokenPrice -} +export async function fetchTokenPrices(): Promise { + const prismaClient = getPrismaClient() -export async function fetchStrategyTokenPrices(): Promise { - const tokenPrices: TokenPrices = {} - - const CMC_TOKEN_IDS = [ - 8100, 21535, 27566, 23782, 29035, 24277, 28476, 15060, 23177, 8085, 25147, 24760, 2396 - ] + const tokenPrices: TokenPrice[] = [] + const tokens = await prismaClient.tokens.findMany() + const cmcTokenIds = tokens.map((t) => t.cmcId) const CMC_API = 'https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest' - const keys = Object.keys(getEigenContracts().Strategies) as Tokens[] - const keysStr = CMC_TOKEN_IDS.join(',') + const keysStr = cmcTokenIds.filter((id) => id !== 0).join(',') const cachedValue = await cacheStore.get(`price_${keysStr}`) @@ -58,61 +35,21 @@ export async function fetchStrategyTokenPrices(): Promise { // biome-ignore lint/suspicious/noExplicitAny: const quotes = Object.values(payload.data) as any[] - keys.map((k) => { - const quoteKey = quotes.find((q) => q.symbol.toLowerCase() === k.toLowerCase()) - const price = { - symbol: k, - // biome-ignore lint/style/noNonNullAssertion: - strategyAddress: getEigenContracts().Strategies[k]!.strategyContract, - eth: quoteKey ? payload.data[quoteKey.id].quote.ETH.price : 0 + tokens.map((t) => { + const quote = quotes.find((q) => q.id === t.cmcId) + + if (quote) { + tokenPrices.push({ + id: t.cmcId, + address: t.address, + symbol: quote.symbol, + ethPrice: quote ? quote.quote.ETH.price : 0, + decimals: t.decimals + }) } - - tokenPrices[k] = price }) await cacheStore.set(`price_${keysStr}`, tokenPrices, 120_000) return tokenPrices } - -export async function fetchRewardTokenPrices(): Promise { - const rewardTokenPrices: RewardTokenPrices = {} - - const CMC_TOKEN_IDS = [4039] // ARPA - - const CMC_API = 'https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest' - - const keys = Object.keys(getEigenContracts().Rewards) as RewardTokens[] - const keysStr = CMC_TOKEN_IDS.join(',') - - const cachedValue = await cacheStore.get(`price_${keysStr}`) - - if (cachedValue) { - return cachedValue - } - - const response = await fetch( - `${CMC_API}?id=${keysStr}&convert=eth`, - // biome-ignore lint/style/noNonNullAssertion: - { headers: { 'X-CMC_PRO_API_KEY': process.env.CMC_API_KEY! } } - ) - const payload = await response.json() - // biome-ignore lint/suspicious/noExplicitAny: - const quotes = Object.values(payload.data) as any[] - - keys.map((k) => { - const quoteKey = quotes.find((q) => q.symbol.toLowerCase() === k.toLowerCase()) - const price = { - symbol: k, - // biome-ignore lint/style/noNonNullAssertion: - tokenAddress: getEigenContracts().Rewards[k]!, - eth: quoteKey ? payload.data[quoteKey.id].quote.ETH.price : 0 - } - - rewardTokenPrices[k] = price - }) - - await cacheStore.set(`price_${keysStr}`, rewardTokenPrices, 120_000) - - return rewardTokenPrices -} diff --git a/packages/prisma/migrations/20241011124312_include_tokens_data/migration.sql b/packages/prisma/migrations/20241011124312_include_tokens_data/migration.sql new file mode 100644 index 00000000..ea390281 --- /dev/null +++ b/packages/prisma/migrations/20241011124312_include_tokens_data/migration.sql @@ -0,0 +1,20 @@ +-- AlterTable +ALTER TABLE "Strategies" ADD COLUMN "underlyingToken" TEXT NOT NULL; + +-- CreateTable +CREATE TABLE "Tokens" ( + "address" TEXT NOT NULL, + "symbol" TEXT NOT NULL, + "name" TEXT NOT NULL, + "decimals" INTEGER NOT NULL, + "cmcId" INTEGER NOT NULL, + "createdAtBlock" BIGINT NOT NULL DEFAULT 0, + "updatedAtBlock" BIGINT NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Tokens_pkey" PRIMARY KEY ("address") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Tokens_address_key" ON "Tokens"("address"); diff --git a/packages/prisma/migrations/20241014123250_include_raw_evm_logs_strategy_whitelist/migration.sql b/packages/prisma/migrations/20241014123250_include_raw_evm_logs_strategy_whitelist/migration.sql new file mode 100644 index 00000000..860621fd --- /dev/null +++ b/packages/prisma/migrations/20241014123250_include_raw_evm_logs_strategy_whitelist/migration.sql @@ -0,0 +1,37 @@ +-- CreateTable +CREATE TABLE "EventLogs_StrategyAddedToDepositWhitelist" ( + "address" TEXT NOT NULL, + "transactionHash" TEXT NOT NULL, + "transactionIndex" INTEGER NOT NULL, + "blockNumber" BIGINT NOT NULL, + "blockHash" TEXT NOT NULL, + "blockTime" TIMESTAMP(3) NOT NULL, + "strategy" TEXT NOT NULL, + + CONSTRAINT "EventLogs_StrategyAddedToDepositWhitelist_pkey" PRIMARY KEY ("transactionHash","transactionIndex") +); + +-- CreateTable +CREATE TABLE "EventLogs_StrategyRemovedFromDepositWhitelist" ( + "address" TEXT NOT NULL, + "transactionHash" TEXT NOT NULL, + "transactionIndex" INTEGER NOT NULL, + "blockNumber" BIGINT NOT NULL, + "blockHash" TEXT NOT NULL, + "blockTime" TIMESTAMP(3) NOT NULL, + "strategy" TEXT NOT NULL, + + CONSTRAINT "EventLogs_StrategyRemovedFromDepositWhitelist_pkey" PRIMARY KEY ("transactionHash","transactionIndex") +); + +-- CreateIndex +CREATE INDEX "EventLogs_StrategyAddedToDepositWhitelist_strategy_idx" ON "EventLogs_StrategyAddedToDepositWhitelist"("strategy"); + +-- CreateIndex +CREATE INDEX "EventLogs_StrategyAddedToDepositWhitelist_blockNumber_idx" ON "EventLogs_StrategyAddedToDepositWhitelist"("blockNumber"); + +-- CreateIndex +CREATE INDEX "EventLogs_StrategyRemovedFromDepositWhitelist_strategy_idx" ON "EventLogs_StrategyRemovedFromDepositWhitelist"("strategy"); + +-- CreateIndex +CREATE INDEX "EventLogs_StrategyRemovedFromDepositWhitelist_blockNumber_idx" ON "EventLogs_StrategyRemovedFromDepositWhitelist"("blockNumber"); diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index fcb905de..c767fd8b 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -85,6 +85,7 @@ model Strategies { address String @id @unique symbol String + underlyingToken String sharesToUnderlying String createdAtBlock BigInt @default(0) @@ -93,6 +94,20 @@ model Strategies { updatedAt DateTime @updatedAt } +model Tokens { + address String @id @unique + symbol String + name String + decimals Int + + cmcId Int + + createdAtBlock BigInt @default(0) + updatedAtBlock BigInt @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + model Operator { address String @id @unique @@ -618,6 +633,38 @@ model EventLogs_AVSRewardsSubmission { @@index([blockNumber]) } +model EventLogs_StrategyAddedToDepositWhitelist { + address String + + transactionHash String + transactionIndex Int + blockNumber BigInt + blockHash String + blockTime DateTime + + strategy String + + @@id([transactionHash, transactionIndex]) + @@index([strategy]) + @@index([blockNumber]) +} + +model EventLogs_StrategyRemovedFromDepositWhitelist { + address String + + transactionHash String + transactionIndex Int + blockNumber BigInt + blockHash String + blockTime DateTime + + strategy String + + @@id([transactionHash, transactionIndex]) + @@index([strategy]) + @@index([blockNumber]) +} + model EthPricesDaily { id Int @id @default(autoincrement()) diff --git a/packages/seeder/src/events/seedLogStrategyWhitelist.ts b/packages/seeder/src/events/seedLogStrategyWhitelist.ts new file mode 100644 index 00000000..c8d8c36c --- /dev/null +++ b/packages/seeder/src/events/seedLogStrategyWhitelist.ts @@ -0,0 +1,106 @@ +import prisma from '@prisma/client' +import { parseAbiItem } from 'viem' +import { getEigenContracts } from '../data/address' +import { getPrismaClient } from '../utils/prismaClient' +import { + bulkUpdateDbTransactions, + fetchLastSyncBlock, + getBlockDataFromDb, + loopThroughBlocks +} from '../utils/seeder' +import { getViemClient } from '../utils/viemClient' + +const blockSyncKeyLogs = 'lastSyncedBlock_logs_strategyWhitelist' + +/** + * Utility function to seed event logs + * + * @param fromBlock + * @param toBlock + */ +export async function seedLogStrategyWhitelist(toBlock?: bigint, fromBlock?: bigint) { + const viemClient = getViemClient() + const prismaClient = getPrismaClient() + + const firstBlock = fromBlock ? fromBlock : await fetchLastSyncBlock(blockSyncKeyLogs) + const lastBlock = toBlock ? toBlock : await viemClient.getBlockNumber() + + // Loop through evm logs + await loopThroughBlocks(firstBlock, lastBlock, async (fromBlock, toBlock) => { + const blockData = await getBlockDataFromDb(fromBlock, toBlock) + + try { + const dbTransactions: any[] = [] + + const logsStrategyWhitelist: prisma.EventLogs_StrategyAddedToDepositWhitelist[] = [] + const logsStrategyWhitelistRemoved: prisma.EventLogs_StrategyRemovedFromDepositWhitelist[] = + [] + + const logs = await viemClient.getLogs({ + address: getEigenContracts().StrategyManager, + events: [ + parseAbiItem('event StrategyAddedToDepositWhitelist(address strategy)'), + parseAbiItem('event StrategyRemovedFromDepositWhitelist(address strategy)') + ], + fromBlock, + toBlock + }) + + for (const l in logs) { + const log = logs[l] + + if (log.eventName === 'StrategyAddedToDepositWhitelist') { + logsStrategyWhitelist.push({ + address: log.address, + transactionHash: log.transactionHash, + transactionIndex: log.logIndex, + blockNumber: BigInt(log.blockNumber), + blockHash: log.blockHash, + blockTime: blockData.get(log.blockNumber) || new Date(0), + strategy: String(log.args.strategy) + }) + } else if (log.eventName === 'StrategyRemovedFromDepositWhitelist') { + logsStrategyWhitelistRemoved.push({ + address: log.address, + transactionHash: log.transactionHash, + transactionIndex: log.logIndex, + blockNumber: BigInt(log.blockNumber), + blockHash: log.blockHash, + blockTime: blockData.get(log.blockNumber) || new Date(0), + strategy: String(log.args.strategy) + }) + } + } + + dbTransactions.push( + prismaClient.eventLogs_StrategyAddedToDepositWhitelist.createMany({ + data: logsStrategyWhitelist, + skipDuplicates: true + }) + ) + + dbTransactions.push( + prismaClient.eventLogs_StrategyRemovedFromDepositWhitelist.createMany({ + data: logsStrategyWhitelistRemoved, + skipDuplicates: true + }) + ) + + // Store last synced block + dbTransactions.push( + prismaClient.settings.upsert({ + where: { key: blockSyncKeyLogs }, + update: { value: Number(toBlock) }, + create: { key: blockSyncKeyLogs, value: Number(toBlock) } + }) + ) + + const seedLength = logsStrategyWhitelist.length + logsStrategyWhitelistRemoved.length + + await bulkUpdateDbTransactions( + dbTransactions, + `[Logs] Strategy Whitelist from: ${fromBlock} to: ${toBlock} size: ${seedLength}` + ) + } catch (error) {} + }) +} diff --git a/packages/seeder/src/index.ts b/packages/seeder/src/index.ts index 1ca8d190..d1f4213e 100644 --- a/packages/seeder/src/index.ts +++ b/packages/seeder/src/index.ts @@ -37,6 +37,7 @@ import { seedAvsStrategyRewards } from './seedAvsStrategyRewards' import { seedLogsAVSRewardsSubmission } from './events/seedLogsRewardsSubmissions' import { monitorAvsApy } from './monitors/avsApy' import { monitorOperatorApy } from './monitors/operatorApy' +import { seedLogStrategyWhitelist } from './events/seedLogStrategyWhitelist' console.log('Initializing Seeder ...') @@ -81,7 +82,8 @@ async function seedEigenData() { seedLogsWithdrawalCompleted(targetBlock), seedLogsDeposit(targetBlock), seedLogsPodSharesUpdated(targetBlock), - seedLogsAVSRewardsSubmission(targetBlock) + seedLogsAVSRewardsSubmission(targetBlock), + seedLogStrategyWhitelist(targetBlock) ]) await Promise.all([ diff --git a/packages/seeder/src/metrics/seedMetricsTvl.ts b/packages/seeder/src/metrics/seedMetricsTvl.ts index 9c6530ce..e9476407 100644 --- a/packages/seeder/src/metrics/seedMetricsTvl.ts +++ b/packages/seeder/src/metrics/seedMetricsTvl.ts @@ -9,7 +9,10 @@ import { } from '../utils/seeder' import { getViemClient } from '../utils/viemClient' import { strategyAbi } from '../data/abi/strategy' -import { getEigenContracts } from '../data/address' +import { + getStrategiesWithShareUnderlying, + StrategyWithShareUnderlying +} from '../utils/strategyShares' const blockSyncKey = 'lastSyncedTimestamp_metrics_tvl' const BATCH_DAYS = 30 @@ -82,7 +85,7 @@ export async function seedMetricsTvl(type: 'full' | 'incremental' = 'incremental async function processLogsInBatches( startDate: Date, endDate: Date, - sharesToUnderlyingMap: Map, + sharesToUnderlyingList: StrategyWithShareUnderlying[], lastStrategyMetrics: ILastStrategyMetrics ) { let metrics: ILastStrategyMetric[] = [] @@ -106,7 +109,7 @@ async function processLogsInBatches( fromDate, toDate, blockNumbers, - sharesToUnderlyingMap, + sharesToUnderlyingList, lastStrategyMetrics ) @@ -129,7 +132,7 @@ async function loopTick( fromDate: Date, toDate: Date, blockNumbers: { number: bigint; timestamp: Date }[], - sharesToUnderlyingMap: Map, + sharesToUnderlyingList: StrategyWithShareUnderlying[], lastStrategyMetrics: ILastStrategyMetrics ): Promise { const viemClient = getViemClient() @@ -142,17 +145,14 @@ async function loopTick( const currentBlockNumber = blockNumbersInRange[blockNumbersInRange.length - 1] // Startegies - const strategyKeys = Object.keys(getEigenContracts().Strategies) - const strategyAddresses = strategyKeys.map((s) => - getEigenContracts().Strategies[s].strategyContract.toLowerCase() - ) + const strategyAddresses = sharesToUnderlyingList.map((s) => s.strategyAddress) // Total shares const results = await Promise.allSettled( strategyAddresses.map(async (sa) => ({ strategyAddress: sa, totalShares: await viemClient.readContract({ - address: sa, + address: sa as `0x${string}`, abi: strategyAbi, functionName: 'totalShares', blockNumber: currentBlockNumber.number @@ -174,13 +174,17 @@ async function loopTick( r.status === 'fulfilled' && r.value.strategyAddress.toLowerCase() === strategyAddress.toLowerCase() ) - if (foundStrategyIndex === -1 || results[foundStrategyIndex].status !== 'fulfilled') continue - const shares = results[foundStrategyIndex].value.totalShares as bigint - const sharesToUnderlying = sharesToUnderlyingMap.get(strategyAddress) + const result = results[foundStrategyIndex] + if (foundStrategyIndex === -1 || result.status !== 'fulfilled') continue + + const shares = result.value.totalShares as bigint + const sharesToUnderlying = sharesToUnderlyingList.find( + (s) => s.strategyAddress.toLowerCase() === strategyAddress.toLowerCase() + ) if (!sharesToUnderlying) continue - const tvl = Number(shares * sharesToUnderlying) / 1e36 + const tvl = (Number(shares) * Number(sharesToUnderlying.sharesToUnderlying)) / 1e36 if (tvl !== Number(lastMetric.tvl)) { const changeTvl = tvl - Number(lastMetric.tvl) @@ -269,13 +273,3 @@ async function getLatestMetricsPerStrategy(): Promise { return new Map() } - -export async function getStrategiesWithShareUnderlying(): Promise> { - const prismaClient = getPrismaClient() - - const sharesToUnderlyingList = await prismaClient.strategies.findMany({ - select: { sharesToUnderlying: true, address: true } - }) - - return new Map(sharesToUnderlyingList.map((s) => [s.address, BigInt(s.sharesToUnderlying)])) -} diff --git a/packages/seeder/src/monitors/avsApy.ts b/packages/seeder/src/monitors/avsApy.ts index 4f460f35..3fd61419 100644 --- a/packages/seeder/src/monitors/avsApy.ts +++ b/packages/seeder/src/monitors/avsApy.ts @@ -1,15 +1,9 @@ -import prisma from '@prisma/client' -import { type IMap, bulkUpdateDbTransactions } from '../utils/seeder' -import { type EigenStrategiesContractAddress, getEigenContracts } from '../data/address' -import { - type TokenPrices, - fetchRewardTokenPrices, - fetchStrategyTokenPrices -} from '../utils/tokenPrices' +import Prisma from '@prisma/client' +import { bulkUpdateDbTransactions } from '../utils/seeder' import { getPrismaClient } from '../utils/prismaClient' -import { getStrategiesWithShareUnderlying } from '../metrics/seedMetricsTvl' -import { getNetwork } from '../utils/viemClient' -import { holesky } from 'viem/chains' +import { sharesToTVLStrategies, getStrategiesWithShareUnderlying } from '../utils/strategyShares' +import { withOperatorShares } from '../utils/operatorShares' +import { fetchTokenPrices } from '../utils/tokenPrices' export async function monitorAvsApy() { const prismaClient = getPrismaClient() @@ -18,17 +12,13 @@ export async function monitorAvsApy() { const dbTransactions: any[] = [] const data: { address: string - apy: prisma.Prisma.Decimal + apy: Prisma.Prisma.Decimal }[] = [] let skip = 0 const take = 32 - const strategyTokenPrices = await fetchStrategyTokenPrices() - const rewardTokenPrices = await fetchRewardTokenPrices() - const eigenContracts = getEigenContracts() - const tokenToStrategyMap = tokenToStrategyAddressMap(eigenContracts.Strategies) - + const tokenPrices = await fetchTokenPrices() const strategiesWithSharesUnderlying = await getStrategiesWithShareUnderlying() while (true) { @@ -62,22 +52,18 @@ export async function monitorAvsApy() { (s) => avs.restakeableStrategies.indexOf(s.strategyAddress.toLowerCase()) !== -1 ) - let apy = new prisma.Prisma.Decimal(0) + let apy = new Prisma.Prisma.Decimal(0) if (avs.rewardSubmissions.length > 0) { // Fetch the AVS tvl for each strategy - const tvlStrategiesEth = sharesToTVLEth( - shares, - strategiesWithSharesUnderlying, - strategyTokenPrices - ) + const tvlStrategiesEth = sharesToTVLStrategies(shares, strategiesWithSharesUnderlying) // Iterate through each strategy and calculate all its rewards const strategiesApy = avs.restakeableStrategies.map((strategyAddress) => { const strategyTvl = tvlStrategiesEth[strategyAddress.toLowerCase()] || 0 if (strategyTvl === 0) return { strategyAddress, apy: 0 } - let totalRewardsEth = new prisma.Prisma.Decimal(0) + let totalRewardsEth = new Prisma.Prisma.Decimal(0) let totalDuration = 0 // Find all reward submissions attributable to the strategy @@ -88,40 +74,25 @@ export async function monitorAvsApy() { // Calculate each reward amount for the strategy for (const submission of relevantSubmissions) { - let rewardIncrementEth = new prisma.Prisma.Decimal(0) + let rewardIncrementEth = new Prisma.Prisma.Decimal(0) const rewardTokenAddress = submission.token.toLowerCase() - const tokenStrategyAddress = tokenToStrategyMap.get(rewardTokenAddress) // Normalize reward amount to its ETH price - if (tokenStrategyAddress) { - const tokenPrice = Object.values(strategyTokenPrices).find( - (tp) => tp.strategyAddress.toLowerCase() === tokenStrategyAddress + if (rewardTokenAddress) { + const tokenPrice = tokenPrices.find( + (tp) => tp.address.toLowerCase() === rewardTokenAddress ) - rewardIncrementEth = submission.amount.mul( - new prisma.Prisma.Decimal(tokenPrice?.eth ?? 0) - ) - } else { - // Check if it is a reward token which isn't a strategy on EL - for (const [, price] of Object.entries(rewardTokenPrices)) { - if (price && price.tokenAddress.toLowerCase() === rewardTokenAddress) { - rewardIncrementEth = submission.amount.mul( - new prisma.Prisma.Decimal(price.eth ?? 0) - ) - } else { - // Check for special tokens - rewardIncrementEth = isSpecialToken(rewardTokenAddress) - ? submission.amount - : new prisma.Prisma.Decimal(0) - } - } + rewardIncrementEth = submission.amount + .mul(new Prisma.Prisma.Decimal(tokenPrice?.ethPrice ?? 0)) + .div(new Prisma.Prisma.Decimal(10).pow(tokenPrice?.decimals ?? 18)) // No decimals } // Multiply reward amount in ETH by the strategy weight rewardIncrementEth = rewardIncrementEth .mul(submission.multiplier) - .div(new prisma.Prisma.Decimal(10).pow(18)) + .div(new Prisma.Prisma.Decimal(10).pow(18)) - totalRewardsEth = totalRewardsEth.add(rewardIncrementEth) + totalRewardsEth = totalRewardsEth.add(rewardIncrementEth) // No decimals totalDuration += submission.duration } @@ -130,8 +101,7 @@ export async function monitorAvsApy() { } // Annualize the reward basis its duration to find yearly APY - const rewardRate = - totalRewardsEth.div(new prisma.Prisma.Decimal(10).pow(18)).toNumber() / strategyTvl + const rewardRate = totalRewardsEth.toNumber() / strategyTvl const annualizedRate = rewardRate * ((365 * 24 * 60 * 60) / totalDuration) const apy = annualizedRate * 100 @@ -139,7 +109,7 @@ export async function monitorAvsApy() { }) // Calculate aggregate APY across strategies - apy = new prisma.Prisma.Decimal( + apy = new Prisma.Prisma.Decimal( strategiesApy.reduce((sum, strategy) => sum + strategy.apy, 0) ) } @@ -167,7 +137,7 @@ export async function monitorAvsApy() { WHERE a2.address = a.address; ` - dbTransactions.push(prismaClient.$executeRaw`${prisma.Prisma.raw(query)}`) + dbTransactions.push(prismaClient.$executeRaw`${Prisma.Prisma.raw(query)}`) } } catch (error) {} } @@ -180,203 +150,3 @@ export async function monitorAvsApy() { ) } } - -// --- Helper methods --- - -function withOperatorShares(avsOperators) { - const sharesMap: IMap = new Map() - - avsOperators.map((avsOperator) => { - const shares = avsOperator.operator.shares.filter( - (s) => avsOperator.restakedStrategies.indexOf(s.strategyAddress.toLowerCase()) !== -1 - ) - - shares.map((s) => { - if (!sharesMap.has(s.strategyAddress)) { - sharesMap.set(s.strategyAddress, '0') - } - - sharesMap.set( - s.strategyAddress, - (BigInt(sharesMap.get(s.strategyAddress)) + BigInt(s.shares)).toString() - ) - }) - }) - - return Array.from(sharesMap, ([strategyAddress, shares]) => ({ - strategyAddress, - shares - })) -} - -export function sharesToTVL( - shares: { - strategyAddress: string - shares: string - }[], - strategiesWithSharesUnderlying: Map, - strategyTokenPrices: TokenPrices -) { - const beaconAddress = '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' - - const beaconStrategy = shares.find((s) => s.strategyAddress.toLowerCase() === beaconAddress) - const restakingStrategies = shares.filter( - (s) => s.strategyAddress.toLowerCase() !== beaconAddress - ) - - const tvlBeaconChain = beaconStrategy ? Number(beaconStrategy.shares) / 1e18 : 0 - - const strategyKeys = Object.keys(getEigenContracts().Strategies) - const strategies = Object.values(getEigenContracts().Strategies) - - let tvlRestaking = 0 - const tvlStrategies: Map = new Map( - strategyKeys.map((sk) => [sk as keyof EigenStrategiesContractAddress, 0]) - ) - const tvlStrategiesEth: Map = new Map( - strategyKeys.map((sk) => [sk as keyof EigenStrategiesContractAddress, 0]) - ) - - restakingStrategies.map((s) => { - const foundStrategyIndex = strategies.findIndex( - (si) => si.strategyContract.toLowerCase() === s.strategyAddress.toLowerCase() - ) - - const strategyTokenPrice = Object.values(strategyTokenPrices).find( - (stp) => stp.strategyAddress.toLowerCase() === s.strategyAddress.toLowerCase() - ) - const sharesUnderlying = strategiesWithSharesUnderlying.get(s.strategyAddress.toLowerCase()) - - if (foundStrategyIndex !== -1 && sharesUnderlying) { - const strategyShares = - Number((BigInt(s.shares) * BigInt(sharesUnderlying)) / BigInt(1e18)) / 1e18 - - tvlStrategies.set( - strategyKeys[foundStrategyIndex] as keyof EigenStrategiesContractAddress, - strategyShares - ) - - if (strategyTokenPrice) { - const strategyTvl = strategyShares * strategyTokenPrice.eth - - tvlStrategiesEth.set( - strategyKeys[foundStrategyIndex] as keyof EigenStrategiesContractAddress, - strategyTvl - ) - - tvlRestaking = tvlRestaking + strategyTvl - } - } - }) - - return { - tvl: tvlBeaconChain + tvlRestaking, - tvlBeaconChain, - tvlWETH: tvlStrategies.has('WETH') ? tvlStrategies.get('WETH') : 0, - tvlRestaking, - tvlStrategies: Object.fromEntries(tvlStrategies.entries()), - tvlStrategiesEth: Object.fromEntries(tvlStrategiesEth.entries()) - } -} - -/** - * Return the Tvl in Eth of a given set of shares across strategies - * - * @param shares - * @param strategiesWithSharesUnderlying - * @param strategyTokenPrices - * @returns - */ -export function sharesToTVLEth( - shares: { - strategyAddress: string - shares: string - }[], - strategiesWithSharesUnderlying: Map, - strategyTokenPrices: TokenPrices -): { [strategyAddress: string]: number } { - const beaconAddress = '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' - - const beaconStrategy = shares.find((s) => s.strategyAddress.toLowerCase() === beaconAddress) - - const tvlBeaconChain = beaconStrategy ? Number(beaconStrategy.shares) / 1e18 : 0 - - const strategies = getEigenContracts().Strategies - const addressToKey = Object.entries(strategies).reduce((acc, [key, value]) => { - acc[value.strategyContract.toLowerCase()] = key - return acc - }, {} as Record) - - const tvlStrategiesEth: { [strategyAddress: string]: number } = { - [beaconAddress]: tvlBeaconChain - } - - for (const share of shares) { - const strategyAddress = share.strategyAddress.toLowerCase() - const isBeaconStrategy = strategyAddress.toLowerCase() === beaconAddress - - const sharesUnderlying = strategiesWithSharesUnderlying.get(strategyAddress) - - const strategyTokenPrice = isBeaconStrategy - ? { eth: 1 } - : Object.values(strategyTokenPrices).find( - (stp) => stp.strategyAddress.toLowerCase() === strategyAddress - ) - - if (sharesUnderlying !== undefined && strategyTokenPrice) { - const strategyShares = - new prisma.Prisma.Decimal(share.shares) - .mul(new prisma.Prisma.Decimal(sharesUnderlying.toString())) - .div(new prisma.Prisma.Decimal(10).pow(18)) - .toNumber() / 1e18 - - const strategyTvl = strategyShares * strategyTokenPrice.eth - - const strategyKey = addressToKey[strategyAddress] - if (strategyKey) { - tvlStrategiesEth[strategyAddress] = (tvlStrategiesEth[strategyAddress] || 0) + strategyTvl - } - } - } - - return tvlStrategiesEth -} - -/** - * Return a map of strategy addresses <> token addresses - * - * @param strategies - * @returns - */ -export function tokenToStrategyAddressMap( - strategies: EigenStrategiesContractAddress -): Map { - const map = new Map() - for (const [key, value] of Object.entries(strategies)) { - if (key !== 'Eigen' && value?.tokenContract && value?.strategyContract) { - map.set(value.tokenContract.toLowerCase(), value.strategyContract.toLowerCase()) - } - } - return map -} - -/** - * Returns whether a given token address belongs to a list of special tokens - * - * @param tokenAddress - * @returns - */ -export function isSpecialToken(tokenAddress: string): boolean { - const specialTokens = - getNetwork() === holesky - ? [ - '0x6Cc9397c3B38739daCbfaA68EaD5F5D77Ba5F455', // WETH - '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' - ] - : [ - '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // WETH - '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' - ] - - return specialTokens.includes(tokenAddress.toLowerCase()) -} diff --git a/packages/seeder/src/monitors/avsMetrics.ts b/packages/seeder/src/monitors/avsMetrics.ts index e9ce1b26..fae5c3d8 100644 --- a/packages/seeder/src/monitors/avsMetrics.ts +++ b/packages/seeder/src/monitors/avsMetrics.ts @@ -1,10 +1,9 @@ import prisma from '@prisma/client' import { createHash } from 'crypto' import { getPrismaClient } from '../utils/prismaClient' -import { bulkUpdateDbTransactions, IMap } from '../utils/seeder' -import { EigenStrategiesContractAddress, getEigenContracts } from '../data/address' -import { fetchStrategyTokenPrices, TokenPrices } from '../utils/tokenPrices' -import { getStrategiesWithShareUnderlying } from '../metrics/seedMetricsTvl' +import { bulkUpdateDbTransactions } from '../utils/seeder' +import { getStrategiesWithShareUnderlying, sharesToTVL } from '../utils/strategyShares' +import { withOperatorShares } from '../utils/operatorShares' export async function monitorAvsMetrics() { const prismaClient = getPrismaClient() @@ -22,7 +21,6 @@ export async function monitorAvsMetrics() { let skip = 0 const take = 1000 - const strategyTokenPrices = await fetchStrategyTokenPrices() const strategiesWithSharesUnderlying = await getStrategiesWithShareUnderlying() while (true) { @@ -79,7 +77,7 @@ export async function monitorAvsMetrics() { avs.totalStakers !== totalStakers || avs.sharesHash !== sharesHash ) { - const tvlObject = sharesToTVL(shares, strategiesWithSharesUnderlying, strategyTokenPrices) + const tvlObject = sharesToTVL(shares, strategiesWithSharesUnderlying) data.push({ address: avs.address, @@ -127,100 +125,3 @@ export async function monitorAvsMetrics() { ) } } - -// Helper methods -function withOperatorShares(avsOperators) { - const sharesMap: IMap = new Map() - - avsOperators.map((avsOperator) => { - const shares = avsOperator.operator.shares.filter( - (s) => avsOperator.restakedStrategies.indexOf(s.strategyAddress.toLowerCase()) !== -1 - ) - - shares.map((s) => { - if (!sharesMap.has(s.strategyAddress)) { - sharesMap.set(s.strategyAddress, '0') - } - - sharesMap.set( - s.strategyAddress, - (BigInt(sharesMap.get(s.strategyAddress)) + BigInt(s.shares)).toString() - ) - }) - }) - - return Array.from(sharesMap, ([strategyAddress, shares]) => ({ - strategyAddress, - shares - })) -} - -export function sharesToTVL( - shares: { - strategyAddress: string - shares: string - }[], - strategiesWithSharesUnderlying: Map, - strategyTokenPrices: TokenPrices -) { - const beaconAddress = '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' - - const beaconStrategy = shares.find((s) => s.strategyAddress.toLowerCase() === beaconAddress) - const restakingStrategies = shares.filter( - (s) => s.strategyAddress.toLowerCase() !== beaconAddress - ) - - const tvlBeaconChain = beaconStrategy ? Number(beaconStrategy.shares) / 1e18 : 0 - - const strategyKeys = Object.keys(getEigenContracts().Strategies) - const strategies = Object.values(getEigenContracts().Strategies) - - let tvlRestaking = 0 - const tvlStrategies: Map = new Map( - strategyKeys.map((sk) => [sk as keyof EigenStrategiesContractAddress, 0]) - ) - const tvlStrategiesEth: Map = new Map( - strategyKeys.map((sk) => [sk as keyof EigenStrategiesContractAddress, 0]) - ) - - restakingStrategies.map((s) => { - const foundStrategyIndex = strategies.findIndex( - (si) => si.strategyContract.toLowerCase() === s.strategyAddress.toLowerCase() - ) - - const strategyTokenPrice = Object.values(strategyTokenPrices).find( - (stp) => stp.strategyAddress.toLowerCase() === s.strategyAddress.toLowerCase() - ) - const sharesUnderlying = strategiesWithSharesUnderlying.get(s.strategyAddress.toLowerCase()) - - if (foundStrategyIndex !== -1 && sharesUnderlying) { - const strategyShares = - Number((BigInt(s.shares) * BigInt(sharesUnderlying)) / BigInt(1e18)) / 1e18 - - tvlStrategies.set( - strategyKeys[foundStrategyIndex] as keyof EigenStrategiesContractAddress, - strategyShares - ) - - if (strategyTokenPrice) { - const strategyTvl = strategyShares * strategyTokenPrice.eth - - tvlStrategiesEth.set( - strategyKeys[foundStrategyIndex] as keyof EigenStrategiesContractAddress, - strategyTvl - ) - - tvlRestaking = tvlRestaking + strategyTvl - } - } - }) - - return { - tvl: tvlBeaconChain + tvlRestaking, - tvlBeaconChain, - tvlWETH: tvlStrategies.has('WETH') ? tvlStrategies.get('WETH') : 0, - tvlRestaking, - tvlStrategies: Object.fromEntries(tvlStrategies.entries()), - tvlStrategiesEth: Object.fromEntries(tvlStrategiesEth.entries()) - } -} diff --git a/packages/seeder/src/monitors/operatorApy.ts b/packages/seeder/src/monitors/operatorApy.ts index f08470d1..5f71d32a 100644 --- a/packages/seeder/src/monitors/operatorApy.ts +++ b/packages/seeder/src/monitors/operatorApy.ts @@ -1,15 +1,9 @@ -import prisma from '@prisma/client' -import { type IMap, bulkUpdateDbTransactions } from '../utils/seeder' -import { type EigenStrategiesContractAddress, getEigenContracts } from '../data/address' -import { - type TokenPrices, - fetchRewardTokenPrices, - fetchStrategyTokenPrices -} from '../utils/tokenPrices' +import { bulkUpdateDbTransactions } from '../utils/seeder' import { getPrismaClient } from '../utils/prismaClient' -import { getStrategiesWithShareUnderlying } from '../metrics/seedMetricsTvl' -import { getNetwork } from '../utils/viemClient' -import { holesky } from 'viem/chains' +import { getStrategiesWithShareUnderlying, sharesToTVLStrategies } from '../utils/strategyShares' +import { withOperatorShares } from '../utils/operatorShares' +import { fetchTokenPrices } from '../utils/tokenPrices' +import Prisma from '@prisma/client' export async function monitorOperatorApy() { const prismaClient = getPrismaClient() @@ -18,17 +12,13 @@ export async function monitorOperatorApy() { const dbTransactions: any[] = [] const data: { address: string - apy: prisma.Prisma.Decimal + apy: Prisma.Prisma.Decimal }[] = [] let skip = 0 const take = 32 - const strategyTokenPrices = await fetchStrategyTokenPrices() - const rewardTokenPrices = await fetchRewardTokenPrices() - const eigenContracts = getEigenContracts() - const tokenToStrategyMap = tokenToStrategyAddressMap(eigenContracts.Strategies) - + const tokenPrices = await fetchTokenPrices() const strategiesWithSharesUnderlying = await getStrategiesWithShareUnderlying() while (true) { @@ -76,7 +66,7 @@ export async function monitorOperatorApy() { // Setup all db transactions for this iteration for (const operator of operatorMetrics) { - let apy = new prisma.Prisma.Decimal(0) + let apy = new Prisma.Prisma.Decimal(0) const avsRewardsMap: Map = new Map() const strategyRewardsMap: Map = new Map() @@ -96,7 +86,7 @@ export async function monitorOperatorApy() { .filter((item) => item.eligibleRewards.length > 0) if (avsWithEligibleRewardSubmissions.length > 0) { - let operatorEarningsEth = new prisma.Prisma.Decimal(0) + let operatorEarningsEth = new Prisma.Prisma.Decimal(0) // Calc aggregate APY for each AVS basis the opted-in strategies for (const avs of avsWithEligibleRewardSubmissions) { @@ -108,18 +98,14 @@ export async function monitorOperatorApy() { ) // Fetch the AVS tvl for each strategy - const tvlStrategiesEth = sharesToTVLEth( - shares, - strategiesWithSharesUnderlying, - strategyTokenPrices - ) + const tvlStrategiesEth = sharesToTVLStrategies(shares, strategiesWithSharesUnderlying) // Iterate through each strategy and calculate all its rewards for (const strategyAddress of optedStrategyAddresses) { const strategyTvl = tvlStrategiesEth[strategyAddress.toLowerCase()] || 0 if (strategyTvl === 0) continue - let totalRewardsEth = new prisma.Prisma.Decimal(0) + let totalRewardsEth = new Prisma.Prisma.Decimal(0) let totalDuration = 0 // Find all reward submissions attributable to the strategy @@ -129,52 +115,39 @@ export async function monitorOperatorApy() { ) for (const submission of relevantSubmissions) { - let rewardIncrementEth = new prisma.Prisma.Decimal(0) + let rewardIncrementEth = new Prisma.Prisma.Decimal(0) const rewardTokenAddress = submission.token.toLowerCase() - const tokenStrategyAddress = tokenToStrategyMap.get(rewardTokenAddress) // Normalize reward amount to its ETH price - if (tokenStrategyAddress) { - const tokenPrice = Object.values(strategyTokenPrices).find( - (tp) => tp.strategyAddress.toLowerCase() === tokenStrategyAddress + if (rewardTokenAddress) { + const tokenPrice = tokenPrices.find( + (tp) => tp.address.toLowerCase() === rewardTokenAddress ) - rewardIncrementEth = submission.amount.mul( - new prisma.Prisma.Decimal(tokenPrice?.eth ?? 0) - ) - } else { - // Check if it is a reward token which isn't a strategy on EL - for (const [, price] of Object.entries(rewardTokenPrices)) { - if (price && price.tokenAddress.toLowerCase() === rewardTokenAddress) { - rewardIncrementEth = submission.amount.mul( - new prisma.Prisma.Decimal(price.eth ?? 0) - ) - } else { - // Check for special tokens - rewardIncrementEth = isSpecialToken(rewardTokenAddress) - ? submission.amount - : new prisma.Prisma.Decimal(0) - } - } + rewardIncrementEth = submission.amount + .mul(new Prisma.Prisma.Decimal(tokenPrice?.ethPrice ?? 0)) + .div(new Prisma.Prisma.Decimal(10).pow(tokenPrice?.decimals ?? 18)) // No decimals } // Multiply reward amount in ETH by the strategy weight rewardIncrementEth = rewardIncrementEth .mul(submission.multiplier) - .div(new prisma.Prisma.Decimal(10).pow(18)) + .div(new Prisma.Prisma.Decimal(10).pow(18)) // Operator takes 10% in commission - const operatorFeesEth = rewardIncrementEth.mul(10).div(100) - operatorEarningsEth = operatorEarningsEth.add(operatorFeesEth) + const operatorFeesEth = rewardIncrementEth.mul(10).div(100) // No decimals + + operatorEarningsEth = operatorEarningsEth.add( + operatorFeesEth.mul(new Prisma.Prisma.Decimal(10).pow(18)) + ) // 18 decimals - totalRewardsEth = totalRewardsEth.add(rewardIncrementEth).sub(operatorFeesEth) + totalRewardsEth = totalRewardsEth.add(rewardIncrementEth).sub(operatorFeesEth) // No decimals totalDuration += submission.duration } if (totalDuration === 0) continue // Annualize the reward basis its duration to find yearly APY - const rewardRate = - totalRewardsEth.div(new prisma.Prisma.Decimal(10).pow(18)).toNumber() / strategyTvl + const rewardRate = totalRewardsEth.toNumber() / strategyTvl // No decimals const annualizedRate = rewardRate * ((365 * 24 * 60 * 60) / totalDuration) const apy = annualizedRate * 100 aggregateApy += apy @@ -193,7 +166,7 @@ export async function monitorOperatorApy() { })) // Calculate aggregates across Avs and strategies - apy = new prisma.Prisma.Decimal(avs.reduce((sum, avs) => sum + avs.apy, 0)) + apy = new Prisma.Prisma.Decimal(avs.reduce((sum, avs) => sum + avs.apy, 0)) } if (operator.apy !== apy) { @@ -220,7 +193,7 @@ export async function monitorOperatorApy() { o2.address = o.address; ` - dbTransactions.push(prismaClient.$executeRaw`${prisma.Prisma.raw(query)}`) + dbTransactions.push(prismaClient.$executeRaw`${Prisma.Prisma.raw(query)}`) } } catch (error) {} } @@ -233,133 +206,3 @@ export async function monitorOperatorApy() { ) } } - -// --- Helper methods --- - -function withOperatorShares(avsOperators) { - const sharesMap: IMap = new Map() - - avsOperators.map((avsOperator) => { - const shares = avsOperator.operator.shares.filter( - (s) => avsOperator.restakedStrategies.indexOf(s.strategyAddress.toLowerCase()) !== -1 - ) - - shares.map((s) => { - if (!sharesMap.has(s.strategyAddress)) { - sharesMap.set(s.strategyAddress, '0') - } - - sharesMap.set( - s.strategyAddress, - (BigInt(sharesMap.get(s.strategyAddress)) + BigInt(s.shares)).toString() - ) - }) - }) - - return Array.from(sharesMap, ([strategyAddress, shares]) => ({ - strategyAddress, - shares - })) -} - -/** - * Return the Tvl in Eth of a given set of shares across strategies - * - * @param shares - * @param strategiesWithSharesUnderlying - * @param strategyTokenPrices - * @returns - */ -export function sharesToTVLEth( - shares: { - strategyAddress: string - shares: string - }[], - strategiesWithSharesUnderlying: Map, - strategyTokenPrices: TokenPrices -): { [strategyAddress: string]: number } { - const beaconAddress = '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' - - const beaconStrategy = shares.find((s) => s.strategyAddress.toLowerCase() === beaconAddress) - - const tvlBeaconChain = beaconStrategy ? Number(beaconStrategy.shares) / 1e18 : 0 - - const strategies = getEigenContracts().Strategies - const addressToKey = Object.entries(strategies).reduce((acc, [key, value]) => { - acc[value.strategyContract.toLowerCase()] = key - return acc - }, {} as Record) - - const tvlStrategiesEth: { [strategyAddress: string]: number } = { - [beaconAddress]: tvlBeaconChain - } - - for (const share of shares) { - const strategyAddress = share.strategyAddress.toLowerCase() - const isBeaconStrategy = strategyAddress.toLowerCase() === beaconAddress - - const sharesUnderlying = strategiesWithSharesUnderlying.get(strategyAddress) - - const strategyTokenPrice = isBeaconStrategy - ? { eth: 1 } - : Object.values(strategyTokenPrices).find( - (stp) => stp.strategyAddress.toLowerCase() === strategyAddress - ) - - if (sharesUnderlying !== undefined && strategyTokenPrice) { - const strategyShares = - new prisma.Prisma.Decimal(share.shares) - .mul(new prisma.Prisma.Decimal(sharesUnderlying.toString())) - .div(new prisma.Prisma.Decimal(10).pow(18)) - .toNumber() / 1e18 - - const strategyTvl = strategyShares * strategyTokenPrice.eth - - const strategyKey = addressToKey[strategyAddress] - if (strategyKey) { - tvlStrategiesEth[strategyAddress] = (tvlStrategiesEth[strategyAddress] || 0) + strategyTvl - } - } - } - - return tvlStrategiesEth -} - -/** - * Return a map of strategy addresses <> token addresses - * - * @param strategies - * @returns - */ -export function tokenToStrategyAddressMap( - strategies: EigenStrategiesContractAddress -): Map { - const map = new Map() - for (const [key, value] of Object.entries(strategies)) { - if (key !== 'Eigen' && value?.tokenContract && value?.strategyContract) { - map.set(value.tokenContract.toLowerCase(), value.strategyContract.toLowerCase()) - } - } - return map -} - -/** - * Returns whether a given token address belongs to a list of special tokens - * - * @param tokenAddress - * @returns - */ -export function isSpecialToken(tokenAddress: string): boolean { - const specialTokens = - getNetwork() === holesky - ? [ - '0x6Cc9397c3B38739daCbfaA68EaD5F5D77Ba5F455', // WETH - '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' - ] - : [ - '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // WETH - '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' - ] - - return specialTokens.includes(tokenAddress.toLowerCase()) -} diff --git a/packages/seeder/src/monitors/operatorMetrics.ts b/packages/seeder/src/monitors/operatorMetrics.ts index a621de14..0189247d 100644 --- a/packages/seeder/src/monitors/operatorMetrics.ts +++ b/packages/seeder/src/monitors/operatorMetrics.ts @@ -2,9 +2,7 @@ import prisma from '@prisma/client' import { createHash } from 'crypto' import { getPrismaClient } from '../utils/prismaClient' import { bulkUpdateDbTransactions } from '../utils/seeder' -import { fetchStrategyTokenPrices } from '../utils/tokenPrices' -import { getStrategiesWithShareUnderlying } from '../metrics/seedMetricsTvl' -import { sharesToTVL } from './avsMetrics' +import { sharesToTVL, getStrategiesWithShareUnderlying } from '../utils/strategyShares' export async function monitorOperatorMetrics() { const prismaClient = getPrismaClient() @@ -22,7 +20,6 @@ export async function monitorOperatorMetrics() { let skip = 0 const take = 1000 - const strategyTokenPrices = await fetchStrategyTokenPrices() const strategiesWithSharesUnderlying = await getStrategiesWithShareUnderlying() while (true) { @@ -62,11 +59,7 @@ export async function monitorOperatorMetrics() { operator.totalStakers !== totalStakers || operator.sharesHash !== sharesHash ) { - const tvlObject = sharesToTVL( - operator.shares, - strategiesWithSharesUnderlying, - strategyTokenPrices - ) + const tvlObject = sharesToTVL(operator.shares, strategiesWithSharesUnderlying) data.push({ address: operator.address, diff --git a/packages/seeder/src/seedEthPricesDaily.ts b/packages/seeder/src/seedEthPricesDaily.ts index 540da605..64e76824 100644 --- a/packages/seeder/src/seedEthPricesDaily.ts +++ b/packages/seeder/src/seedEthPricesDaily.ts @@ -5,11 +5,6 @@ import { getPrismaClient } from './utils/prismaClient' import { bulkUpdateDbTransactions } from './utils/seeder' const CMC_API = 'https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/historical' -const apiKey = process.env.CMC_API_KEY -const CMC_TOKEN_IDS = [ - 8100, 21535, 27566, 23782, 29035, 24277, 28476, 15060, 23177, 8085, 25147, 24760, 2396, 4039 -] -const keysStr = CMC_TOKEN_IDS.join(',') export async function seedEthPricesDaily() { const prismaClient = getPrismaClient() @@ -28,10 +23,14 @@ export async function seedEthPricesDaily() { } try { + const tokens = await prismaClient.tokens.findMany() + const cmcTokenIds = tokens.map((t) => t.cmcId) + const keysStr = cmcTokenIds.join(',') + const response = await fetch( `${CMC_API}?id=${keysStr}&convert=eth&interval=daily&time_start=${startAt.toISOString()}&time_end=${endAt.toISOString()}`, { - headers: { 'X-CMC_PRO_API_KEY': `${apiKey}` } + headers: { 'X-CMC_PRO_API_KEY': process.env.CMC_API_KEY! } } ) const payload = await response.json() diff --git a/packages/seeder/src/seedStrategies.ts b/packages/seeder/src/seedStrategies.ts index 3b7b91c5..c1a0221c 100644 --- a/packages/seeder/src/seedStrategies.ts +++ b/packages/seeder/src/seedStrategies.ts @@ -1,5 +1,4 @@ import { strategyAbi } from './data/abi/strategy' -import { getEigenContracts } from './data/address' import { getPrismaClient } from './utils/prismaClient' import { bulkUpdateDbTransactions, saveLastSyncBlock } from './utils/seeder' import { getViemClient } from './utils/viemClient' @@ -7,7 +6,7 @@ import { getViemClient } from './utils/viemClient' const blockSyncKey = 'lastSyncedBlock_strategies' /** - * Seed strategies data + * Seed strategies data to update sharesToUnderlying * * @param toBlock * @param fromBlock @@ -17,49 +16,36 @@ export async function seedStrategies(toBlock?: bigint) { const viemClient = getViemClient() const lastBlock = toBlock ? toBlock : await viemClient.getBlockNumber() + const strategies = await prismaClient.strategies.findMany() // Prepare db transaction object - // biome-ignore lint/suspicious/noExplicitAny: const dbTransactions: any[] = [] - const strategies = Object.values(getEigenContracts().Strategies) - const strategyKeys = Object.keys(getEigenContracts().Strategies) - - await Promise.all( - strategies.map(async (s, i) => { - const strategyAddress = s.strategyContract.toLowerCase() - let sharesToUnderlying = 1e18 - - try { - sharesToUnderlying = (await viemClient.readContract({ - address: strategyAddress, - abi: strategyAbi, - functionName: 'sharesToUnderlyingView', - args: [1e18] - })) as number - } catch {} - - dbTransactions.push( - prismaClient.strategies.upsert({ - where: { - address: strategyAddress - }, - create: { - address: strategyAddress, - symbol: strategyKeys[i], - sharesToUnderlying: String(sharesToUnderlying), - createdAtBlock: Number(lastBlock), - updatedAtBlock: Number(lastBlock) - }, - update: { - symbol: strategyKeys[i], - sharesToUnderlying: String(sharesToUnderlying), - updatedAtBlock: Number(lastBlock) - } - }) - ) - }) - ) + for (const s of strategies) { + const strategyAddress = s.address.toLowerCase() + let sharesToUnderlying = s.sharesToUnderlying || 1e18 + + try { + sharesToUnderlying = (await viemClient.readContract({ + address: strategyAddress as `0x${string}`, + abi: strategyAbi, + functionName: 'sharesToUnderlyingView', + args: [1e18] + })) as number + } catch {} + + dbTransactions.push( + prismaClient.strategies.update({ + where: { + address: strategyAddress + }, + data: { + sharesToUnderlying: String(sharesToUnderlying), + updatedAtBlock: Number(lastBlock) + } + }) + ) + } await bulkUpdateDbTransactions( dbTransactions, diff --git a/packages/seeder/src/seedStrategyWhitelist.ts b/packages/seeder/src/seedStrategyWhitelist.ts new file mode 100644 index 00000000..a687b823 --- /dev/null +++ b/packages/seeder/src/seedStrategyWhitelist.ts @@ -0,0 +1,136 @@ +import { erc20Abi, getContract } from 'viem' +import { strategyAbi } from './data/abi/strategy' +import { getPrismaClient } from './utils/prismaClient' +import { fetchLastSyncBlock, loopThroughBlocks } from './utils/seeder' +import { getViemClient } from './utils/viemClient' + +const blockSyncKey = 'lastSyncedBlock_strategyWhitelist' +const blockSyncKeyLogs = 'lastSyncedBlock_logs_strategyWhitelist' + +/** + * Seed strategy whitelist data + * + * @param toBlock + * @param fromBlock + */ +export async function seedStrategyWhitelist(toBlock?: bigint, fromBlock?: bigint) { + const prismaClient = getPrismaClient() + + const firstBlock = fromBlock ? fromBlock : await fetchLastSyncBlock(blockSyncKey) + const lastBlock = toBlock ? toBlock : await fetchLastSyncBlock(blockSyncKeyLogs) + + // Bail early if there is no block diff to sync + if (lastBlock - firstBlock <= 0) { + console.log(`[In Sync] [Data] Strategy Whitelist from: ${firstBlock} to: ${lastBlock}`) + return + } + + await loopThroughBlocks(firstBlock, lastBlock, async (fromBlock, toBlock) => { + let allLogs: any[] = [] + + await prismaClient.eventLogs_StrategyAddedToDepositWhitelist + .findMany({ where: { blockNumber: { gt: fromBlock, lte: toBlock } } }) + .then((logs) => { + allLogs = [ + ...allLogs, + ...logs.map((log) => ({ + ...log, + eventName: 'StrategyAddedToDepositWhitelist' + })) + ] + }) + + await prismaClient.eventLogs_StrategyRemovedFromDepositWhitelist + .findMany({ where: { blockNumber: { gt: fromBlock, lte: toBlock } } }) + .then((logs) => { + allLogs = [ + ...allLogs, + ...logs.map((log) => ({ + ...log, + eventName: 'StrategyRemovedFromDepositWhitelist' + })) + ] + }) + + allLogs = allLogs.sort((a, b) => { + if (a.blockNumber === b.blockNumber) { + return a.transactionIndex - b.transactionIndex + } + + return Number(a.blockNumber - b.blockNumber) + }) + + // Strategy list + for (const l in allLogs) { + const log = allLogs[l] + const strategyAddress = String(log.strategy).toLowerCase() + + if (log.eventName === 'StrategyAddedToDepositWhitelist') { + try { + // Strategy contract + const strategyContract = getContract({ + address: strategyAddress as `0x${string}`, + abi: strategyAbi, + client: getViemClient() + }) + + const underlyingToken = String( + await strategyContract.read.underlyingToken() + ).toLowerCase() + const sharesToUnderlying = await strategyContract.read.sharesToUnderlyingView([1e18]) + if (!underlyingToken) continue + + // Underlying token contract + const underlyingTokenContract = getContract({ + address: underlyingToken as `0x${string}`, + abi: erc20Abi, + client: getViemClient() + }) + + const symbol = await underlyingTokenContract.read.symbol() + const decimals = await underlyingTokenContract.read.decimals() + const name = await underlyingTokenContract.read.name() + + await prismaClient.strategies.upsert({ + where: { + address: strategyAddress + }, + update: { + updatedAtBlock: Number(lastBlock) + }, + create: { + address: strategyAddress, + sharesToUnderlying: String(sharesToUnderlying), + symbol: String(symbol), + underlyingToken: String(underlyingToken), + createdAtBlock: Number(lastBlock), + updatedAtBlock: Number(lastBlock) + } + }) + + await prismaClient.tokens.upsert({ + where: { + address: String(underlyingToken) + }, + update: { + updatedAtBlock: Number(lastBlock) + }, + create: { + address: String(underlyingToken), + symbol: String(symbol), + name: String(name), + decimals: Number(decimals), + cmcId: 0, + createdAtBlock: Number(lastBlock), + updatedAtBlock: Number(lastBlock) + } + }) + } catch (error) { + console.log('failed to add Strategy', strategyAddress, error) + } + } else if (log.eventName === 'StrategyRemovedFromDepositWhitelist') { + await prismaClient.strategies.delete({ where: { address: strategyAddress } }) + } + } + }) +} diff --git a/packages/seeder/src/utils/operatorShares.ts b/packages/seeder/src/utils/operatorShares.ts new file mode 100644 index 00000000..75befd0e --- /dev/null +++ b/packages/seeder/src/utils/operatorShares.ts @@ -0,0 +1,25 @@ +export function withOperatorShares(avsOperators) { + const sharesMap: Map = new Map() + + avsOperators.map((avsOperator) => { + const shares = avsOperator.operator.shares.filter( + (s) => avsOperator.restakedStrategies.indexOf(s.strategyAddress.toLowerCase()) !== -1 + ) + + shares.map((s) => { + if (!sharesMap.has(s.strategyAddress)) { + sharesMap.set(s.strategyAddress, '0') + } + + sharesMap.set( + s.strategyAddress, + (BigInt(sharesMap.get(s.strategyAddress)!) + BigInt(s.shares)).toString() + ) + }) + }) + + return Array.from(sharesMap, ([strategyAddress, shares]) => ({ + strategyAddress, + shares + })) +} diff --git a/packages/seeder/src/utils/strategyShares.ts b/packages/seeder/src/utils/strategyShares.ts new file mode 100644 index 00000000..f5f06d6a --- /dev/null +++ b/packages/seeder/src/utils/strategyShares.ts @@ -0,0 +1,159 @@ +import prisma from '@prisma/client' +import { getPrismaClient } from './prismaClient' +import { fetchTokenPrices } from './tokenPrices' +import { getViemClient } from './viemClient' +import { serviceManagerUIAbi } from '../data/abi/serviceManagerUIAbi' + +export interface StrategyWithShareUnderlying { + symbol: string + strategyAddress: string + tokenAddress: string + sharesToUnderlying: number + ethPrice: number +} + +/** + * Get the strategies with their shares underlying and their token prices + * + * @returns + */ +export async function getStrategiesWithShareUnderlying(): Promise { + const prismaClient = getPrismaClient() + const strategies = await prismaClient.strategies.findMany() + const tokenPrices = await fetchTokenPrices() + + return strategies.map((s) => { + const foundTokenPrice = tokenPrices.find( + (tp) => tp.address.toLowerCase() === s.underlyingToken.toLowerCase() + ) + + return { + symbol: s.symbol, + strategyAddress: s.address, + tokenAddress: s.underlyingToken, + sharesToUnderlying: BigInt(s.sharesToUnderlying) as unknown as number, + ethPrice: foundTokenPrice?.ethPrice || 0 + } + }) +} + +/** + * Return the Tvl of a given set of shares across strategies + * + * @param shares + * @param strategiesWithSharesUnderlying + * @returns + */ +export function sharesToTVL( + shares: { + strategyAddress: string + shares: string + }[], + strategiesWithSharesUnderlying: StrategyWithShareUnderlying[] +) { + const beaconAddress = '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' + + const beaconStrategy = shares.find((s) => s.strategyAddress.toLowerCase() === beaconAddress) + const restakingStrategies = shares.filter( + (s) => s.strategyAddress.toLowerCase() !== beaconAddress + ) + + const tvlBeaconChain = beaconStrategy ? Number(beaconStrategy.shares) / 1e18 : 0 + + let tvlRestaking = 0 + const tvlStrategies: Map = new Map() + const tvlStrategiesEth: Map = new Map() + + restakingStrategies.map((s) => { + const sharesUnderlying = strategiesWithSharesUnderlying.find( + (su) => su.strategyAddress.toLowerCase() === s.strategyAddress.toLowerCase() + ) + + if (sharesUnderlying) { + const strategyShares = + Number((BigInt(s.shares) * BigInt(sharesUnderlying.sharesToUnderlying)) / BigInt(1e18)) / + 1e18 + + tvlStrategies.set(sharesUnderlying.symbol, strategyShares) + + // Add the TVL in ETH, if 0, it will not be added to the total + if (sharesUnderlying.ethPrice) { + const strategyTvl = strategyShares * sharesUnderlying.ethPrice + + tvlStrategiesEth.set(sharesUnderlying.symbol, strategyTvl) + + tvlRestaking = tvlRestaking + strategyTvl + } + } + }) + + return { + tvl: tvlBeaconChain + tvlRestaking, + tvlBeaconChain, + tvlWETH: tvlStrategies.has('WETH') ? tvlStrategies.get('WETH') : 0, + tvlRestaking, + tvlStrategies: Object.fromEntries(tvlStrategies.entries()), + tvlStrategiesEth: Object.fromEntries(tvlStrategiesEth.entries()) + } +} + +/** + * Return the Tvl in Eth of a given set of shares across strategies + * + * @param shares + * @param strategiesWithSharesUnderlying + * @returns + */ +export function sharesToTVLStrategies( + shares: { + strategyAddress: string + shares: string + }[], + strategiesWithSharesUnderlying: StrategyWithShareUnderlying[] +): { [strategyAddress: string]: number } { + const tvlStrategiesEth: { [strategyAddress: string]: number } = {} + + for (const share of shares) { + const strategyAddress = share.strategyAddress.toLowerCase() + + const sharesUnderlying = strategiesWithSharesUnderlying.find( + (su) => su.strategyAddress.toLowerCase() === strategyAddress + ) + + if (sharesUnderlying) { + const strategyShares = + new prisma.Prisma.Decimal(share.shares) + .mul(new prisma.Prisma.Decimal(sharesUnderlying.sharesToUnderlying.toString())) + .div(new prisma.Prisma.Decimal(10).pow(18)) + .toNumber() / 1e18 + + const strategyTokenPrice = sharesUnderlying.ethPrice || 0 + const strategyTvl = strategyShares * strategyTokenPrice + tvlStrategiesEth[strategyAddress] = (tvlStrategiesEth[strategyAddress] || 0) + strategyTvl + } + } + + return tvlStrategiesEth +} + +/** + * Get the restakeable strategies for a given avs + * + * @param avsAddress + * @returns + */ +export async function getRestakeableStrategies(avsAddress: string): Promise { + try { + const viemClient = getViemClient() + + const strategies = (await viemClient.readContract({ + address: avsAddress as `0x${string}`, + abi: serviceManagerUIAbi, + functionName: 'getRestakeableStrategies' + })) as string[] + + return strategies.map((s) => s.toLowerCase()) + } catch (error) {} + + return [] +} diff --git a/packages/seeder/src/utils/tokenPrices.ts b/packages/seeder/src/utils/tokenPrices.ts index 5710a812..19e5d716 100644 --- a/packages/seeder/src/utils/tokenPrices.ts +++ b/packages/seeder/src/utils/tokenPrices.ts @@ -1,50 +1,28 @@ -import { - type EigenStrategiesContractAddress, - type RewardsTokensContractAddress, - getEigenContracts -} from '../data/address' import NodeCache from 'node-cache' +import { getPrismaClient } from './prismaClient' -const cacheStore = new NodeCache() - -type Tokens = keyof EigenStrategiesContractAddress -type RewardTokens = keyof RewardsTokensContractAddress +const cacheStore = new NodeCache({ stdTTL: 240 }) type TokenPrice = { + id: number + address: string symbol: string - strategyAddress: string - eth: number - usd?: number + ethPrice: number + decimals: number } -type RewardTokenPrice = { - symbol: string - tokenAddress: string - eth: number - usd?: number -} +export async function fetchTokenPrices(): Promise { + const prismaClient = getPrismaClient() -export type TokenPrices = { - [key in Tokens]?: TokenPrice -} - -export type RewardTokenPrices = { - [key in RewardTokens]?: RewardTokenPrice -} - -export async function fetchStrategyTokenPrices(): Promise { - const tokenPrices: TokenPrices = {} - - const CMC_TOKEN_IDS = [ - 8100, 21535, 27566, 23782, 29035, 24277, 28476, 15060, 23177, 8085, 25147, 24760, 2396 - ] + const tokenPrices: TokenPrice[] = [] + const tokens = await prismaClient.tokens.findMany() + const cmcTokenIds = tokens.map((t) => t.cmcId) const CMC_API = 'https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest' - const keys = Object.keys(getEigenContracts().Strategies) as Tokens[] - const keysStr = CMC_TOKEN_IDS.join(',') + const keysStr = cmcTokenIds.filter((id) => id !== 0).join(',') - const cachedValue = cacheStore.get(`price_${keysStr}`) + const cachedValue = await cacheStore.get(`price_${keysStr}`) if (cachedValue) { return cachedValue @@ -59,61 +37,21 @@ export async function fetchStrategyTokenPrices(): Promise { // biome-ignore lint/suspicious/noExplicitAny: const quotes = Object.values(payload.data) as any[] - keys.map((k) => { - const quoteKey = quotes.find((q) => q.symbol.toLowerCase() === k.toLowerCase()) - const price = { - symbol: k, - // biome-ignore lint/style/noNonNullAssertion: - strategyAddress: getEigenContracts().Strategies[k]!.strategyContract, - eth: quoteKey ? payload.data[quoteKey.id].quote.ETH.price : 0 + tokens.map((t) => { + const quote = quotes.find((q) => q.id === t.cmcId) + + if (quote) { + tokenPrices.push({ + id: t.cmcId, + address: t.address, + symbol: quote.symbol, + ethPrice: quote ? quote.quote.ETH.price : 0, + decimals: t.decimals + }) } - - tokenPrices[k] = price }) - cacheStore.set(`price_${keysStr}`, tokenPrices, 120) + cacheStore.set(`price_${keysStr}`, tokenPrices, 120_000) return tokenPrices } - -export async function fetchRewardTokenPrices(): Promise { - const rewardTokenPrices: RewardTokenPrices = {} - - const CMC_TOKEN_IDS = [4039] // ARPA - - const CMC_API = 'https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest' - - const keys = Object.keys(getEigenContracts().Rewards) as RewardTokens[] - const keysStr = CMC_TOKEN_IDS.join(',') - - const cachedValue = await cacheStore.get(`price_${keysStr}`) - - if (cachedValue) { - return cachedValue - } - - const response = await fetch( - `${CMC_API}?id=${keysStr}&convert=eth`, - // biome-ignore lint/style/noNonNullAssertion: - { headers: { 'X-CMC_PRO_API_KEY': process.env.CMC_API_KEY! } } - ) - const payload = await response.json() - // biome-ignore lint/suspicious/noExplicitAny: - const quotes = Object.values(payload.data) as any[] - - keys.map((k) => { - const quoteKey = quotes.find((q) => q.symbol.toLowerCase() === k.toLowerCase()) - const price = { - symbol: k, - // biome-ignore lint/style/noNonNullAssertion: - tokenAddress: getEigenContracts().Rewards[k]!, - eth: quoteKey ? payload.data[quoteKey.id].quote.ETH.price : 0 - } - - rewardTokenPrices[k] = price - }) - - cacheStore.set(`price_${keysStr}`, rewardTokenPrices, 120_000) - - return rewardTokenPrices -}