diff --git a/package-lock.json b/package-lock.json index 7e393507..584016b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2060,6 +2060,20 @@ "resolved": "packages/openapi", "link": true }, + "node_modules/p-limit": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz", + "integrity": "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==", + "dependencies": { + "yocto-queue": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2621,6 +2635,17 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" }, + "node_modules/yocto-queue": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", @@ -2667,6 +2692,7 @@ "morgan": "~1.9.1", "node-cache": "^5.1.2", "node-cron": "^3.0.3", + "p-limit": "^6.2.0", "route-cache": "^0.7.0", "viem": "^2.8.14", "zod": "^3.23.4", diff --git a/packages/api/package.json b/packages/api/package.json index 543fac60..872f4fb7 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -25,6 +25,7 @@ "morgan": "~1.9.1", "node-cache": "^5.1.2", "node-cron": "^3.0.3", + "p-limit": "^6.2.0", "route-cache": "^0.7.0", "viem": "^2.8.14", "zod": "^3.23.4", @@ -37,8 +38,8 @@ "@types/debug": "^4.1.12", "@types/express": "^4.17.21", "@types/morgan": "^1.9.9", - "@types/node-cron": "^3.0.11", "@types/node": "^20.12.2", + "@types/node-cron": "^3.0.11", "@types/route-cache": "^0.5.5", "prisma": "^5.11.0", "tsx": "^4.7.1", diff --git a/packages/api/src/routes/avs/avsController.ts b/packages/api/src/routes/avs/avsController.ts index 213d9c41..7cd1fe31 100644 --- a/packages/api/src/routes/avs/avsController.ts +++ b/packages/api/src/routes/avs/avsController.ts @@ -9,6 +9,7 @@ import { SortByQuerySchema } from '../../schema/zod/schemas/sortByQuery' import { SearchByTextQuerySchema } from '../../schema/zod/schemas/searchByTextQuery' import { WithRewardsQuerySchema } from '../../schema/zod/schemas/withRewardsQuery' import { getOperatorSearchQuery } from '../operators/operatorController' +import { LegacyQuerySchema } from '../../schema/zod/schemas/legacyQuery' import { EigenExplorerApiError, handleAndReturnErrorResponse } from '../../schema/errors' import { getStrategiesWithShareUnderlying, @@ -49,6 +50,7 @@ export async function getAllAVS(req: Request, res: Response) { .and(SortByQuerySchema) .and(WithCuratedMetadata) .and(SearchByTextQuerySchema) + .and(LegacyQuerySchema) .safeParse(req.query) if (!queryCheck.success) { @@ -67,8 +69,10 @@ export async function getAllAVS(req: Request, res: Response) { sortByTotalOperators, sortByApy, searchByText, - searchMode + searchMode, + legacy } = queryCheck.data + const isLegacy = legacy === 'true' // Setup sort if applicable const sortConfig = sortByTotalStakers @@ -87,7 +91,7 @@ export async function getAllAVS(req: Request, res: Response) { // Fetch records and apply search/sort const avsRecords = await prisma.avs.findMany({ where: { - ...getAvsFilterQuery(true), + ...getAvsFilterQuery(true, isLegacy), ...searchFilterQuery, ...(minTvl ? { tvlEth: { gte: minTvl } } : {}) }, @@ -117,7 +121,7 @@ export async function getAllAVS(req: Request, res: Response) { // Fetch count const avsCount = await prisma.avs.count({ where: { - ...getAvsFilterQuery(true), + ...getAvsFilterQuery(true, isLegacy), ...searchFilterQuery, ...(minTvl ? { tvlEth: { gte: minTvl } } : {}) } @@ -171,7 +175,9 @@ export async function getAllAVS(req: Request, res: Response) { */ export async function getAllAVSAddresses(req: Request, res: Response) { // Validate pagination query - const queryCheck = PaginationQuerySchema.and(SearchByTextQuerySchema).safeParse(req.query) + const queryCheck = PaginationQuerySchema.and(SearchByTextQuerySchema) + .and(LegacyQuerySchema) + .safeParse(req.query) if (!queryCheck.success) { return handleAndReturnErrorResponse(req, res, queryCheck.error) } @@ -223,7 +229,7 @@ export async function getAllAVSAddresses(req: Request, res: Response) { // Determine count const avsCount = await prisma.avs.count({ where: { - ...getAvsFilterQuery(true), + ...getAvsFilterQuery(true, isLegacy), ...searchFilterQuery } }) @@ -336,6 +342,7 @@ export async function getAVS(req: Request, res: Response) { const queryCheck = WithTvlQuerySchema.and(WithCuratedMetadata) .and(WithRewardsQuerySchema) .and(WithTrailingApySchema) + .and(LegacyQuerySchema) .safeParse(req.query) if (!queryCheck.success) { return handleAndReturnErrorResponse(req, res, queryCheck.error) @@ -348,10 +355,10 @@ export async function getAVS(req: Request, res: Response) { try { const { address } = req.params - const { withTvl, withCuratedMetadata, withRewards, withTrailingApy } = queryCheck.data + const { withTvl, withCuratedMetadata, withRewards, withTrailingApy, legacy } = queryCheck.data const avs = await prisma.avs.findUniqueOrThrow({ - where: { address: address.toLowerCase(), ...getAvsFilterQuery() }, + where: { address: address.toLowerCase(), ...getAvsFilterQuery(false, legacy === 'true') }, include: { curatedMetadata: withCuratedMetadata, additionalInfo: withCuratedMetadata, @@ -369,9 +376,12 @@ export async function getAVS(req: Request, res: Response) { } }) - const shares = withOperatorShares(avs.operators).filter((s) => true) // TODO: Add back with operator set strategies - // (s) => avs.restakeableStrategies.indexOf(s.strategyAddress.toLowerCase()) !== -1 + // TODO: Select whether to use operator set strategies or all strategies + // const shares = withOperatorShares(avs.operators).filter((s) => true) + const shares = withOperatorShares(avs.operators).filter( + (s) => avs.restakeableStrategies.indexOf(s.strategyAddress.toLowerCase()) !== -1 + ) const strategiesWithSharesUnderlying = withTvl ? await getStrategiesWithShareUnderlying() : [] @@ -427,6 +437,7 @@ export async function getAVSStakers(req: Request, res: Response) { // Validate query and params const queryCheck = PaginationQuerySchema.and(WithTvlQuerySchema) .and(UpdatedSinceQuerySchema) + .and(LegacyQuerySchema) .safeParse(req.query) if (!queryCheck.success) { @@ -440,10 +451,10 @@ export async function getAVSStakers(req: Request, res: Response) { try { const { address } = req.params - const { skip, take, withTvl, updatedSince } = queryCheck.data + const { skip, take, withTvl, updatedSince, legacy } = queryCheck.data const avs = await prisma.avs.findUniqueOrThrow({ - where: { address: address.toLowerCase(), ...getAvsFilterQuery() }, + where: { address: address.toLowerCase(), ...getAvsFilterQuery(false, legacy === 'true') }, include: { operators: true } }) @@ -525,6 +536,7 @@ export async function getAVSOperators(req: Request, res: Response) { .and(MinTvlQuerySchema) .and(SortByQuerySchema) .and(SearchByTextQuerySchema) + .and(LegacyQuerySchema) .safeParse(req.query) if (!queryCheck.success) { return handleAndReturnErrorResponse(req, res, queryCheck.error) @@ -537,12 +549,12 @@ export async function getAVSOperators(req: Request, res: Response) { try { const { address } = req.params - const { skip, take, withTvl, minTvl, sortOperatorsByTvl, searchByText, searchMode } = + const { skip, take, withTvl, minTvl, sortOperatorsByTvl, searchByText, searchMode, legacy } = queryCheck.data const searchFilterQuery = getOperatorSearchQuery(searchByText, searchMode, 'partial') const avs = await prisma.avs.findUniqueOrThrow({ - where: { address: address.toLowerCase(), ...getAvsFilterQuery() }, + where: { address: address.toLowerCase(), ...getAvsFilterQuery(false, legacy === 'true') }, include: { operators: { where: { isActive: true } @@ -1304,21 +1316,33 @@ export function getAvsFilterQuery(filterName?: boolean, isLegacy = true) { } } - // After introduction of area-internal-dashboard, `isVisible` checks move to `AvsAdditionalInfo` with `CuratedMetadata` only as fallback - // Currently, this is only accessible by setting the flag `legacy=false` when using full text search + // After introduction of area-internal-dashboard, validity checks move to `AvsAdditionalInfo` with `CuratedMetadata` only as fallback + // Currently, this is only accessible by setting the flag `legacy=false`, on routes where `LegacyQuerySchema` enabled return { AND: [ queryWithName, { OR: [ - // Check if `additionalInfo.isVisible` is true + // Check `additionalInfo.isVisible` && `additionalInfo.isVerified` is true { - additionalInfo: { - some: { - metadataKey: 'isVisible', - metadataContent: 'true' + AND: [ + { + additionalInfo: { + some: { + metadataKey: 'isVisible', + metadataContent: 'true' + } + } + }, + { + additionalInfo: { + some: { + metadataKey: 'isVerified', + metadataContent: 'true' + } + } } - } + ] }, // If `additionalInfo.isVisible` does not exist, check `curatedMetadata.isVisible` is true { diff --git a/packages/api/src/routes/operators/operatorController.ts b/packages/api/src/routes/operators/operatorController.ts index 6b5c33a1..c19dc16b 100644 --- a/packages/api/src/routes/operators/operatorController.ts +++ b/packages/api/src/routes/operators/operatorController.ts @@ -28,6 +28,7 @@ import { getDailyAvsStrategyTvl } from '../../utils/trailingApyUtils' import { fetchBaseApys } from '../../utils/baseApys' +import pLimit from 'p-limit' /** * Function for route /operators @@ -39,6 +40,8 @@ import { fetchBaseApys } from '../../utils/baseApys' export async function getAllOperators(req: Request, res: Response) { // Validate pagination query const result = PaginationQuerySchema.and(WithTvlQuerySchema) + .and(WithRewardsQuerySchema) + .and(WithTrailingApySchema) .and(MinTvlQuerySchema) .and(SortByQuerySchema) .and(SearchByTextQuerySchema) @@ -50,6 +53,8 @@ export async function getAllOperators(req: Request, res: Response) { skip, take, withTvl, + withRewards, + withTrailingApy, minTvl, sortByTvl, sortByTotalStakers, @@ -105,12 +110,18 @@ export async function getAllOperators(req: Request, res: Response) { const strategiesWithSharesUnderlying = withTvl ? await getStrategiesWithShareUnderlying() : [] + const rewardsMap = + withRewards || withTrailingApy + ? await calculateOperatorApyForAll(operatorRecords, withTrailingApy) + : {} + const operators = operatorRecords.map((operator) => ({ ...operator, avsRegistrations: operator.avs, totalStakers: operator.totalStakers, totalAvs: operator.totalAvs, tvl: withTvl ? sharesToTVL(operator.shares, strategiesWithSharesUnderlying) : undefined, + rewards: withRewards || withTrailingApy ? rewardsMap[operator.address] : undefined, metadataUrl: undefined, isMetadataSynced: undefined, avs: undefined, @@ -234,7 +245,7 @@ export async function getOperator(req: Request, res: Response) { tvl: withTvl ? sharesToTVL(operator.shares, strategiesWithSharesUnderlying) : undefined, rewards: withRewards || withTrailingApy - ? await calculateOperatorApy(operator, withTrailingApy) + ? await calculateOperatorApyForAll([operator], withTrailingApy) : undefined, stakers: undefined, metadataUrl: undefined, @@ -564,7 +575,23 @@ export function getOperatorSearchQuery( } // biome-ignore lint/suspicious/noExplicitAny: -async function calculateOperatorApy(operator: any, withTrailingApy: boolean = false) { +async function calculateOperatorApy( + operator: any, + avsWithEligibleRewardSubmissions: { + avs: any + eligibleRewards: any[] + status: boolean + }[], + withTrailingApy: boolean, + tokenPriceMap: Map, + baseApyMap: Map, + avsRegistrationByDay: Record, + dailyTvlMap: Record, + splitMap: Map, + startDate: Date, + endDate: Date, + avsTvlMap: Map> +) { try { const avsApyMap: Map< string, @@ -587,134 +614,11 @@ async function calculateOperatorApy(operator: any, withTrailingApy: boolean = fa }[] } > = new Map() - const strategyTvlMap: Map = new Map() const operatorStrategyTvlMap: Map = new Map() - - const startDate = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000) - startDate.setUTCHours(0, 0, 0, 0) - const endDate = new Date() - endDate.setUTCHours(0, 0, 0, 0) - - if (!operator?.shares?.length) { - return [] - } - operator.shares.forEach((share) => operatorStrategyTvlMap.set(share.strategyAddress.toLowerCase(), BigInt(share.shares)) ) - // Filter AVS with reward submissions - const avsWithRewards = operator.avs.filter( - (avsOp) => - avsOp.avs.rewardSubmissions.length > 0 || - avsOp.avs.operatorDirectedRewardSubmissions.length > 0 - ) - - if (!avsWithRewards || avsWithRewards.length === 0) return [] - - const pastYearStartSec = Math.floor(startDate.getTime() / 1000) - // Filter AVS with eligible rewards - const isEligibleReward = (reward: any) => { - const endTimeSec = reward.startTimestamp + BigInt(reward.duration) - return ( - (operatorStrategyTvlMap.get(reward.strategyAddress.toLowerCase()) ?? 0n) > 0n && - endTimeSec >= BigInt(pastYearStartSec) && - (!reward.operatorAddress || - reward.operatorAddress.toLowerCase() === operator.address.toLowerCase()) - ) - } - - const avsWithEligibleRewardSubmissions = avsWithRewards - .map((avsOp) => ({ - avs: avsOp.avs, - eligibleRewards: [ - // Filter for rewardSubmissions based on strategy address - ...avsOp.avs.rewardSubmissions.filter(isEligibleReward), - // Filter for operatorDirectedRewardSubmissions based on strategy address and operator address - ...avsOp.avs.operatorDirectedRewardSubmissions.filter(isEligibleReward) - ], - status: avsOp.isActive - })) - .filter((item) => item.eligibleRewards.length > 0) - - if (!avsWithEligibleRewardSubmissions || avsWithEligibleRewardSubmissions.length === 0) - return [] - - const avsStrategyPairs = withTrailingApy - ? avsWithEligibleRewardSubmissions.flatMap(({ avs, eligibleRewards }) => - [...new Set(eligibleRewards.map((r: any) => r.strategyAddress.toLowerCase()))].map( - (strategyAddress) => ({ - avsAddress: avs.address, - strategyAddress - }) - ) - ) - : [] - - const avsOperators = avsWithRewards.map((avsOp) => ({ - avsAddress: avsOp.avsAddress, - isActive: avsOp.isActive - })) - - // Parallelize initial data fetching - const [ - tokenPrices, - strategiesWithSharesUnderlying, - avsRegistrationByDay, - dailyTvlMap, - baseApys, - operatorAvsSplits - ] = await Promise.all([ - fetchTokenPrices(), - getStrategiesWithShareUnderlying(), - withTrailingApy - ? buildOperatorAvsRegistrationMap( - operator.address.toLowerCase(), - avsOperators, - startDate, - endDate - ) - : [], - withTrailingApy ? getDailyAvsStrategyTvl(avsStrategyPairs, startDate, endDate) : {}, - fetchBaseApys(), - await prisma.operatorAvsSplit.findMany({ - where: { - operatorAddress: operator.address.toLowerCase(), - avsAddress: { - in: avsWithEligibleRewardSubmissions.map(({ avs }) => avs.address.toLowerCase()) - } - }, - orderBy: [{ activatedAt: 'desc' }] - }) - ]) - - const tokenPriceMap = new Map(tokenPrices.map((tp) => [tp.address.toLowerCase(), tp])) - const baseApyMap = new Map(baseApys.map((ba) => [ba.strategyAddress.toLowerCase(), ba.apy])) - - // Create a lookup for splitBips - const splitMap: Map = new Map() - operatorAvsSplits.forEach((split) => { - const key = `${split.operatorAddress.toLowerCase()}:${split.avsAddress.toLowerCase()}` - if (!splitMap.has(key)) { - splitMap.set(key, []) - } - splitMap.get(key)!.push({ - activatedAt: split.activatedAt, - splitBips: split.splitBips - }) - }) - - // Function to get splitBips for a given operator, AVS, and timestamp - const getSplit = (operatorAddress: string, avsAddress: string, timestamp: bigint): number => { - const key = `${operatorAddress.toLowerCase()}:${avsAddress.toLowerCase()}` - const splits = splitMap.get(key) || [] - const validSplit = splits - .filter((split) => split.activatedAt <= timestamp) - .sort((a, b) => Number(b.activatedAt) - Number(a.activatedAt))[0] - return validSplit ? validSplit.splitBips / 100 : 10 // Default to 10% - } - - // Process Projected and Trailing APY for AVSs for (const { avs, eligibleRewards, status } of avsWithEligibleRewardSubmissions) { const avsAddressLower = avs.address.toLowerCase() const strategyApyMap: Map< @@ -731,12 +635,8 @@ async function calculateOperatorApy(operator: any, withTrailingApy: boolean = fa } > = new Map() - const shares = withOperatorShares(avs.operators).filter( - (s) => avs.restakeableStrategies?.indexOf(s.strategyAddress.toLowerCase()) !== -1 - ) - - // Fetch the AVS tvl for each strategy - const tvlStrategiesEth = sharesToTVLStrategies(shares, strategiesWithSharesUnderlying) + // Use precomputed AVS TVL + const tvlStrategiesEth = avsTvlMap.get(avsAddressLower) || {} // Process strategies for Current and Trailing APY for (const strategyAddress of avs.restakeableStrategies || []) { @@ -759,8 +659,6 @@ async function calculateOperatorApy(operator: any, withTrailingApy: boolean = fa if (!relevantSubmissions || relevantSubmissions.length === 0) continue - strategyTvlMap.set(strategyAddressLower, strategyTvl) - const tokenApyMap: Map = new Map() const tokenRewards: Map< string, @@ -780,12 +678,14 @@ async function calculateOperatorApy(operator: any, withTrailingApy: boolean = fa if (rewardTokenAddress) { const tokenPrice = tokenPriceMap.get(rewardTokenAddress) - // Apply operator commission from OperatorAvsSplit - const operatorSplit = getSplit( - operator.address, - avsAddressLower, - submission.startTimestamp - ) + // Apply operator commission from splitMap + const key = `${operator.address.toLowerCase()}:${avsAddressLower}` + const splits = splitMap.get(key) || [] + const validSplit = splits + .filter((split) => split.activatedAt <= submission.startTimestamp) + .sort((a, b) => Number(b.activatedAt) - Number(a.activatedAt))[0] + const operatorSplit = validSplit ? validSplit.splitBips / 100 : 10 + rewardIncrementEth = submission.amount .mul(new Prisma.Prisma.Decimal(tokenPrice?.ethPrice ?? 0)) .div(new Prisma.Prisma.Decimal(10).pow(tokenPrice?.decimals ?? 18)) @@ -801,7 +701,7 @@ async function calculateOperatorApy(operator: any, withTrailingApy: boolean = fa } tokenData.totalRewardsEth = status ? tokenData.totalRewardsEth.add(rewardIncrementEth) - : (tokenData.totalRewardsEth = new Prisma.Prisma.Decimal(0)) + : new Prisma.Prisma.Decimal(0) tokenData.timeSegments.push({ start: Number(submission.startTimestamp), end: Number(submission.startTimestamp) + submission.duration @@ -953,3 +853,220 @@ async function calculateOperatorApy(operator: any, withTrailingApy: boolean = fa return Array.from(avsApyMap.values()) } catch {} } + +async function calculateOperatorApyForAll(operators: any[], withTrailingApy: boolean = false) { + try { + const startDate = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000) + startDate.setUTCHours(0, 0, 0, 0) + const endDate = new Date() + endDate.setUTCHours(0, 0, 0, 0) + const pastYearStartSec = Math.floor(startDate.getTime() / 1000) + + // Pre-fetch shared data + const [tokenPrices, strategiesWithSharesUnderlying, baseApys, avsWithRewards] = + await Promise.all([ + fetchTokenPrices(), + getStrategiesWithShareUnderlying(), + fetchBaseApys(), + prisma.avs.findMany({ + where: { + OR: [ + { + rewardSubmissions: { some: { startTimestamp: { gte: BigInt(pastYearStartSec) } } } + }, + { + operatorDirectedRewardSubmissions: { + some: { startTimestamp: { gte: BigInt(pastYearStartSec) } } + } + } + ] + }, + select: { + address: true, + rewardSubmissions: { + where: { startTimestamp: { gte: BigInt(pastYearStartSec) } } + }, + operatorDirectedRewardSubmissions: { + where: { startTimestamp: { gte: BigInt(pastYearStartSec) } } + }, + restakeableStrategies: true, + operators: { + where: { isActive: true }, + include: { + operator: { + include: { shares: true } + } + } + } + } + }) + ]) + + const tokenPriceMap = new Map(tokenPrices.map((tp) => [tp.address.toLowerCase(), tp])) + const baseApyMap = new Map(baseApys.map((ba) => [ba.strategyAddress.toLowerCase(), ba.apy])) + + // Build avsSet and operatorAvsMap + const avsSet = new Set() + const avsTvlMap = new Map>() + + avsWithRewards.forEach((avs) => { + const avsAddress = avs.address.toLowerCase() + avsSet.add(avsAddress) + }) + + // Build avsWithEligibleRewardSubmissionsMap + const avsWithEligibleRewardSubmissionsMap = new Map< + string, + { avs: any; eligibleRewards: any[]; status: boolean }[] + >() + avsWithRewards.forEach((avs) => { + const avsAddress = avs.address.toLowerCase() + const eligibleRewards = [...avs.rewardSubmissions, ...avs.operatorDirectedRewardSubmissions] + avsWithEligibleRewardSubmissionsMap.set(avsAddress, [ + { + avs, + eligibleRewards, + status: avs.operators.some((op) => op.isActive) + } + ]) + + // Precompute AVS TVL + const shares = withOperatorShares(avs.operators).filter( + (s) => avs.restakeableStrategies?.indexOf(s.strategyAddress.toLowerCase()) !== -1 + ) + const tvlStrategiesEth = sharesToTVLStrategies(shares, strategiesWithSharesUnderlying) + avsTvlMap.set(avsAddress, tvlStrategiesEth) + }) + + // Build avsStrategyPairs for TVL + let dailyTvlMap: Record = {} + if (withTrailingApy) { + const avsStrategyPairs = avsWithRewards.flatMap((avs) => { + const eligibleRewards = [ + ...(avs.rewardSubmissions || []), + ...(avs.operatorDirectedRewardSubmissions || []) + ] + const eligibleStrategyAddresses = new Set( + eligibleRewards.map((r) => r.strategyAddress.toLowerCase()) + ) + const filteredStrategies = (avs.restakeableStrategies || []) + .map((s) => s.toLowerCase()) + .filter((strategyAddress) => eligibleStrategyAddresses.has(strategyAddress)) + return filteredStrategies.map((strategyAddress) => ({ + avsAddress: avs.address.toLowerCase(), + strategyAddress + })) + }) + dailyTvlMap = await getDailyAvsStrategyTvl(avsStrategyPairs, startDate, endDate) + } + + // Fetch operator splits + const operatorAvsSplits = await prisma.operatorAvsSplit.findMany({ + where: { + operatorAddress: { + in: operators.map((op) => op.address.toLowerCase()) + }, + avsAddress: { + in: Array.from(avsSet) + } + }, + orderBy: [{ activatedAt: 'desc' }] + }) + + const splitMap: Map = new Map() + operatorAvsSplits.forEach((split) => { + const key = `${split.operatorAddress.toLowerCase()}:${split.avsAddress.toLowerCase()}` + if (!splitMap.has(key)) { + splitMap.set(key, []) + } + splitMap.get(key)!.push({ + activatedAt: split.activatedAt, + splitBips: split.splitBips + }) + }) + + // Process each operator with p-limit + const limit = pLimit(20) + const operatorResults = await Promise.all( + operators.map(async (operator) => + limit(async () => { + if (!operator?.shares?.length) { + return { + ...operator, + rewards: [] + } + } + + const operatorStrategyTvlMap: Map = new Map() + operator.shares.forEach((share) => + operatorStrategyTvlMap.set(share.strategyAddress.toLowerCase(), BigInt(share.shares)) + ) + + // Filter relevant AVSs using operator.avs + const avsWithEligibleRewardSubmissions = operator.avs + .filter((avsOp) => avsSet.has(avsOp.avsAddress.toLowerCase())) + .flatMap((avsOp) => + avsWithEligibleRewardSubmissionsMap + .get(avsOp.avsAddress.toLowerCase())! + .map((item) => ({ + ...item, + eligibleRewards: item.eligibleRewards.filter( + (reward) => + (operatorStrategyTvlMap.get(reward.strategyAddress.toLowerCase()) ?? 0n) > + 0n && + (!reward.operatorAddress || + reward.operatorAddress.toLowerCase() === operator.address.toLowerCase()) + ), + status: avsOp.isActive + })) + ) + .filter((item) => item.eligibleRewards.length > 0) + + if (!avsWithEligibleRewardSubmissions.length) { + return { + ...operator, + rewards: [] + } + } + + // Fetch AVS registration data for this operator + let avsRegistrationByDay: Record = {} + if (withTrailingApy) { + const avsOperators = avsWithEligibleRewardSubmissions.map((item) => ({ + avsAddress: item.avs.address, + isActive: item.status + })) + avsRegistrationByDay = await buildOperatorAvsRegistrationMap( + operator.address.toLowerCase(), + avsOperators, + startDate, + endDate + ) + } + + // Process APY + const rewards = await calculateOperatorApy( + operator, + avsWithEligibleRewardSubmissions, + withTrailingApy, + tokenPriceMap, + baseApyMap, + avsRegistrationByDay, + dailyTvlMap, + splitMap, + startDate, + endDate, + avsTvlMap + ) + + return { + address: operator.address, + rewards + } + }) + ) + ) + + return Object.fromEntries(operatorResults.map((op) => [op.address.toLowerCase(), op.rewards])) + } catch {} +} diff --git a/packages/api/src/schema/zod/schemas/legacyQuery.ts b/packages/api/src/schema/zod/schemas/legacyQuery.ts new file mode 100644 index 00000000..0790ca99 --- /dev/null +++ b/packages/api/src/schema/zod/schemas/legacyQuery.ts @@ -0,0 +1,5 @@ +import z from '../' + +export const LegacyQuerySchema = z.object({ + legacy: z.enum(['true', 'false']).default('true').openapi({ example: 'false' }) +}) diff --git a/packages/api/src/schema/zod/schemas/searchByTextQuery.ts b/packages/api/src/schema/zod/schemas/searchByTextQuery.ts index c00db23c..acea8967 100644 --- a/packages/api/src/schema/zod/schemas/searchByTextQuery.ts +++ b/packages/api/src/schema/zod/schemas/searchByTextQuery.ts @@ -33,8 +33,7 @@ export const SearchByTextQuerySchema = z return value.trim().split(/\s+/).join('&') // Replace spaces with '&' for tsquery compatibility }) .describe('Case-insensitive search query') - .openapi({ example: 'eigen' }), - legacy: z.enum(['true', 'false']).default('true').openapi({ example: 'false' }) + .openapi({ example: 'eigen' }) }) .refine( (data) => { diff --git a/packages/api/src/utils/baseApys.ts b/packages/api/src/utils/baseApys.ts index 3c594216..753db608 100644 --- a/packages/api/src/utils/baseApys.ts +++ b/packages/api/src/utils/baseApys.ts @@ -121,7 +121,8 @@ export async function fetchBaseApys(): Promise { } const latestEntry = data.data[data.data.length - 1] - const apyBase = Number(latestEntry.apyBase) || 0 + const apyBase = + Number(latestEntry?.apyBase7d || latestEntry?.apyBase || latestEntry?.apy) || 0 // Cache APY with random TTL const randomHour = Math.floor(Math.random() * (maxHours - minHours + 1)) + minHours // Random hour: 12 to 24 diff --git a/packages/api/src/utils/operatorShares.ts b/packages/api/src/utils/operatorShares.ts index d2cc7b24..ba92c839 100644 --- a/packages/api/src/utils/operatorShares.ts +++ b/packages/api/src/utils/operatorShares.ts @@ -6,10 +6,11 @@ export function withOperatorShares(avsOperators) { const sharesMap: IMap = new Map() avsOperators.map((avsOperator) => { + // TODO: Add back with operator set strategies + // TODO: Select whether to use operator set strategies or all strategies + // const shares = avsOperator.operator.shares.filter((s) => true) const shares = avsOperator.operator.shares.filter( - (s) => true - // TODO: Add back with operator set strategies - // avsOperator.restakedStrategies.indexOf(s.strategyAddress.toLowerCase()) !== -1 + (s) => avsOperator.restakedStrategies.indexOf(s.strategyAddress.toLowerCase()) !== -1 ) shares.map((s) => { diff --git a/packages/seeder/src/monitors/avsMetrics.ts b/packages/seeder/src/monitors/avsMetrics.ts index 839eda59..1d375902 100644 --- a/packages/seeder/src/monitors/avsMetrics.ts +++ b/packages/seeder/src/monitors/avsMetrics.ts @@ -67,15 +67,16 @@ export async function monitorAvsMetrics(params: MonitorAvsMetricsParams) { where: { operatorAddress: { in: avs.operators.map((o) => o.operatorAddress) - }, - shares: { - some: { - strategyAddress: { - in: avs.restakeableStrategies - }, - shares: { gt: '0' } - } } + // TODO: Add back with operator set strategies + // shares: { + // some: { + // strategyAddress: { + // in: avs.restakeableStrategies + // }, + // shares: { gt: '0' } + // } + // } } })