From 55fce57b0c820dcffb2f1ae5bc1c88c079b5a534 Mon Sep 17 00:00:00 2001 From: Udit Veerwani <25996904+uditdc@users.noreply.github.com> Date: Tue, 22 Oct 2024 14:12:57 +0530 Subject: [PATCH 01/17] Remove rate limits on health route (#270) --- packages/api/src/index.ts | 3 +-- packages/api/src/routes/index.ts | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index b1881637..547fb009 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -8,7 +8,6 @@ import helmet from 'helmet' import cors from 'cors' import apiRouter from './routes' import { EigenExplorerApiError, handleAndReturnErrorResponse } from './schema/errors' -import { rateLimiter } from './utils/auth' const PORT = process.env.PORT ? Number.parseInt(process.env.PORT) : 3002 @@ -24,7 +23,7 @@ app.use(express.urlencoded({ extended: false })) app.use(cookieParser()) // Routes -app.use('/', rateLimiter, apiRouter) +app.use('/', apiRouter) app.get('/favicon.ico', (req, res) => res.sendStatus(204)) diff --git a/packages/api/src/routes/index.ts b/packages/api/src/routes/index.ts index a964e8a6..a900ffb1 100644 --- a/packages/api/src/routes/index.ts +++ b/packages/api/src/routes/index.ts @@ -8,6 +8,7 @@ import withdrawalRoutes from './withdrawals/withdrawalRoutes' import depositRoutes from './deposits/depositRoutes' import auxiliaryRoutes from './auxiliary/auxiliaryRoutes' import rewardRoutes from './rewards/rewardRoutes' +import { rateLimiter } from '../utils/auth' const apiRouter = express.Router() @@ -20,14 +21,14 @@ apiRouter.get('/version', (_, res) => ) // Remaining routes -apiRouter.use('/avs', avsRoutes) -apiRouter.use('/strategies', strategiesRoutes) -apiRouter.use('/operators', operatorRoutes) -apiRouter.use('/stakers', stakerRoutes) -apiRouter.use('/metrics', metricRoutes) -apiRouter.use('/withdrawals', withdrawalRoutes) -apiRouter.use('/deposits', depositRoutes) -apiRouter.use('/aux', auxiliaryRoutes) -apiRouter.use('/rewards', rewardRoutes) +apiRouter.use('/avs', rateLimiter, avsRoutes) +apiRouter.use('/strategies', rateLimiter, strategiesRoutes) +apiRouter.use('/operators', rateLimiter, operatorRoutes) +apiRouter.use('/stakers', rateLimiter, stakerRoutes) +apiRouter.use('/metrics', rateLimiter, metricRoutes) +apiRouter.use('/withdrawals', rateLimiter, withdrawalRoutes) +apiRouter.use('/deposits', rateLimiter, depositRoutes) +apiRouter.use('/aux', rateLimiter, auxiliaryRoutes) +apiRouter.use('/rewards', rateLimiter, rewardRoutes) export default apiRouter From 14f602d0d640ce5467594d62da8b0eb8307995db Mon Sep 17 00:00:00 2001 From: Gowtham Sundaresan <131300352+gowthamsundaresan@users.noreply.github.com> Date: Tue, 22 Oct 2024 14:13:20 +0530 Subject: [PATCH 02/17] fix: filter inactive avs when monitoring operator apy (#267) --- packages/seeder/src/monitors/operatorApy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/seeder/src/monitors/operatorApy.ts b/packages/seeder/src/monitors/operatorApy.ts index 5f71d32a..f5b2aadb 100644 --- a/packages/seeder/src/monitors/operatorApy.ts +++ b/packages/seeder/src/monitors/operatorApy.ts @@ -30,9 +30,9 @@ export async function monitorOperatorApy() { select: { strategyAddress: true, shares: true } }, avs: { + where: { isActive: true }, select: { avsAddress: true, - isActive: true, avs: { select: { address: true, From fcac90dd51397800612d0b20a938509d7e6e7b18 Mon Sep 17 00:00:00 2001 From: Surbhit Agrawal <82264758+surbhit14@users.noreply.github.com> Date: Fri, 25 Oct 2024 14:08:49 +0530 Subject: [PATCH 03/17] 235 feat operator event history (#263) * Add route * Add Schema and Types * Add core logic for event fetching * Remove the unnecesary code * Add default range and max range * Capitalise the enum types * Add the case insensitive queries * Add optional fields underlyingToken and underlyingValue to response with flag * Move additional fields out of args and lowercase the response fields --- .../routes/operators/operatorController.ts | 222 ++++++++++++++++++ .../src/routes/operators/operatorRoutes.ts | 3 + .../src/schema/zod/schemas/operatorEvents.ts | 162 +++++++++++++ .../schema/zod/schemas/withTokenDataQuery.ts | 12 + 4 files changed, 399 insertions(+) create mode 100644 packages/api/src/schema/zod/schemas/operatorEvents.ts create mode 100644 packages/api/src/schema/zod/schemas/withTokenDataQuery.ts diff --git a/packages/api/src/routes/operators/operatorController.ts b/packages/api/src/routes/operators/operatorController.ts index 5d76443f..8180f1fc 100644 --- a/packages/api/src/routes/operators/operatorController.ts +++ b/packages/api/src/routes/operators/operatorController.ts @@ -6,6 +6,7 @@ import { WithAdditionalDataQuerySchema } from '../../schema/zod/schemas/withAddi import { SortByQuerySchema } from '../../schema/zod/schemas/sortByQuery' import { SearchByTextQuerySchema } from '../../schema/zod/schemas/searchByTextQuery' import { WithRewardsQuerySchema } from '../../schema/zod/schemas/withRewardsQuery' +import { OperatorEventQuerySchema } from '../../schema/zod/schemas/operatorEvents' import { handleAndReturnErrorResponse } from '../../schema/errors' import { getStrategiesWithShareUnderlying, @@ -16,6 +17,24 @@ import { withOperatorShares } from '../../utils/operatorShares' import Prisma from '@prisma/client' import prisma from '../../utils/prismaClient' import { fetchTokenPrices } from '../../utils/tokenPrices' +import { WithTokenDataQuerySchema } from '../../schema/zod/schemas/withTokenDataQuery' +import { getSharesToUnderlying } from '../../../../seeder/src/utils/strategies' + +type EventRecordArgs = { + staker: string + strategy?: string + shares?: number +} + +type EventRecord = { + type: 'SHARES_INCREASED' | 'SHARES_DECREASED' | 'DELEGATION' | 'UNDELEGATION' + tx: string + blockNumber: number + blockTime: Date + args: EventRecordArgs + underlyingToken?: string + underlyingValue?: number +} /** * Function for route /operators @@ -370,6 +389,106 @@ export async function getOperatorRewards(req: Request, res: Response) { } } +/** + * Function for route /operators/:address/events + * Fetches and returns a list of events for a specific operator with optional filters + * + * @param req + * @param res + */ +export async function getOperatorEvents(req: Request, res: Response) { + const result = OperatorEventQuerySchema.and(WithTokenDataQuerySchema) + .and(PaginationQuerySchema) + .safeParse(req.query) + if (!result.success) { + return handleAndReturnErrorResponse(req, res, result.error) + } + + try { + const { + type, + stakerAddress, + strategyAddress, + txHash, + startAt, + endAt, + withTokenData, + skip, + take + } = result.data + const { address } = req.params + + const baseFilterQuery = { + operator: { + contains: address, + mode: 'insensitive' + }, + ...(stakerAddress && { + staker: { + contains: stakerAddress, + mode: 'insensitive' + } + }), + ...(strategyAddress && { + strategy: { + contains: strategyAddress, + mode: 'insensitive' + } + }), + ...(txHash && { + transactionHash: { + contains: txHash, + mode: 'insensitive' + } + }), + blockTime: { + gte: new Date(startAt), + ...(endAt ? { lte: new Date(endAt) } : {}) + } + } + + let eventRecords: EventRecord[] = [] + let eventCount = 0 + + const eventTypesToFetch = type + ? [type] + : strategyAddress + ? ['SHARES_INCREASED', 'SHARES_DECREASED'] + : ['SHARES_INCREASED', 'SHARES_DECREASED', 'DELEGATION', 'UNDELEGATION'] + + const fetchEventsForTypes = async (types: string[]) => { + const results = await Promise.all( + types.map((eventType) => + fetchAndMapEvents(eventType, baseFilterQuery, withTokenData, skip, take) + ) + ) + return results + } + + const results = await fetchEventsForTypes(eventTypesToFetch) + + eventRecords = results.flatMap((result) => result.eventRecords) + eventRecords = eventRecords + .sort((a, b) => (b.blockNumber > a.blockNumber ? 1 : -1)) + .slice(0, take) + + eventCount = results.reduce((acc, result) => acc + result.eventCount, 0) + + const response = { + data: eventRecords, + meta: { + total: eventCount, + skip, + take + } + } + + res.send(response) + } catch (error) { + handleAndReturnErrorResponse(req, res, error) + } +} + /** * Function for route /operators/:address/invalidate-metadata * Protected route to invalidate the metadata of a given Operator @@ -555,3 +674,106 @@ async function calculateOperatorApy(operator: any) { return response } catch {} } + +/** + * Utility function to fetch and map event records from the database. + * + * @param eventType + * @param baseFilterQuery + * @param skip + * @param take + * @returns + */ +async function fetchAndMapEvents( + eventType: string, + baseFilterQuery: any, + withTokenData: boolean, + skip: number, + take: number +): Promise<{ eventRecords: EventRecord[]; eventCount: number }> { + const modelName = (() => { + switch (eventType) { + case 'SHARES_INCREASED': + return 'eventLogs_OperatorSharesIncreased' + case 'SHARES_DECREASED': + return 'eventLogs_OperatorSharesDecreased' + case 'DELEGATION': + return 'eventLogs_StakerDelegated' + case 'UNDELEGATION': + return 'eventLogs_StakerUndelegated' + default: + throw new Error(`Unknown event type: ${eventType}`) + } + })() + + const model = prisma[modelName] as any + + const eventCount = await model.count({ + where: baseFilterQuery + }) + + const eventLogs = await model.findMany({ + where: baseFilterQuery, + skip, + take, + orderBy: { blockNumber: 'desc' } + }) + + const tokenPrices = withTokenData ? await fetchTokenPrices() : undefined + const sharesToUnderlying = withTokenData ? await getSharesToUnderlying() : undefined + + const eventRecords = await Promise.all( + eventLogs.map(async (event) => { + let underlyingToken: string | undefined + let underlyingValue: number | undefined + + if ( + withTokenData && + (eventType === 'SHARES_INCREASED' || eventType === 'SHARES_DECREASED') && + event.strategy + ) { + const strategy = await prisma.strategies.findUnique({ + where: { + address: event.strategy.toLowerCase() + } + }) + + if (strategy && sharesToUnderlying) { + underlyingToken = strategy.underlyingToken + const sharesMultiplier = Number(sharesToUnderlying.get(event.strategy.toLowerCase())) + const strategyTokenPrice = tokenPrices?.find( + (tp) => tp.address.toLowerCase() === strategy.underlyingToken.toLowerCase() + ) + + if (sharesMultiplier && strategyTokenPrice) { + underlyingValue = + (Number(event.shares) / Math.pow(10, strategyTokenPrice.decimals)) * + sharesMultiplier * + strategyTokenPrice.ethPrice + } + } + } + + return { + type: eventType, + tx: event.transactionHash, + blockNumber: event.blockNumber, + blockTime: event.blockTime, + args: { + staker: event.staker.toLowerCase(), + strategy: event.strategy?.toLowerCase(), + shares: event.shares + }, + ...(withTokenData && { + underlyingToken: underlyingToken?.toLowerCase(), + underlyingValue + }) + } + }) + ) + + return { + eventRecords, + eventCount + } +} diff --git a/packages/api/src/routes/operators/operatorRoutes.ts b/packages/api/src/routes/operators/operatorRoutes.ts index 13ae1643..218a781e 100644 --- a/packages/api/src/routes/operators/operatorRoutes.ts +++ b/packages/api/src/routes/operators/operatorRoutes.ts @@ -4,6 +4,7 @@ import { getOperator, getAllOperatorAddresses, getOperatorRewards, + getOperatorEvents, invalidateMetadata } from './operatorController' import { authenticateJWT } from '../../utils/jwtUtils' @@ -108,6 +109,8 @@ router.get('/:address', routeCache.cacheSeconds(120), getOperator) router.get('/:address/rewards', routeCache.cacheSeconds(120), getOperatorRewards) +router.get('/:address/events', routeCache.cacheSeconds(120), getOperatorEvents) + // Protected routes router.get( '/:address/invalidate-metadata', diff --git a/packages/api/src/schema/zod/schemas/operatorEvents.ts b/packages/api/src/schema/zod/schemas/operatorEvents.ts new file mode 100644 index 00000000..57838b90 --- /dev/null +++ b/packages/api/src/schema/zod/schemas/operatorEvents.ts @@ -0,0 +1,162 @@ +import z from '../' + +const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ +const yyyymmddRegex = /^\d{4}-\d{2}-\d{2}$/ +const maxDuration = 30 * 24 * 60 * 60 * 1000 // 30 days +const defaultDuration = 7 * 24 * 60 * 60 * 1000 // 7 days + +/** + * Validates that the given time range doesn't exceed the max allowed duration. + * + * @param startAt + * @param endAt + * @returns + */ +const validateDateRange = (startAt: string, endAt: string) => { + const start = new Date(startAt) + const end = new Date(endAt || new Date()) + const durationMs = end.getTime() - start.getTime() + return durationMs <= maxDuration +} + +/** + * Utility to get default dates if not provided. + * Default to last 7 days + * + * @param startAt + * @param endAt + * @returns + */ +const getValidatedDates = (startAt?: string, endAt?: string) => { + const now = new Date() + + if (!startAt && !endAt) { + return { + startAt: new Date(now.getTime() - defaultDuration).toISOString(), + endAt: null + } + } + + if (startAt && !endAt) { + const start = new Date(startAt) + return { + startAt, + endAt: new Date(Math.min(start.getTime() + defaultDuration, now.getTime())).toISOString() + } + } + + if (!startAt && endAt) { + const end = new Date(endAt) + return { + startAt: new Date(end.getTime() - defaultDuration).toISOString(), + endAt + } + } + + return { startAt, endAt } +} + +export const OperatorEventQuerySchema = z + .object({ + stakerAddress: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address') + .optional() + .describe('The address of the staker') + .openapi({ example: '0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd' }), + + strategyAddress: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address') + .optional() + .describe('The contract address of the restaking strategy') + .openapi({ example: '0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6' }), + + txHash: z + .string() + .regex(/^0x([A-Fa-f0-9]{64})$/, 'Invalid transaction hash') + .optional() + .describe('The transaction hash associated with the event') + .openapi({ example: '0xe95a203b1a91a908f9b9ce46459d101078c2c3cb' }), + + type: z + .enum(['SHARES_INCREASED', 'SHARES_DECREASED', 'DELEGATION', 'UNDELEGATION']) + .optional() + .describe('The type of the operator event') + .openapi({ example: 'SHARES_INCREASED' }), + + startAt: z + .string() + .optional() + .refine( + (val) => + !val || + ((isoRegex.test(val) || yyyymmddRegex.test(val)) && + !Number.isNaN(new Date(val).getTime())), + { + message: 'Invalid date format for startAt. Use YYYY-MM-DD or ISO 8601 format.' + } + ) + .default('') + .describe('Start date in ISO string format') + .openapi({ example: '2024-04-11T08:31:11.000' }), + + endAt: z + .string() + .optional() + .refine( + (val) => + !val || + ((isoRegex.test(val) || yyyymmddRegex.test(val)) && + !Number.isNaN(new Date(val).getTime())), + { + message: 'Invalid date format for endAt. Use YYYY-MM-DD or ISO 8601 format.' + } + ) + .default('') + .describe('End date in ISO string format') + .openapi({ example: '2024-04-12T08:31:11.000' }) + }) + .refine( + (data) => { + if ((data.type === 'DELEGATION' || data.type === 'UNDELEGATION') && data.strategyAddress) { + return false + } + return true + }, + { + message: + 'strategyAddress filter is not supported for DELEGATION or UNDELEGATION event types.', + path: ['strategyAddress'] + } + ) + .refine( + (data) => { + if (data.startAt && data.endAt) { + return new Date(data.endAt).getTime() >= new Date(data.startAt).getTime() + } + return true + }, + { + message: 'endAt must be after startAt', + path: ['endAt'] + } + ) + .refine( + (data) => { + try { + const dates = getValidatedDates(data.startAt, data.endAt) + Object.assign(data, dates) + + return validateDateRange(data.startAt, data.endAt) + } catch { + return false + } + }, + { + message: 'Duration between startAt and endAt exceeds the allowed limit of 30 days.', + path: ['startAt', 'endAt'] + } + ) + +export default OperatorEventQuerySchema diff --git a/packages/api/src/schema/zod/schemas/withTokenDataQuery.ts b/packages/api/src/schema/zod/schemas/withTokenDataQuery.ts new file mode 100644 index 00000000..520a6eee --- /dev/null +++ b/packages/api/src/schema/zod/schemas/withTokenDataQuery.ts @@ -0,0 +1,12 @@ +import z from '..' + +export const WithTokenDataQuerySchema = z.object({ + withTokenData: z + .enum(['true', 'false']) + .default('false') + .describe( + 'Toggle whether the route should return underlying token address and underlying value' + ) + .transform((val) => val === 'true') + .openapi({ example: 'false' }) +}) From 601615c9dad01867a37bfd0b1e5ea6b5e98a3e1d Mon Sep 17 00:00:00 2001 From: Surbhit Agrawal <82264758+surbhit14@users.noreply.github.com> Date: Fri, 25 Oct 2024 20:53:06 +0530 Subject: [PATCH 04/17] Operator Event API Patch (#274) * Add /delegation to route path * Use getStrategiesWithShareUnderlying * Modify share calculation logic * Changes to add withEthValue flag which returns ethValue * Add check for sharesUnderlying.ethPrice and remove Number typecast --------- Co-authored-by: Udit <25996904+uditdc@users.noreply.github.com> --- .../routes/operators/operatorController.ts | 40 +++++++++++-------- .../src/routes/operators/operatorRoutes.ts | 2 +- .../src/schema/zod/schemas/operatorEvents.ts | 15 +++++++ .../schema/zod/schemas/withTokenDataQuery.ts | 9 +++++ 4 files changed, 48 insertions(+), 18 deletions(-) diff --git a/packages/api/src/routes/operators/operatorController.ts b/packages/api/src/routes/operators/operatorController.ts index 8180f1fc..dc178cb5 100644 --- a/packages/api/src/routes/operators/operatorController.ts +++ b/packages/api/src/routes/operators/operatorController.ts @@ -17,8 +17,6 @@ import { withOperatorShares } from '../../utils/operatorShares' import Prisma from '@prisma/client' import prisma from '../../utils/prismaClient' import { fetchTokenPrices } from '../../utils/tokenPrices' -import { WithTokenDataQuerySchema } from '../../schema/zod/schemas/withTokenDataQuery' -import { getSharesToUnderlying } from '../../../../seeder/src/utils/strategies' type EventRecordArgs = { staker: string @@ -34,6 +32,7 @@ type EventRecord = { args: EventRecordArgs underlyingToken?: string underlyingValue?: number + ethValue?: number } /** @@ -397,9 +396,7 @@ export async function getOperatorRewards(req: Request, res: Response) { * @param res */ export async function getOperatorEvents(req: Request, res: Response) { - const result = OperatorEventQuerySchema.and(WithTokenDataQuerySchema) - .and(PaginationQuerySchema) - .safeParse(req.query) + const result = OperatorEventQuerySchema.and(PaginationQuerySchema).safeParse(req.query) if (!result.success) { return handleAndReturnErrorResponse(req, res, result.error) } @@ -413,6 +410,7 @@ export async function getOperatorEvents(req: Request, res: Response) { startAt, endAt, withTokenData, + withEthValue, skip, take } = result.data @@ -459,7 +457,7 @@ export async function getOperatorEvents(req: Request, res: Response) { const fetchEventsForTypes = async (types: string[]) => { const results = await Promise.all( types.map((eventType) => - fetchAndMapEvents(eventType, baseFilterQuery, withTokenData, skip, take) + fetchAndMapEvents(eventType, baseFilterQuery, withTokenData, withEthValue, skip, take) ) ) return results @@ -688,6 +686,7 @@ async function fetchAndMapEvents( eventType: string, baseFilterQuery: any, withTokenData: boolean, + withEthValue: boolean, skip: number, take: number ): Promise<{ eventRecords: EventRecord[]; eventCount: number }> { @@ -719,13 +718,15 @@ async function fetchAndMapEvents( orderBy: { blockNumber: 'desc' } }) - const tokenPrices = withTokenData ? await fetchTokenPrices() : undefined - const sharesToUnderlying = withTokenData ? await getSharesToUnderlying() : undefined + const strategiesWithSharesUnderlying = withTokenData + ? await getStrategiesWithShareUnderlying() + : undefined const eventRecords = await Promise.all( eventLogs.map(async (event) => { let underlyingToken: string | undefined let underlyingValue: number | undefined + let ethValue: number | undefined if ( withTokenData && @@ -738,18 +739,22 @@ async function fetchAndMapEvents( } }) - if (strategy && sharesToUnderlying) { + if (strategy && strategiesWithSharesUnderlying) { underlyingToken = strategy.underlyingToken - const sharesMultiplier = Number(sharesToUnderlying.get(event.strategy.toLowerCase())) - const strategyTokenPrice = tokenPrices?.find( - (tp) => tp.address.toLowerCase() === strategy.underlyingToken.toLowerCase() + + const sharesUnderlying = strategiesWithSharesUnderlying.find( + (s) => s.strategyAddress.toLowerCase() === event.strategy.toLowerCase() ) - if (sharesMultiplier && strategyTokenPrice) { + if (sharesUnderlying) { underlyingValue = - (Number(event.shares) / Math.pow(10, strategyTokenPrice.decimals)) * - sharesMultiplier * - strategyTokenPrice.ethPrice + Number( + (BigInt(event.shares) * BigInt(sharesUnderlying.sharesToUnderlying)) / BigInt(1e18) + ) / 1e18 + + if (withEthValue && sharesUnderlying.ethPrice) { + ethValue = underlyingValue * sharesUnderlying.ethPrice + } } } } @@ -767,7 +772,8 @@ async function fetchAndMapEvents( ...(withTokenData && { underlyingToken: underlyingToken?.toLowerCase(), underlyingValue - }) + }), + ...(withEthValue && { ethValue }) } }) ) diff --git a/packages/api/src/routes/operators/operatorRoutes.ts b/packages/api/src/routes/operators/operatorRoutes.ts index 218a781e..43053c45 100644 --- a/packages/api/src/routes/operators/operatorRoutes.ts +++ b/packages/api/src/routes/operators/operatorRoutes.ts @@ -109,7 +109,7 @@ router.get('/:address', routeCache.cacheSeconds(120), getOperator) router.get('/:address/rewards', routeCache.cacheSeconds(120), getOperatorRewards) -router.get('/:address/events', routeCache.cacheSeconds(120), getOperatorEvents) +router.get('/:address/events/delegation', routeCache.cacheSeconds(120), getOperatorEvents) // Protected routes router.get( diff --git a/packages/api/src/schema/zod/schemas/operatorEvents.ts b/packages/api/src/schema/zod/schemas/operatorEvents.ts index 57838b90..21063d09 100644 --- a/packages/api/src/schema/zod/schemas/operatorEvents.ts +++ b/packages/api/src/schema/zod/schemas/operatorEvents.ts @@ -1,4 +1,5 @@ import z from '../' +import { WithTokenDataQuerySchema, WithEthValueQuerySchema } from './withTokenDataQuery' const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ const yyyymmddRegex = /^\d{4}-\d{2}-\d{2}$/ @@ -117,6 +118,20 @@ export const OperatorEventQuerySchema = z .describe('End date in ISO string format') .openapi({ example: '2024-04-12T08:31:11.000' }) }) + .merge(WithTokenDataQuerySchema) + .merge(WithEthValueQuerySchema) + .refine( + (data) => { + if (data.withEthValue && !data.withTokenData) { + return false + } + return true + }, + { + message: "'withEthValue' requires 'withTokenData' to be enabled.", + path: ['withEthValue'] + } + ) .refine( (data) => { if ((data.type === 'DELEGATION' || data.type === 'UNDELEGATION') && data.strategyAddress) { diff --git a/packages/api/src/schema/zod/schemas/withTokenDataQuery.ts b/packages/api/src/schema/zod/schemas/withTokenDataQuery.ts index 520a6eee..2048ceb9 100644 --- a/packages/api/src/schema/zod/schemas/withTokenDataQuery.ts +++ b/packages/api/src/schema/zod/schemas/withTokenDataQuery.ts @@ -10,3 +10,12 @@ export const WithTokenDataQuerySchema = z.object({ .transform((val) => val === 'true') .openapi({ example: 'false' }) }) + +export const WithEthValueQuerySchema = z.object({ + withEthValue: z + .enum(['true', 'false']) + .default('false') + .describe('Toggle whether the route should return value denominated in ETH') + .transform((val) => val === 'true') + .openapi({ example: 'false' }) +}) From e4ef171ff3c3c93147f8ea5e524e36005fb1de0e Mon Sep 17 00:00:00 2001 From: Gowtham Sundaresan <131300352+gowthamsundaresan@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:07:24 +0530 Subject: [PATCH 05/17] fix: treat staker address input as case insensitive (#279) --- packages/api/src/routes/stakers/stakerController.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/api/src/routes/stakers/stakerController.ts b/packages/api/src/routes/stakers/stakerController.ts index 8896c2f5..53db47eb 100644 --- a/packages/api/src/routes/stakers/stakerController.ts +++ b/packages/api/src/routes/stakers/stakerController.ts @@ -182,7 +182,7 @@ export async function getStakerWithdrawalsQueued(req: Request, res: Response) { try { const { address } = req.params - const filterQuery = { stakerAddress: address, completedWithdrawal: null } + const filterQuery = { stakerAddress: address.toLowerCase(), completedWithdrawal: null } const withdrawalCount = await prisma.withdrawalQueued.count({ where: filterQuery @@ -245,7 +245,7 @@ export async function getStakerWithdrawalsWithdrawable(req: Request, res: Respon (await viemClient.getBlockNumber()) - BigInt((minDelayBlocks?.value as string) || 0) const filterQuery = { - stakerAddress: address, + stakerAddress: address.toLowerCase(), completedWithdrawal: null, createdAtBlock: { lte: minDelayBlock } } @@ -303,7 +303,7 @@ export async function getStakerWithdrawalsCompleted(req: Request, res: Response) try { const { address } = req.params const filterQuery = { - stakerAddress: address, + stakerAddress: address.toLowerCase(), NOT: { completedWithdrawal: null } @@ -367,7 +367,7 @@ export async function getStakerDeposits(req: Request, res: Response) { try { const { address } = req.params - const filterQuery = { stakerAddress: address } + const filterQuery = { stakerAddress: address.toLowerCase() } const depositCount = await prisma.deposit.count({ where: filterQuery From 101efc8a953482bdb48e9c87162f46642effe7b2 Mon Sep 17 00:00:00 2001 From: Gowtham Sundaresan <131300352+gowthamsundaresan@users.noreply.github.com> Date: Tue, 3 Dec 2024 10:43:39 +0530 Subject: [PATCH 06/17] feat: add active flag for filtering stakers with shares > 0 (#284) --- .../src/routes/stakers/stakerController.ts | 34 +++++++++++++++++-- .../api/src/schema/zod/schemas/activeQuery.ts | 9 +++++ 2 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 packages/api/src/schema/zod/schemas/activeQuery.ts diff --git a/packages/api/src/routes/stakers/stakerController.ts b/packages/api/src/routes/stakers/stakerController.ts index 53db47eb..8d25257f 100644 --- a/packages/api/src/routes/stakers/stakerController.ts +++ b/packages/api/src/routes/stakers/stakerController.ts @@ -7,6 +7,7 @@ import { getViemClient } from '../../viem/viemClient' import { getStrategiesWithShareUnderlying, sharesToTVL } from '../../utils/strategyShares' import { EthereumAddressSchema } from '../../schema/zod/schemas/base/ethereumAddress' import { UpdatedSinceQuerySchema } from '../../schema/zod/schemas/updatedSinceQuery' +import { ActiveQuerySchema } from '../../schema/zod/schemas/activeQuery' /** * Route to get a list of all stakers @@ -18,23 +19,50 @@ export async function getAllStakers(req: Request, res: Response) { // Validate pagination query const result = PaginationQuerySchema.and(WithTvlQuerySchema) .and(UpdatedSinceQuerySchema) + .and(ActiveQuerySchema) .safeParse(req.query) if (!result.success) { return handleAndReturnErrorResponse(req, res, result.error) } - const { skip, take, withTvl, updatedSince } = result.data + const { skip, take, withTvl, updatedSince, active } = result.data try { // Fetch count and record const stakersCount = await prisma.staker.count({ - where: updatedSince ? { updatedAt: { gte: new Date(updatedSince) } } : {} + where: { + ...(updatedSince ? { updatedAt: { gte: new Date(updatedSince) } } : {}), + ...(active + ? { + shares: { + some: { + shares: { + not: '0' + } + } + } + } + : {}) + } }) const stakersRecords = await prisma.staker.findMany({ skip, take, - where: updatedSince ? { updatedAt: { gte: new Date(updatedSince) } } : {}, + where: { + ...(updatedSince ? { updatedAt: { gte: new Date(updatedSince) } } : {}), + ...(active + ? { + shares: { + some: { + shares: { + not: '0' + } + } + } + } + : {}) + }, include: { shares: { select: { strategyAddress: true, shares: true } diff --git a/packages/api/src/schema/zod/schemas/activeQuery.ts b/packages/api/src/schema/zod/schemas/activeQuery.ts new file mode 100644 index 00000000..a9ad1f80 --- /dev/null +++ b/packages/api/src/schema/zod/schemas/activeQuery.ts @@ -0,0 +1,9 @@ +import z from '../' + +export const ActiveQuerySchema = z.object({ + active: z + .enum(['true', 'false']) + .optional() + .describe('Fetch only those stakers with shares > 0') + .openapi({ example: 'true' }) +}) From b7903a750f529cf2ec72625864a1266d4cfe97c0 Mon Sep 17 00:00:00 2001 From: Surbhit Agrawal <82264758+surbhit14@users.noreply.github.com> Date: Tue, 3 Dec 2024 10:44:07 +0530 Subject: [PATCH 07/17] Update documentation v0.3.3 (#278) * Keep openapi code insync with openapi.json * Add stakerAddress to avs strategy response * Include github and token address in avs curated metadata * Add and modify avs/operator all addresses route * Add withRewads parameter to endpoints * Add rewards section amd rewards/strategies route * Change strategy name to strategy address * Change example value for searching * Modify response for getAvsOperatorsByAddress * Remove rewards from all AVS endpoint response * Add rewards route for AVS and Operator * Clean up and operator directory changes in openapi response * Add operatotWithRewardsResponseSchema * Rewards response addition and changes for openApi * Modify examples * Modify description of fields --- .../schema/zod/schemas/base/avsMetaData.ts | 14 +- .../schema/zod/schemas/searchByTextQuery.ts | 2 +- .../zod/schemas/separateSearchQueries.ts | 2 +- packages/openapi/openapi.json | 1350 +++++++++++++++-- .../avs/avsAddressResponse.ts | 8 +- .../avs/avsOperatorResponse.ts | 13 +- .../src/apiResponseSchema/avs/avsResponse.ts | 14 +- .../avs/avsRewardsResponse.ts | 79 + .../avs/avsStakerResponse.ts | 60 + .../avs/avsWithRewardsResponse.ts | 22 + .../base/rewardTokensResponse.ts | 22 + .../apiResponseSchema/base/rewardsResponse.ts | 20 + .../deposits/depositsResponseSchema.ts | 2 +- .../operator/operatorAddressResponse.ts | 13 + .../{ => operator}/operatorResponse.ts | 14 +- .../operator/operatorRewardsResponse.ts | 26 + .../operator/operatorWithRewardsResponse.ts | 297 ++++ .../src/apiResponseSchema/stakerResponse.ts | 5 +- .../withdrawals/withdrawalsResponseSchema.ts | 12 - packages/openapi/src/documentBase.ts | 4 +- .../openapi/src/routes/avs/getAvsByAddress.ts | 11 +- .../routes/avs/getAvsOperatorsByAddress.ts | 1 - .../openapi/src/routes/avs/getAvsRewards.ts | 32 + .../src/routes/avs/getAvsStakersByAddress.ts | 4 +- packages/openapi/src/routes/avs/index.ts | 4 +- .../historical/getHistoricalTvlRestaking.ts | 8 +- .../src/routes/metrics/getTvlByStrategy.ts | 11 +- .../operators/getAllOperatorAddresses.ts | 42 + .../src/routes/operators/getAllOperators.ts | 2 +- .../routes/operators/getOperatorByAddress.ts | 6 +- .../routes/operators/getOperatorRewards.ts | 33 + .../openapi/src/routes/operators/index.ts | 6 +- .../src/routes/rewards/getStrategies.ts | 23 + packages/openapi/src/routes/rewards/index.ts | 6 + 34 files changed, 2022 insertions(+), 146 deletions(-) create mode 100644 packages/openapi/src/apiResponseSchema/avs/avsRewardsResponse.ts create mode 100644 packages/openapi/src/apiResponseSchema/avs/avsStakerResponse.ts create mode 100644 packages/openapi/src/apiResponseSchema/avs/avsWithRewardsResponse.ts create mode 100644 packages/openapi/src/apiResponseSchema/base/rewardTokensResponse.ts create mode 100644 packages/openapi/src/apiResponseSchema/base/rewardsResponse.ts create mode 100644 packages/openapi/src/apiResponseSchema/operator/operatorAddressResponse.ts rename packages/openapi/src/apiResponseSchema/{ => operator}/operatorResponse.ts (83%) create mode 100644 packages/openapi/src/apiResponseSchema/operator/operatorRewardsResponse.ts create mode 100644 packages/openapi/src/apiResponseSchema/operator/operatorWithRewardsResponse.ts create mode 100644 packages/openapi/src/routes/avs/getAvsRewards.ts create mode 100644 packages/openapi/src/routes/operators/getAllOperatorAddresses.ts create mode 100644 packages/openapi/src/routes/operators/getOperatorRewards.ts create mode 100644 packages/openapi/src/routes/rewards/getStrategies.ts create mode 100644 packages/openapi/src/routes/rewards/index.ts diff --git a/packages/api/src/schema/zod/schemas/base/avsMetaData.ts b/packages/api/src/schema/zod/schemas/base/avsMetaData.ts index 97827603..59b03b5d 100644 --- a/packages/api/src/schema/zod/schemas/base/avsMetaData.ts +++ b/packages/api/src/schema/zod/schemas/base/avsMetaData.ts @@ -31,5 +31,17 @@ export const AvsMetaDataSchema = z.object({ .url() .nullable() .describe("The URL of the AVS's X") - .openapi({ example: 'https://twitter.com/acme' }) + .openapi({ example: 'https://twitter.com/acme' }), + metadataGithub: z + .string() + .url() + .nullable() + .describe("The URL of the AVS's Github") + .openapi({ example: 'https://github.com/acme' }), + metadataTokenAddress: z + .string() + .url() + .nullable() + .describe('The Token Address of the AVS') + .openapi({ example: '0x2344c0fe02ccd2b32155ca0ffcb1978a6d96a552' }) }) diff --git a/packages/api/src/schema/zod/schemas/searchByTextQuery.ts b/packages/api/src/schema/zod/schemas/searchByTextQuery.ts index b3391c00..acea8967 100644 --- a/packages/api/src/schema/zod/schemas/searchByTextQuery.ts +++ b/packages/api/src/schema/zod/schemas/searchByTextQuery.ts @@ -33,7 +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: 'blockless' }) + .openapi({ example: 'eigen' }) }) .refine( (data) => { diff --git a/packages/api/src/schema/zod/schemas/separateSearchQueries.ts b/packages/api/src/schema/zod/schemas/separateSearchQueries.ts index e5436714..44e80250 100644 --- a/packages/api/src/schema/zod/schemas/separateSearchQueries.ts +++ b/packages/api/src/schema/zod/schemas/separateSearchQueries.ts @@ -14,5 +14,5 @@ export const SearchByText = z.object({ .string() .optional() .describe('Case-insensitive search query') - .openapi({ example: 'blockless' }) + .openapi({ example: 'eigen' }) }) diff --git a/packages/openapi/openapi.json b/packages/openapi/openapi.json index b2048a42..ea1b2eef 100644 --- a/packages/openapi/openapi.json +++ b/packages/openapi/openapi.json @@ -495,18 +495,19 @@ "/metrics/tvl/restaking/{strategy}": { "get": { "operationId": "getTvlRestakingMetricByStrategy", - "summary": "Retrieve a strategy TVL by name", + "summary": "Retrieve a strategy TVL by address", "description": "Returns the total value locked (TVL) in a specific LST strategy.", "tags": ["Metrics"], "parameters": [ { "in": "path", "name": "strategy", - "description": "The name of the restaking strategy", + "description": "The address of the restaking strategy", "schema": { "type": "string", - "description": "The name of the restaking strategy", - "example": "cbETH" + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the restaking strategy", + "example": "0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6" }, "required": true }, @@ -1854,11 +1855,11 @@ { "in": "path", "name": "address", - "description": "The address of the strategy", + "description": "The contract address of the restaking strategy", "schema": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The address of the strategy", + "description": "The contract address of the restaking strategy", "example": "0x54945180dB7943c0ed0FEE7EdaB2Bd24620256bc" }, "required": true @@ -2786,7 +2787,7 @@ "schema": { "type": "string", "description": "Case-insensitive search query", - "example": "blockless" + "example": "eigen" } }, { @@ -2933,7 +2934,7 @@ "apy": { "type": "string", "description": "The latest APY recorded for the AVS", - "example": "1.302" + "example": "1.0" }, "createdAtBlock": { "type": "string", @@ -3003,6 +3004,20 @@ "description": "The URL of the AVS's X", "example": "https://twitter.com/acme" }, + "metadataGithub": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's Github", + "example": "https://github.com/acme" + }, + "metadataTokenAddress": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The Token Address of the AVS", + "example": "0x2344c0fe02ccd2b32155ca0ffcb1978a6d96a552" + }, "avsAddress": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$", @@ -3036,6 +3051,8 @@ "metadataTelegram", "metadataWebsite", "metadataX", + "metadataGithub", + "metadataTokenAddress", "avsAddress", "tags", "isVisible", @@ -3046,11 +3063,13 @@ "avsAddress": "0x2344c0fe02ccd2b32155ca0ffcb1978a6d96a552", "metadataName": "Example AVS", "metadataDescription": "This is an example AVS", - "metadataDiscord": "https://discord.com/invite/abcdefghij", + "metadataDiscord": "https://discord.com/invite/example", "metadataLogo": "The URL of the AVS's logo", "metadataTelegram": "The URL of the AVS's Telegram channel", - "metadataWebsite": "https://acme.com", - "metadataX": "https://twitter.com/acme", + "metadataWebsite": "https://example.com", + "metadataX": "https://twitter.com/example", + "metadataGithub": "https://github.com/example", + "metadataTokenAddress": "0x2344c0fe02ccd2b32155ca0ffcb1978a6d96a552", "tags": ["Example tag 1", "Example tag 2"], "isVisible": true, "isVerified": true @@ -3079,11 +3098,11 @@ "example": [ { "strategyAddress": "0x93c4b944d05dfe6df7645a86cd2206016c51564d", - "shares": "135064894598947935263152" + "shares": "1000000000000000000000" }, { "strategyAddress": "0x54945180db7943c0ed0fee7edab2bd24620256bc", - "shares": "9323641881708650182301" + "shares": "1000000000000000000000" } ] }, @@ -3259,7 +3278,7 @@ "schema": { "type": "string", "description": "Case-insensitive search query", - "example": "blockless" + "example": "eigen" } }, { @@ -3298,19 +3317,24 @@ "items": { "type": "object", "properties": { + "address": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "AVS service manager contract address", + "example": "0x35f4f28a8d3ff20eed10e087e8f96ea2641e6aa1" + }, "name": { "type": "string", "description": "The AVS's name", "example": "Example AVS" }, - "address": { + "logo": { "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "AVS service manager contract address", - "example": "0x35f4f28a8d3ff20eed10e087e8f96ea2641e6aa1" + "description": "The AVS's logo URL", + "example": "https://example.avs/logo.png" } }, - "required": ["name", "address"] + "required": ["address", "name", "logo"] } }, "meta": { @@ -3406,6 +3430,18 @@ "description": "Toggle whether the route should send curated metadata", "example": "false" } + }, + { + "in": "query", + "name": "withRewards", + "description": "Toggle whether the route should return Avs/Operator rewards APY data", + "schema": { + "type": "string", + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should return Avs/Operator rewards APY data", + "example": "false" + } } ], "responses": { @@ -3480,7 +3516,7 @@ "apy": { "type": "string", "description": "The latest APY recorded for the AVS", - "example": "1.302" + "example": "1.0" }, "createdAtBlock": { "type": "string", @@ -3550,6 +3586,20 @@ "description": "The URL of the AVS's X", "example": "https://twitter.com/acme" }, + "metadataGithub": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's Github", + "example": "https://github.com/acme" + }, + "metadataTokenAddress": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The Token Address of the AVS", + "example": "0x2344c0fe02ccd2b32155ca0ffcb1978a6d96a552" + }, "avsAddress": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$", @@ -3583,6 +3633,8 @@ "metadataTelegram", "metadataWebsite", "metadataX", + "metadataGithub", + "metadataTokenAddress", "avsAddress", "tags", "isVisible", @@ -3593,11 +3645,13 @@ "avsAddress": "0x2344c0fe02ccd2b32155ca0ffcb1978a6d96a552", "metadataName": "Example AVS", "metadataDescription": "This is an example AVS", - "metadataDiscord": "https://discord.com/invite/abcdefghij", + "metadataDiscord": "https://discord.com/invite/example", "metadataLogo": "The URL of the AVS's logo", "metadataTelegram": "The URL of the AVS's Telegram channel", - "metadataWebsite": "https://acme.com", - "metadataX": "https://twitter.com/acme", + "metadataWebsite": "https://example.com", + "metadataX": "https://twitter.com/example", + "metadataGithub": "https://github.com/example", + "metadataTokenAddress": "0x2344c0fe02ccd2b32155ca0ffcb1978a6d96a552", "tags": ["Example tag 1", "Example tag 2"], "isVisible": true, "isVerified": true @@ -3626,11 +3680,11 @@ "example": [ { "strategyAddress": "0x93c4b944d05dfe6df7645a86cd2206016c51564d", - "shares": "135064894598947935263152" + "shares": "1000000000000000000000" }, { "strategyAddress": "0x54945180db7943c0ed0fee7edab2bd24620256bc", - "shares": "9323641881708650182301" + "shares": "1000000000000000000000" } ] }, @@ -3707,6 +3761,50 @@ "cbETH": 2000000 } } + }, + "rewards": { + "type": "object", + "properties": { + "strategies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "strategyAddress": { + "type": "string", + "description": "The contract address of the restaking strategy", + "example": "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" + }, + "apy": { + "type": "number", + "description": "The latest APY recorded for the restaking strategy", + "example": 0.1 + } + }, + "required": ["strategyAddress", "apy"] + } + }, + "aggregateApy": { + "type": "number", + "description": "The aggregate APY across all strategies", + "example": 1 + } + }, + "required": ["strategies", "aggregateApy"], + "description": "The rewards and APY information of the AVS strategies", + "example": { + "strategies": [ + { + "strategyAddress": "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0", + "apy": 0.1 + }, + { + "strategyAddress": "0x93c4b944d05dfe6df7645a86cd2206016c51564d", + "apy": 0.1 + } + ], + "aggregateApy": 1 + } } }, "required": [ @@ -3869,6 +3967,12 @@ "items": { "type": "object", "properties": { + "stakerAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the staker", + "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" + }, "strategyAddress": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$", @@ -3878,10 +3982,10 @@ "shares": { "type": "string", "description": "The amount of shares held in the strategy", - "example": "40888428658906049" + "example": "40000000000000000" } }, - "required": ["strategyAddress", "shares"] + "required": ["stakerAddress", "strategyAddress", "shares"] } }, "tvl": { @@ -4059,7 +4163,7 @@ "schema": { "type": "string", "description": "Case-insensitive search query", - "example": "blockless" + "example": "eigen" } }, { @@ -4172,6 +4276,21 @@ "description": "The URL of the AVS operator's X", "example": "https://twitter.com/acme" }, + "totalStakers": { + "type": "number", + "description": "The total number of stakers who have delegated to this AVS operator", + "example": 10 + }, + "totalAvs": { + "type": "number", + "description": "The total number of AVS opted by the AVS operator", + "example": 10 + }, + "apy": { + "type": "number", + "description": "The latest APY recorded for the Operator", + "example": 1 + }, "createdAtBlock": { "type": "string", "description": "The block number at which the AVS Operator was registered", @@ -4232,11 +4351,6 @@ "description": "The list of restaked strategies", "example": ["0x35f4f28a8d3ff20eed10e087e8f96ea2641e6aa1"] }, - "totalStakers": { - "type": "number", - "description": "The total number of stakers who have delegated to this AVS operator", - "example": 10 - }, "tvl": { "type": "object", "properties": { @@ -4321,13 +4435,15 @@ "metadataTelegram", "metadataWebsite", "metadataX", + "totalStakers", + "totalAvs", + "apy", "createdAtBlock", "updatedAtBlock", "createdAt", "updatedAt", "shares", - "restakedStrategies", - "totalStakers" + "restakedStrategies" ] } }, @@ -4382,6 +4498,177 @@ } } }, + "/avs/{address}/rewards": { + "get": { + "operationId": "getAvsRewards", + "summary": "Retrieve all rewards for a given AVS address", + "description": "Returns a list of all rewards for a given AVS address.", + "tags": ["AVS"], + "parameters": [ + { + "in": "path", + "name": "address", + "description": "AVS service manager contract address", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "AVS service manager contract address", + "example": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "The rewards found for the AVS.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "AVS service manager contract address", + "example": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0" + }, + "submissions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "rewardsSubmissionHash": { + "type": "string", + "description": "The hash of the rewards submission", + "example": "0x141e6ea51d92c9ceaefbd5d1ac1b5f1c2ee06555ef20e224ff23ec3448edb7dd" + }, + "startTimestamp": { + "type": "number", + "description": "The timestamp marking the start of this rewards distribution period", + "example": 1720000000 + }, + "duration": { + "type": "number", + "description": "The duration (in seconds) over which the rewards are distributed", + "example": 2500000 + }, + "totalAmount": { + "type": "string", + "description": "The total amount of rewards allocated in this submission", + "example": "5000000000000000000" + }, + "tokenAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the token used for rewards distribution", + "example": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + }, + "strategies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "strategyAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the restaking strategy", + "example": "0xacb55c530acdb2849e6d4f36992cd8c9d50ed8f7" + }, + "multiplier": { + "type": "string", + "description": "The multiplier associated with this strategy", + "example": "1000000000000000000" + }, + "amount": { + "type": "string", + "description": "The amount of rewards allocated to this strategy from the total rewards in this submissionn", + "example": "5000000000000000000" + } + }, + "required": ["strategyAddress", "multiplier", "amount"] + }, + "description": "List of strategies involved in the rewards submission" + } + }, + "required": [ + "rewardsSubmissionHash", + "startTimestamp", + "duration", + "totalAmount", + "tokenAddress", + "strategies" + ] + }, + "description": "The list of of individual rewards submissions associated with the AVS" + }, + "totalRewards": { + "type": "string", + "description": "The aggregate amount of rewards distributed across all submissions", + "example": "1000000000000000000" + }, + "totalSubmissions": { + "type": "number", + "description": "The total count of rewards submissions associated with the AVS", + "example": 10 + }, + "rewardTokens": { + "type": "array", + "items": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$" + }, + "description": "The list of token addresses used for reward distribution", + "example": ["0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"] + }, + "rewardStrategies": { + "type": "array", + "items": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$" + }, + "description": "The list of strategy addresses for which rewards are distributed", + "example": [ + "0xacb55c530acdb2849e6d4f36992cd8c9d50ed8f7", + "0x13760f50a9d7377e4f20cb8cf9e4c26586c658ff" + ] + } + }, + "required": [ + "address", + "submissions", + "totalRewards", + "totalSubmissions", + "rewardTokens", + "rewardStrategies" + ] + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, "/operators": { "get": { "operationId": "getAllOperators", @@ -4408,7 +4695,7 @@ "schema": { "type": "string", "description": "Case-insensitive search query", - "example": "blockless" + "example": "eigen" } }, { @@ -4555,7 +4842,7 @@ "apy": { "type": "string", "description": "The latest APY recorded for the operator", - "example": "1.39" + "example": "1.0" }, "createdAtBlock": { "type": "string", @@ -4786,62 +5073,210 @@ } } }, - "/operators/{address}": { + "/operators/addresses": { "get": { - "operationId": "getOperatorByAddress", - "summary": "Retrieve an operator by address", - "description": "Returns an operator record by address.", + "operationId": "getAllOperatorAddresses", + "summary": "Retrieve all operator addresses", + "description": "Returns a list of all operator addresses. This page supports pagination.", "tags": ["Operators"], "parameters": [ { - "in": "path", - "name": "address", - "description": "The address of the operator", + "in": "query", + "name": "searchMode", + "description": "Search mode", "schema": { "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The address of the operator", - "example": "0x00107cfdeaddc0a3160ed2f6fedd627f313e7b1a" - }, - "required": true + "enum": ["contains", "startsWith"], + "default": "contains", + "description": "Search mode", + "example": "contains" + } }, { "in": "query", - "name": "withTvl", - "description": "Toggle whether the route should calculate the TVL from shares", + "name": "searchByText", + "description": "Case-insensitive search query", "schema": { "type": "string", - "enum": ["true", "false"], - "default": "false", - "description": "Toggle whether the route should calculate the TVL from shares", - "example": "false" + "description": "Case-insensitive search query", + "example": "eigen" } }, { "in": "query", - "name": "withAvsData", - "description": "Toggle whether to return additional data for each AVS registration for a given Operator", + "name": "skip", + "description": "The number of records to skip for pagination", "schema": { "type": "string", - "enum": ["true", "false"], - "default": "false", - "description": "Toggle whether to return additional data for each AVS registration for a given Operator", - "example": "false" + "default": "0", + "description": "The number of records to skip for pagination", + "example": 0 + } + }, + { + "in": "query", + "name": "take", + "description": "The number of records to return for pagination", + "schema": { + "type": "string", + "default": "12", + "description": "The number of records to return for pagination", + "example": 12 } } ], "responses": { "200": { - "description": "The record of the requested operator.", + "description": "The list of operator addresses.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "address": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the AVS operator", + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the AVS operator", + "example": "0x0000039b2f2ac9e3492a0f805ec7aea9eaee0c25" + }, + "name": { + "type": "string", + "description": "The Operator's name", + "example": "Example Operator" + }, + "logo": { + "type": "string", + "description": "The Operator's logo URL", + "example": "https://example.operator/logo.png" + } + }, + "required": ["address", "name", "logo"] + } + }, + "meta": { + "type": "object", + "properties": { + "total": { + "type": "number", + "description": "Total number of records in the database", + "example": 30 + }, + "skip": { + "type": "number", + "description": "The number of skiped records for this query", + "example": 0 + }, + "take": { + "type": "number", + "description": "The number of records returned for this query", + "example": 12 + } + }, + "required": ["total", "skip", "take"] + } + }, + "required": ["data", "meta"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, + "/operators/{address}": { + "get": { + "operationId": "getOperatorByAddress", + "summary": "Retrieve an operator by address", + "description": "Returns an operator record by address.", + "tags": ["Operators"], + "parameters": [ + { + "in": "path", + "name": "address", + "description": "The address of the operator", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the operator", + "example": "0x00107cfdeaddc0a3160ed2f6fedd627f313e7b1a" + }, + "required": true + }, + { + "in": "query", + "name": "withTvl", + "description": "Toggle whether the route should calculate the TVL from shares", + "schema": { + "type": "string", + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should calculate the TVL from shares", + "example": "false" + } + }, + { + "in": "query", + "name": "withAvsData", + "description": "Toggle whether to return additional data for each AVS registration for a given Operator", + "schema": { + "type": "string", + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether to return additional data for each AVS registration for a given Operator", + "example": "false" + } + }, + { + "in": "query", + "name": "withRewards", + "description": "Toggle whether the route should return Avs/Operator rewards APY data", + "schema": { + "type": "string", + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should return Avs/Operator rewards APY data", + "example": "false" + } + } + ], + "responses": { + "200": { + "description": "The record of the requested operator.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the AVS operator", "example": "0x09e6eb09213bdd3698bd8afb43ec3cb0ecff683a" }, "metadataName": { @@ -4902,7 +5337,7 @@ "apy": { "type": "string", "description": "The latest APY recorded for the operator", - "example": "1.39" + "example": "1.0" }, "createdAtBlock": { "type": "string", @@ -4961,30 +5396,549 @@ "type": "object", "properties": { "avsAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the AVS contract", + "example": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0" + }, + "isActive": { + "type": "boolean", + "description": "True indicates operator is an active participant while False indicates it used to be one but not anymore", + "example": false + }, + "metadataName": { + "type": "string", + "description": "The name of the AVS", + "example": "Example AVS" + }, + "metadataDescription": { + "type": "string", + "nullable": true, + "description": "The description of the AVS", + "example": "This is an example AVS" + }, + "metadataDiscord": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's Discord server", + "example": "https://discord.com/invite/abcdefghij" + }, + "metadataLogo": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's logo" + }, + "metadataTelegram": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's Telegram channel", + "example": "https://t.me/acme" + }, + "metadataWebsite": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's website", + "example": "https://acme.com" + }, + "metadataX": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's X", + "example": "https://twitter.com/acme" + }, + "metadataUrl": { + "type": "string", + "description": "URL for AVS metadata", + "example": "https://example.json" + }, + "curatedMetadata": { + "type": "object", + "properties": { + "metadataName": { + "type": "string", + "description": "The name of the AVS", + "example": "Example AVS" + }, + "metadataDescription": { + "type": "string", + "nullable": true, + "description": "The description of the AVS", + "example": "This is an example AVS" + }, + "metadataDiscord": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's Discord server", + "example": "https://discord.com/invite/abcdefghij" + }, + "metadataLogo": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's logo" + }, + "metadataTelegram": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's Telegram channel", + "example": "https://t.me/acme" + }, + "metadataWebsite": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's website", + "example": "https://acme.com" + }, + "metadataX": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's X", + "example": "https://twitter.com/acme" + }, + "metadataGithub": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's Github", + "example": "https://github.com/acme" + }, + "metadataTokenAddress": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The Token Address of the AVS", + "example": "0x2344c0fe02ccd2b32155ca0ffcb1978a6d96a552" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags to describe the AVS", + "example": ["Example tag 1", "Example tag 2"] + }, + "isVisible": { + "type": "boolean", + "description": "Indicates if AVS visibility is allowed", + "example": false + }, + "isVerified": { + "type": "boolean", + "description": "Indicates if the AVS has been verified by the EigenExplorer team", + "example": false + } + }, + "required": [ + "metadataName", + "metadataDescription", + "metadataDiscord", + "metadataLogo", + "metadataTelegram", + "metadataWebsite", + "metadataX", + "metadataGithub", + "metadataTokenAddress", + "tags", + "isVisible", + "isVerified" + ], + "description": "Curated metadata information for AVS" + }, + "restakeableStrategies": { + "type": "array", + "items": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$" + }, + "description": "The list of restakeable strategies for AVS" + }, + "totalStakers": { + "type": "number", + "description": "Total number of stakers", + "example": 80000 + }, + "totalOperators": { + "type": "number", + "description": "Total number of operators", + "example": 200 + }, + "tvlEth": { + "type": "string", + "description": "Total TVL in ETH", + "example": "3000000" + }, + "createdAtBlock": { + "type": "number", + "description": "The block number at which the AVS was created", + "example": 19592323 + }, + "updatedAtBlock": { + "type": "number", + "description": "The block number at which the AVS was last updated", + "example": 19592323 + }, + "createdAt": { + "type": "string", + "description": "The time stamp at which the AVS was created", + "example": "2024-04-05T21:49:59.000Z" + }, + "updatedAt": { + "type": "string", + "description": "The time stamp at which the AVS was last updated", + "example": "2024-04-05T21:49:59.000Z" + }, + "address": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$", "description": "AVS service manager contract address", "example": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0" }, - "isActive": { - "type": "boolean", - "description": "True indicates operator is an active participant while False indicates it used to be one but not anymore", - "example": false + "rewardsSubmissions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "Id for the rewards submission", + "example": 1 + }, + "submissionNonce": { + "type": "number", + "description": "The nonce of the rewards submission", + "example": 0 + }, + "rewardsSubmissionHash": { + "type": "string", + "description": "The hash of the rewards submission", + "example": "0x2bc2f7cef0974f7064dbdae054f9a0e5ea1c2293d180a749c70100506382d85" + }, + "avsAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "AVS address for the rewards submission", + "example": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0" + }, + "strategyAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "Strategy address for the rewards submission", + "example": "0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6" + }, + "multiplier": { + "type": "string", + "description": "The multiplier associated with this strategy", + "example": "1000000000000000000" + }, + "token": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the token used for rewards distribution", + "example": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + }, + "amount": { + "type": "string", + "description": "The amount of rewards allocated to this strategy from the total rewards", + "example": "300000000000000000" + }, + "startTimestamp": { + "type": "number", + "description": "The timestamp marking the start of this rewards distribution period", + "example": 1720000000 + }, + "duration": { + "type": "number", + "description": "The duration (in seconds) over which the rewards are distributed", + "example": 2500000 + }, + "createdAtBlock": { + "type": "number", + "description": "The block number at which the reward submission was recorded", + "example": 20495824 + }, + "createdAt": { + "type": "string", + "description": "The timestamp at which the reward submission was recorded", + "example": "2024-08-10T04:28:47.000Z" + } + }, + "required": [ + "id", + "submissionNonce", + "rewardsSubmissionHash", + "avsAddress", + "strategyAddress", + "multiplier", + "token", + "amount", + "startTimestamp", + "duration", + "createdAtBlock", + "createdAt" + ] + }, + "description": "List of rewards submissions associated with AVS" + }, + "operators": { + "type": "array", + "items": { + "type": "object", + "properties": { + "avsAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "AVS service manager contract address", + "example": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0" + }, + "operatorAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the AVS operator", + "example": "0x4cd2086e1d708e65db5d4f5712a9ca46ed4bbd0a" + }, + "isActive": { + "type": "boolean", + "description": "True indicates operator is an active participant while False indicates it used to be one but not anymore", + "example": true + }, + "restakedStrategies": { + "type": "array", + "items": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$" + }, + "description": "List of strategies restaked by the operator", + "example": [ + "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0", + "0x93c4b944d05dfe6df7645a86cd2206016c51564d" + ] + }, + "createdAtBlock": { + "type": "number", + "description": "The block number at which the AVS Operator was registered", + "example": 19614553 + }, + "updatedAtBlock": { + "type": "number", + "description": "The block number at which the AVS Operator registration was last updated", + "example": 19614553 + }, + "createdAt": { + "type": "string", + "description": "The time stamp at which the AVS Operator was registered", + "example": "2024-04-09T00:35:35.000Z" + }, + "updatedAt": { + "type": "string", + "description": "The time stamp at which the AVS Operator registration was last updated", + "example": "2024-04-09T00:35:35.000Z" + }, + "operator": { + "type": "object", + "properties": { + "address": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the AVS operator", + "example": "0x09e6eb09213bdd3698bd8afb43ec3cb0ecff683a" + }, + "metadataUrl": { + "type": "string", + "description": "URL for operator metadata", + "example": "https://raw.githubusercontent.com/github-infstones/eigenlayer/main/metadata.json" + }, + "metadataName": { + "type": "string", + "description": "The name of the AVS operator", + "example": "Example AVS Operator" + }, + "metadataDescription": { + "type": "string", + "nullable": true, + "description": "Description of the operator", + "example": "This is an example AVS operator" + }, + "metadataDiscord": { + "type": "string", + "nullable": true, + "description": "The URL of the AVS operator's Discord server", + "example": "https://discord.com/invite/example" + }, + "metadataLogo": { + "type": "string", + "nullable": true, + "description": "Logo URL", + "example": "" + }, + "metadataTelegram": { + "type": "string", + "nullable": true, + "description": "The URL of the AVS operator's Telegram channel", + "example": "https://t.me/example" + }, + "metadataWebsite": { + "type": "string", + "nullable": true, + "description": "The URL of the AVS operator's website", + "example": "https://example.com" + }, + "metadataX": { + "type": "string", + "nullable": true, + "description": "The URL of the AVS operator's X", + "example": "https://twitter.com/example" + }, + "isMetadataSynced": { + "type": "boolean", + "description": "Indicates if metadata is synced", + "example": true + }, + "totalStakers": { + "type": "number", + "description": "The total number of stakers who have delegated to this AVS operator", + "example": 20000 + }, + "totalAvs": { + "type": "number", + "description": "The total number of AVS opted by the AVS operator", + "example": 10 + }, + "apy": { + "type": "string", + "description": "The latest APY recorded for the operator", + "example": "1.0" + }, + "tvlEth": { + "type": "string", + "description": "Total TVL in ETH", + "example": "30000" + }, + "sharesHash": { + "type": "string", + "description": "Shares hash for the operator", + "example": "0c67d2a677454013c442732ee3bcf07b" + }, + "createdAtBlock": { + "type": "number", + "description": "The block number at which the AVS Operator was registered", + "example": 19613775 + }, + "updatedAtBlock": { + "type": "number", + "description": "The block number at which the AVS Operator registration was last updated", + "example": 19613775 + }, + "createdAt": { + "type": "string", + "description": "The time stamp at which the AVS Operator was registered", + "example": "2024-04-08T21:58:35.000Z" + }, + "updatedAt": { + "type": "string", + "description": "The time stamp at which the AVS Operator registration was last updated", + "example": "2024-04-08T21:58:35.000Z" + }, + "shares": { + "type": "array", + "items": { + "type": "object", + "properties": { + "operatorAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the AVS operator", + "example": "0x4cd2086e1d708e65db5d4f5712a9ca46ed4bbd0a" + }, + "strategyAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the restaking strategy", + "example": "0x73a18a6304d05b495ecb161dbf1ab496461bbf2e" + }, + "shares": { + "type": "string", + "description": "The amount of shares held in the strategy", + "example": "1000000000000000000000" + } + }, + "required": ["operatorAddress", "strategyAddress", "shares"] + }, + "description": "The strategy shares held in the AVS operator" + } + }, + "required": [ + "address", + "metadataUrl", + "metadataName", + "metadataDescription", + "metadataDiscord", + "metadataLogo", + "metadataTelegram", + "metadataWebsite", + "metadataX", + "isMetadataSynced", + "totalStakers", + "totalAvs", + "apy", + "tvlEth", + "sharesHash", + "createdAtBlock", + "updatedAtBlock", + "createdAt", + "updatedAt", + "shares" + ] + } + }, + "required": [ + "avsAddress", + "operatorAddress", + "isActive", + "restakedStrategies", + "createdAtBlock", + "updatedAtBlock", + "createdAt", + "updatedAt", + "operator" + ] + }, + "description": "List of operators associated with the AVS registration" } }, - "required": ["avsAddress", "isActive"] + "required": [ + "avsAddress", + "isActive", + "metadataName", + "metadataDescription", + "metadataDiscord", + "metadataLogo", + "metadataTelegram", + "metadataWebsite", + "metadataX", + "metadataUrl", + "restakeableStrategies", + "totalStakers", + "totalOperators", + "tvlEth", + "createdAtBlock", + "updatedAtBlock", + "createdAt", + "updatedAt", + "address", + "rewardsSubmissions", + "operators" + ] }, - "description": "Operator AVS registrations and their participation status", - "example": [ - { - "avsAddress": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0", - "isActive": true - }, - { - "avsAddress": "0xe8e59c6c8b56f2c178f63bcfc4ce5e5e2359c8fc", - "isActive": false - } - ] + "description": "Detailed AVS registrations information for the operator" }, "tvl": { "type": "object", @@ -5059,6 +6013,63 @@ "cbETH": 2000000 } } + }, + "rewards": { + "type": "object", + "properties": { + "avs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "avsAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "AVS service manager contract address", + "example": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0" + }, + "apy": { + "type": "number", + "description": "Latest APY recorded for the AVS", + "example": 0.15973119826488588 + } + }, + "required": ["avsAddress", "apy"] + } + }, + "strategies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "strategyAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the restaking strategy", + "example": "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" + }, + "apy": { + "type": "number", + "description": "APY of the restaking strategy", + "example": 0.1 + } + }, + "required": ["strategyAddress", "apy"] + } + }, + "aggregateApy": { + "type": "number", + "description": "The aggregate APY across all strategies", + "example": 1 + }, + "operatorEarningsEth": { + "type": "string", + "description": "Total earnings for the operator in ETH", + "example": "1000000000000000000" + } + }, + "required": ["avs", "strategies", "aggregateApy", "operatorEarningsEth"], + "description": "The rewards information for the operator" } }, "required": [ @@ -5078,7 +6089,8 @@ "createdAt", "updatedAt", "shares", - "avsRegistrations" + "avsRegistrations", + "rewards" ] } } @@ -5108,6 +6120,94 @@ } } }, + "/operators/{address}/rewards": { + "get": { + "operationId": "getOperatorRewards", + "summary": "Retrieve rewards info for an operator", + "description": "Returns a list of strategies that the Operator is rewarded for, and the tokens they are rewarded in.", + "tags": ["Operators"], + "parameters": [ + { + "in": "path", + "name": "address", + "description": "The address of the operator", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the operator", + "example": "0x00107cfdeaddc0a3160ed2f6fedd627f313e7b1a" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "The reward strategies and tokens found for the Operator.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the AVS operator", + "example": "0xdbed88d83176316fc46797b43adee927dc2ff2f5" + }, + "rewardTokens": { + "type": "array", + "items": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$" + }, + "description": "List of tokens in which the operator receives rewards", + "example": [ + "0xba50933c268f567bdc86e1ac131be072c6b0b71a", + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + ] + }, + "rewardStrategies": { + "type": "array", + "items": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$" + }, + "description": "List of strategies for which the operator receives rewards", + "example": [ + "0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6", + "0x13760f50a9d7377e4f20cb8cf9e4c26586c658ff" + ] + } + }, + "required": ["address", "rewardTokens", "rewardStrategies"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, "/withdrawals": { "get": { "operationId": "getAllWithdrawals", @@ -5596,7 +6696,7 @@ "shares": { "type": "string", "description": "The amount of shares held in the strategy", - "example": "40888428658906049" + "example": "40000000000000000" } }, "required": ["strategyAddress", "shares"] @@ -5825,7 +6925,7 @@ "shares": { "type": "string", "description": "The amount of shares held in the strategy", - "example": "40888428658906049" + "example": "40000000000000000" } }, "required": ["strategyAddress", "shares"] @@ -6775,7 +7875,7 @@ "shares": { "type": "string", "description": "The amount of shares held in the strategy", - "example": "40888428658906049" + "example": "40000000000000000" }, "createdAtBlock": { "type": "number", @@ -6949,7 +8049,7 @@ "shares": { "type": "string", "description": "The amount of shares held in the strategy", - "example": "40888428658906049" + "example": "40000000000000000" }, "createdAtBlock": { "type": "number", @@ -7023,6 +8123,80 @@ } } } + }, + "/rewards/strategies": { + "get": { + "operationId": "getStrategies", + "summary": "Retrieve all strategies with their reward tokens", + "description": "Returns a list of strategies with their corresponding reward tokens, including strategy addresses and associated token addresses.", + "tags": ["Rewards"], + "responses": { + "200": { + "description": "List of strategies along with associated reward tokens.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "strategies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "strategyAddress": { + "type": "string", + "description": "The contract address of the restaking strategy", + "example": "0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6" + }, + "tokens": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of reward token addresses associated with the strategy", + "example": [ + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0xba50933c268f567bdc86e1ac131be072c6b0b71a" + ] + } + }, + "required": ["strategyAddress", "tokens"] + } + }, + "total": { + "type": "number", + "description": "The total number of strategies", + "example": 15 + } + }, + "required": ["strategies", "total"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } } }, "components": { diff --git a/packages/openapi/src/apiResponseSchema/avs/avsAddressResponse.ts b/packages/openapi/src/apiResponseSchema/avs/avsAddressResponse.ts index 4c78ad38..2d64d4a7 100644 --- a/packages/openapi/src/apiResponseSchema/avs/avsAddressResponse.ts +++ b/packages/openapi/src/apiResponseSchema/avs/avsAddressResponse.ts @@ -2,8 +2,12 @@ import z from '../../../../api/src/schema/zod' import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/base/ethereumAddress' export const AvsAddressSchema = z.object({ - name: z.string().describe("The AVS's name").openapi({ example: 'Example AVS' }), address: EthereumAddressSchema.describe('AVS service manager contract address').openapi({ example: '0x35f4f28a8d3ff20eed10e087e8f96ea2641e6aa1' - }) + }), + name: z.string().describe("The AVS's name").openapi({ example: 'Example AVS' }), + logo: z + .string() + .describe("The AVS's logo URL") + .openapi({ example: 'https://example.avs/logo.png' }) }) diff --git a/packages/openapi/src/apiResponseSchema/avs/avsOperatorResponse.ts b/packages/openapi/src/apiResponseSchema/avs/avsOperatorResponse.ts index a98afa60..977f873c 100644 --- a/packages/openapi/src/apiResponseSchema/avs/avsOperatorResponse.ts +++ b/packages/openapi/src/apiResponseSchema/avs/avsOperatorResponse.ts @@ -15,6 +15,15 @@ export const AvsOperatorResponseSchema = z.object({ metadataTelegram: OperatorMetaDataSchema.shape.metadataTelegram, metadataWebsite: OperatorMetaDataSchema.shape.metadataWebsite, metadataX: OperatorMetaDataSchema.shape.metadataX, + totalStakers: z + .number() + .describe('The total number of stakers who have delegated to this AVS operator') + .openapi({ example: 10 }), + totalAvs: z + .number() + .describe('The total number of AVS opted by the AVS operator') + .openapi({ example: 10 }), + apy: z.number().describe('The latest APY recorded for the Operator').openapi({ example: 1.0 }), createdAtBlock: z .string() .describe('The block number at which the AVS Operator was registered') @@ -50,10 +59,6 @@ export const AvsOperatorResponseSchema = z.object({ .array(EthereumAddressSchema) .describe('The list of restaked strategies') .openapi({ example: ['0x35f4f28a8d3ff20eed10e087e8f96ea2641e6aa1'] }), - totalStakers: z - .number() - .describe('The total number of stakers who have delegated to this AVS operator') - .openapi({ example: 10 }), tvl: TvlSchema.optional() .describe('The total value locked (TVL) in the AVS operator') .openapi({ diff --git a/packages/openapi/src/apiResponseSchema/avs/avsResponse.ts b/packages/openapi/src/apiResponseSchema/avs/avsResponse.ts index 2f810cda..18aac15d 100644 --- a/packages/openapi/src/apiResponseSchema/avs/avsResponse.ts +++ b/packages/openapi/src/apiResponseSchema/avs/avsResponse.ts @@ -24,7 +24,7 @@ export const AvsSchema = z.object({ .number() .describe('The total number of operators operating the AVS') .openapi({ example: 10 }), - apy: z.string().describe('The latest APY recorded for the AVS').openapi({ example: '1.302' }), + apy: z.string().describe('The latest APY recorded for the AVS').openapi({ example: '1.0' }), createdAtBlock: z .string() .describe('The block number at which the AVS was created') @@ -53,11 +53,13 @@ export const AvsSchema = z.object({ avsAddress: '0x2344c0fe02ccd2b32155ca0ffcb1978a6d96a552', metadataName: 'Example AVS', metadataDescription: 'This is an example AVS', - metadataDiscord: 'https://discord.com/invite/abcdefghij', + metadataDiscord: 'https://discord.com/invite/example', metadataLogo: "The URL of the AVS's logo", metadataTelegram: "The URL of the AVS's Telegram channel", - metadataWebsite: 'https://acme.com', - metadataX: 'https://twitter.com/acme', + metadataWebsite: 'https://example.com', + metadataX: 'https://twitter.com/example', + metadataGithub: 'https://github.com/example', + metadataTokenAddress: '0x2344c0fe02ccd2b32155ca0ffcb1978a6d96a552', tags: ['Example tag 1', 'Example tag 2'], isVisible: true, isVerified: true @@ -70,11 +72,11 @@ export const AvsSchema = z.object({ example: [ { strategyAddress: '0x93c4b944d05dfe6df7645a86cd2206016c51564d', - shares: '135064894598947935263152' + shares: '1000000000000000000000' }, { strategyAddress: '0x54945180db7943c0ed0fee7edab2bd24620256bc', - shares: '9323641881708650182301' + shares: '1000000000000000000000' } ] }), diff --git a/packages/openapi/src/apiResponseSchema/avs/avsRewardsResponse.ts b/packages/openapi/src/apiResponseSchema/avs/avsRewardsResponse.ts new file mode 100644 index 00000000..d6818827 --- /dev/null +++ b/packages/openapi/src/apiResponseSchema/avs/avsRewardsResponse.ts @@ -0,0 +1,79 @@ +import z from '../../../../api/src/schema/zod' +import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/base/ethereumAddress' + +const SubmissionStrategySchema = z.object({ + strategyAddress: EthereumAddressSchema.describe('The address of the restaking strategy').openapi({ + example: '0xacb55c530acdb2849e6d4f36992cd8c9d50ed8f7' + }), + multiplier: z.string().describe('The multiplier associated with this strategy').openapi({ + example: '1000000000000000000' + }), + amount: z + .string() + .describe( + 'The amount of rewards allocated to this strategy from the total rewards in this submissionn' + ) + .openapi({ + example: '5000000000000000000' + }) +}) + +const RewardsSubmissionSchema = z.object({ + rewardsSubmissionHash: z.string().describe('The hash of the rewards submission').openapi({ + example: '0x141e6ea51d92c9ceaefbd5d1ac1b5f1c2ee06555ef20e224ff23ec3448edb7dd' + }), + startTimestamp: z + .number() + .describe('The timestamp marking the start of this rewards distribution period') + .openapi({ example: 1720000000 }), + duration: z + .number() + .describe('The duration (in seconds) over which the rewards are distributed') + .openapi({ example: 2500000 }), + totalAmount: z + .string() + .describe('The total amount of rewards allocated in this submission') + .openapi({ + example: '5000000000000000000' + }), + tokenAddress: EthereumAddressSchema.describe( + 'The contract address of the token used for rewards distribution' + ).openapi({ + example: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' + }), + strategies: z + .array(SubmissionStrategySchema) + .describe('List of strategies involved in the rewards submission') +}) + +export const AvsRewardsSchema = z.object({ + address: EthereumAddressSchema.describe('AVS service manager contract address').openapi({ + example: '0x870679e138bcdf293b7ff14dd44b70fc97e12fc0' + }), + submissions: z + .array(RewardsSubmissionSchema) + .describe('The list of of individual rewards submissions associated with the AVS'), + totalRewards: z + .string() + .describe('The aggregate amount of rewards distributed across all submissions') + .openapi({ example: '1000000000000000000' }), + totalSubmissions: z + .number() + .describe('The total count of rewards submissions associated with the AVS') + .openapi({ example: 10 }), + rewardTokens: z + .array(EthereumAddressSchema) + .describe('The list of token addresses used for reward distribution') + .openapi({ + example: ['0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'] + }), + rewardStrategies: z + .array(EthereumAddressSchema) + .describe('The list of strategy addresses for which rewards are distributed') + .openapi({ + example: [ + '0xacb55c530acdb2849e6d4f36992cd8c9d50ed8f7', + '0x13760f50a9d7377e4f20cb8cf9e4c26586c658ff' + ] + }) +}) diff --git a/packages/openapi/src/apiResponseSchema/avs/avsStakerResponse.ts b/packages/openapi/src/apiResponseSchema/avs/avsStakerResponse.ts new file mode 100644 index 00000000..f87d6698 --- /dev/null +++ b/packages/openapi/src/apiResponseSchema/avs/avsStakerResponse.ts @@ -0,0 +1,60 @@ +import z from '../../../../api/src/schema/zod' +import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/base/ethereumAddress' +import { TvlSchema } from '../base/tvlResponses' + +const StakerSharesSchema = z.object({ + stakerAddress: EthereumAddressSchema.describe('The address of the staker').openapi({ + example: '0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd' + }), + strategyAddress: EthereumAddressSchema.describe( + 'The contract address of the restaking strategy' + ).openapi({ example: '0x93c4b944d05dfe6df7645a86cd2206016c51564a' }), + shares: z + .string() + .describe('The amount of shares held in the strategy') + .openapi({ example: '40000000000000000' }) +}) + +export const AvsStakerSchema = z.object({ + address: EthereumAddressSchema.describe('The contract address of the staker').openapi({ + example: '0x0000006c21964af0d420af8992851a30fa13a68b' + }), + operatorAddress: EthereumAddressSchema.describe('The address of the operator').openapi({ + example: '0x71c6f7ed8c2d4925d0baf16f6a85bb1736d412eb' + }), + createdAtBlock: z + .string() + .describe('The block number at which the Staker made first delegation') + .openapi({ example: '19631203' }), + updatedAtBlock: z + .string() + .describe('The block number at which the Staker made last delegation') + .openapi({ example: '19631203' }), + createdAt: z + .string() + .describe('The time stamp at which the Staker made first delegation') + .openapi({ example: '2024-04-11T08:31:11.000Z' }), + updatedAt: z + .string() + .describe('The time stamp at which the Staker made last delegation') + .openapi({ example: '2024-04-11T08:31:11.000Z' }), + shares: z.array(StakerSharesSchema), + tvl: TvlSchema.optional() + .describe('The total value locked (TVL) in the AVS staker') + .openapi({ + example: { + tvl: 1000000, + tvlBeaconChain: 1000000, + tvlWETH: 1000000, + tvlRestaking: 1000000, + tvlStrategies: { + Eigen: 1000000, + cbETH: 2000000 + }, + tvlStrategiesEth: { + stETH: 1000000, + cbETH: 2000000 + } + } + }) +}) diff --git a/packages/openapi/src/apiResponseSchema/avs/avsWithRewardsResponse.ts b/packages/openapi/src/apiResponseSchema/avs/avsWithRewardsResponse.ts new file mode 100644 index 00000000..a14aa002 --- /dev/null +++ b/packages/openapi/src/apiResponseSchema/avs/avsWithRewardsResponse.ts @@ -0,0 +1,22 @@ +import { RewardsSchema } from '../base/rewardsResponse' +import { AvsSchema } from './avsResponse' + +export const AvsWithRewardsSchema = AvsSchema.extend({ + rewards: RewardsSchema.optional() + .describe('The rewards and APY information of the AVS strategies') + .openapi({ + example: { + strategies: [ + { + strategyAddress: '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0', + apy: 0.1 + }, + { + strategyAddress: '0x93c4b944d05dfe6df7645a86cd2206016c51564d', + apy: 0.1 + } + ], + aggregateApy: 1.0 + } + }) +}) diff --git a/packages/openapi/src/apiResponseSchema/base/rewardTokensResponse.ts b/packages/openapi/src/apiResponseSchema/base/rewardTokensResponse.ts new file mode 100644 index 00000000..df29269c --- /dev/null +++ b/packages/openapi/src/apiResponseSchema/base/rewardTokensResponse.ts @@ -0,0 +1,22 @@ +import z from '../../../../api/src/schema/zod' + +export const RewardsTokenSchema = z.object({ + strategies: z.array( + z.object({ + strategyAddress: z + .string() + .describe('The contract address of the restaking strategy') + .openapi({ example: '0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6' }), + tokens: z + .array(z.string()) + .describe('List of reward token addresses associated with the strategy') + .openapi({ + example: [ + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + '0xba50933c268f567bdc86e1ac131be072c6b0b71a' + ] + }) + }) + ), + total: z.number().describe('The total number of strategies').openapi({ example: 15 }) +}) diff --git a/packages/openapi/src/apiResponseSchema/base/rewardsResponse.ts b/packages/openapi/src/apiResponseSchema/base/rewardsResponse.ts new file mode 100644 index 00000000..c4ea1779 --- /dev/null +++ b/packages/openapi/src/apiResponseSchema/base/rewardsResponse.ts @@ -0,0 +1,20 @@ +import z from '../../../../api/src/schema/zod' + +export const RewardsSchema = z.object({ + strategies: z.array( + z.object({ + strategyAddress: z + .string() + .describe('The contract address of the restaking strategy') + .openapi({ example: '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' }), + apy: z + .number() + .describe('The latest APY recorded for the restaking strategy') + .openapi({ example: 0.1 }) + }) + ), + aggregateApy: z + .number() + .describe('The aggregate APY across all strategies') + .openapi({ example: 1.0 }) +}) diff --git a/packages/openapi/src/apiResponseSchema/deposits/depositsResponseSchema.ts b/packages/openapi/src/apiResponseSchema/deposits/depositsResponseSchema.ts index ea7ae2bb..751a8802 100644 --- a/packages/openapi/src/apiResponseSchema/deposits/depositsResponseSchema.ts +++ b/packages/openapi/src/apiResponseSchema/deposits/depositsResponseSchema.ts @@ -21,7 +21,7 @@ export const DepositsResponseSchema = z.object({ shares: z .string() .describe('The amount of shares held in the strategy') - .openapi({ example: '40888428658906049' }), + .openapi({ example: '40000000000000000' }), createdAtBlock: z .number() .describe('The block number when the withdrawal was recorded by EigenExplorer') diff --git a/packages/openapi/src/apiResponseSchema/operator/operatorAddressResponse.ts b/packages/openapi/src/apiResponseSchema/operator/operatorAddressResponse.ts new file mode 100644 index 00000000..26349631 --- /dev/null +++ b/packages/openapi/src/apiResponseSchema/operator/operatorAddressResponse.ts @@ -0,0 +1,13 @@ +import z from '../../../../api/src/schema/zod' +import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/base/ethereumAddress' + +export const OperatorAddressSchema = z.object({ + address: EthereumAddressSchema.describe('The contract address of the AVS operator').openapi({ + example: '0x0000039b2f2ac9e3492a0f805ec7aea9eaee0c25' + }), + name: z.string().describe("The Operator's name").openapi({ example: 'Example Operator' }), + logo: z + .string() + .describe("The Operator's logo URL") + .openapi({ example: 'https://example.operator/logo.png' }) +}) diff --git a/packages/openapi/src/apiResponseSchema/operatorResponse.ts b/packages/openapi/src/apiResponseSchema/operator/operatorResponse.ts similarity index 83% rename from packages/openapi/src/apiResponseSchema/operatorResponse.ts rename to packages/openapi/src/apiResponseSchema/operator/operatorResponse.ts index 5b610dfa..e7c3c093 100644 --- a/packages/openapi/src/apiResponseSchema/operatorResponse.ts +++ b/packages/openapi/src/apiResponseSchema/operator/operatorResponse.ts @@ -1,9 +1,9 @@ -import z from '../../../api/src/schema/zod' -import { OperatorMetaDataSchema } from '../../../api/src/schema/zod/schemas/base/operatorMetaData' -import { EthereumAddressSchema } from '../../../api/src/schema/zod/schemas/base/ethereumAddress' -import { TvlSchema } from './base/tvlResponses' -import { StrategySharesSchema } from '../../../api/src/schema/zod/schemas/base/strategyShares' -import { AvsRegistrationSchema } from '../../../api/src/schema/zod/schemas/base/avsRegistrations' +import z from '../../../../api/src/schema/zod' +import { OperatorMetaDataSchema } from '../../../../api/src/schema/zod/schemas/base/operatorMetaData' +import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/base/ethereumAddress' +import { TvlSchema } from '.././base/tvlResponses' +import { StrategySharesSchema } from '../../../../api/src/schema/zod/schemas/base/strategyShares' +import { AvsRegistrationSchema } from '../../../../api/src/schema/zod/schemas/base/avsRegistrations' export const OperatorResponseSchema = z.object({ address: EthereumAddressSchema.describe('The contract address of the AVS operator').openapi({ @@ -24,7 +24,7 @@ export const OperatorResponseSchema = z.object({ .number() .describe('The total number of AVS opted by the AVS operator') .openapi({ example: 10 }), - apy: z.string().describe('The latest APY recorded for the operator').openapi({ example: '1.39' }), + apy: z.string().describe('The latest APY recorded for the operator').openapi({ example: '1.0' }), createdAtBlock: z .string() .describe('The block number at which the AVS Operator was registered') diff --git a/packages/openapi/src/apiResponseSchema/operator/operatorRewardsResponse.ts b/packages/openapi/src/apiResponseSchema/operator/operatorRewardsResponse.ts new file mode 100644 index 00000000..bdda3abc --- /dev/null +++ b/packages/openapi/src/apiResponseSchema/operator/operatorRewardsResponse.ts @@ -0,0 +1,26 @@ +import z from '../../../../api/src/schema/zod' +import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/base/ethereumAddress' + +export const OperatorRewardsSchema = z.object({ + address: EthereumAddressSchema.describe('The contract address of the AVS operator').openapi({ + example: '0xdbed88d83176316fc46797b43adee927dc2ff2f5' + }), + rewardTokens: z + .array(EthereumAddressSchema) + .describe('List of tokens in which the operator receives rewards') + .openapi({ + example: [ + '0xba50933c268f567bdc86e1ac131be072c6b0b71a', + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' + ] + }), + rewardStrategies: z + .array(EthereumAddressSchema) + .describe('List of strategies for which the operator receives rewards') + .openapi({ + example: [ + '0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6', + '0x13760f50a9d7377e4f20cb8cf9e4c26586c658ff' + ] + }) +}) diff --git a/packages/openapi/src/apiResponseSchema/operator/operatorWithRewardsResponse.ts b/packages/openapi/src/apiResponseSchema/operator/operatorWithRewardsResponse.ts new file mode 100644 index 00000000..0f614371 --- /dev/null +++ b/packages/openapi/src/apiResponseSchema/operator/operatorWithRewardsResponse.ts @@ -0,0 +1,297 @@ +import z from '../../../../api/src/schema/zod' +import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/base/ethereumAddress' +import { AvsMetaDataSchema } from '../../../../api/src/schema/zod/schemas/base/avsMetaData' +import { CuratedMetadataSchema } from '.././base/curatedMetadataResponses' +import { OperatorResponseSchema } from './operatorResponse' + +export const AvsRegistrationSchema = z.object({ + avsAddress: EthereumAddressSchema.describe('The address of the AVS contract').openapi({ + example: '0x870679e138bcdf293b7ff14dd44b70fc97e12fc0' + }), + isActive: z + .boolean() + .describe( + 'True indicates operator is an active participant while False indicates it used to be one but not anymore' + ) + .openapi({ example: false }) +}) + +export const AvsMetaDataFields = z.object({ + metadataName: AvsMetaDataSchema.shape.metadataName, + metadataDescription: AvsMetaDataSchema.shape.metadataDescription, + metadataDiscord: AvsMetaDataSchema.shape.metadataDiscord, + metadataLogo: AvsMetaDataSchema.shape.metadataLogo, + metadataTelegram: AvsMetaDataSchema.shape.metadataTelegram, + metadataWebsite: AvsMetaDataSchema.shape.metadataWebsite, + metadataX: AvsMetaDataSchema.shape.metadataX, + metadataUrl: z.string().describe('URL for AVS metadata').openapi({ + example: 'https://example.json' + }) +}) + +export const RewardsSubmissionSchema = z.object({ + id: z.number().describe('Id for the rewards submission').openapi({ example: 1 }), + submissionNonce: z + .number() + .describe('The nonce of the rewards submission') + .openapi({ example: 0 }), + rewardsSubmissionHash: z.string().describe('The hash of the rewards submission').openapi({ + example: '0x2bc2f7cef0974f7064dbdae054f9a0e5ea1c2293d180a749c70100506382d85' + }), + avsAddress: EthereumAddressSchema.describe('AVS address for the rewards submission').openapi({ + example: '0x870679e138bcdf293b7ff14dd44b70fc97e12fc0' + }), + strategyAddress: EthereumAddressSchema.describe( + 'Strategy address for the rewards submission' + ).openapi({ + example: '0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6' + }), + multiplier: z + .string() + .describe('The multiplier associated with this strategy') + .openapi({ example: '1000000000000000000' }), + token: EthereumAddressSchema.describe( + 'The contract address of the token used for rewards distribution' + ).openapi({ + example: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' + }), + amount: z + .string() + .describe('The amount of rewards allocated to this strategy from the total rewards') + .openapi({ example: '300000000000000000' }), + startTimestamp: z + .number() + .describe('The timestamp marking the start of this rewards distribution period') + .openapi({ example: 1720000000 }), + duration: z + .number() + .describe('The duration (in seconds) over which the rewards are distributed') + .openapi({ example: 2500000 }), + createdAtBlock: z + .number() + .describe('The block number at which the reward submission was recorded') + .openapi({ example: 20495824 }), + createdAt: z + .string() + .describe('The timestamp at which the reward submission was recorded') + .openapi({ + example: '2024-08-10T04:28:47.000Z' + }) +}) + +export const OperatorDetailsSchema = z.object({ + address: EthereumAddressSchema.describe('The contract address of the AVS operator').openapi({ + example: '0x09e6eb09213bdd3698bd8afb43ec3cb0ecff683a' + }), + metadataUrl: z.string().describe('URL for operator metadata').openapi({ + example: 'https://raw.githubusercontent.com/github-infstones/eigenlayer/main/metadata.json' + }), + metadataName: z + .string() + .describe('The name of the AVS operator') + .openapi({ example: 'Example AVS Operator' }), + metadataDescription: z.string().nullable().describe('Description of the operator').openapi({ + example: 'This is an example AVS operator' + }), + metadataDiscord: z + .string() + .nullable() + .describe("The URL of the AVS operator's Discord server") + .openapi({ example: 'https://discord.com/invite/example' }), + metadataLogo: z.string().nullable().describe('Logo URL').openapi({ + example: '' + }), + metadataTelegram: z + .string() + .nullable() + .describe("The URL of the AVS operator's Telegram channel") + .openapi({ example: 'https://t.me/example' }), + metadataWebsite: z + .string() + .nullable() + .describe("The URL of the AVS operator's website") + .openapi({ example: 'https://example.com' }), + metadataX: z + .string() + .nullable() + .describe("The URL of the AVS operator's X") + .openapi({ example: 'https://twitter.com/example' }), + isMetadataSynced: z + .boolean() + .describe('Indicates if metadata is synced') + .openapi({ example: true }), + totalStakers: z + .number() + .describe('The total number of stakers who have delegated to this AVS operator') + .openapi({ example: 20000 }), + totalAvs: z + .number() + .describe('The total number of AVS opted by the AVS operator') + .openapi({ example: 10 }), + apy: z.string().describe('The latest APY recorded for the operator').openapi({ example: '1.0' }), + tvlEth: z.string().describe('Total TVL in ETH').openapi({ example: '30000' }), + sharesHash: z.string().describe('Shares hash for the operator').openapi({ + example: '0c67d2a677454013c442732ee3bcf07b' + }), + createdAtBlock: z + .number() + .describe('The block number at which the AVS Operator was registered') + .openapi({ example: 19613775 }), + updatedAtBlock: z + .number() + .describe('The block number at which the AVS Operator registration was last updated') + .openapi({ example: 19613775 }), + createdAt: z + .string() + .describe('The time stamp at which the AVS Operator was registered') + .openapi({ + example: '2024-04-08T21:58:35.000Z' + }), + updatedAt: z + .string() + .describe('The time stamp at which the AVS Operator registration was last updated') + .openapi({ + example: '2024-04-08T21:58:35.000Z' + }), + shares: z + .array( + z.object({ + operatorAddress: EthereumAddressSchema.describe( + 'The contract address of the AVS operator' + ).openapi({ + example: '0x4cd2086e1d708e65db5d4f5712a9ca46ed4bbd0a' + }), + strategyAddress: EthereumAddressSchema.describe( + 'The contract address of the restaking strategy' + ).openapi({ + example: '0x73a18a6304d05b495ecb161dbf1ab496461bbf2e' + }), + shares: z + .string() + .describe('The amount of shares held in the strategy') + .openapi({ example: '1000000000000000000000' }) + }) + ) + .describe('The strategy shares held in the AVS operator') +}) + +export const OperatorSchema = z.object({ + avsAddress: EthereumAddressSchema.describe('AVS service manager contract address').openapi({ + example: '0x870679e138bcdf293b7ff14dd44b70fc97e12fc0' + }), + operatorAddress: EthereumAddressSchema.describe( + 'The contract address of the AVS operator' + ).openapi({ + example: '0x4cd2086e1d708e65db5d4f5712a9ca46ed4bbd0a' + }), + isActive: z + .boolean() + .describe( + 'True indicates operator is an active participant while False indicates it used to be one but not anymore' + ) + .openapi({ example: true }), + restakedStrategies: z + .array(EthereumAddressSchema) + .describe('List of strategies restaked by the operator') + .openapi({ + example: [ + '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0', + '0x93c4b944d05dfe6df7645a86cd2206016c51564d' + ] + }), + createdAtBlock: z + .number() + .describe('The block number at which the AVS Operator was registered') + .openapi({ example: 19614553 }), + updatedAtBlock: z + .number() + .describe('The block number at which the AVS Operator registration was last updated') + .openapi({ example: 19614553 }), + createdAt: z + .string() + .describe('The time stamp at which the AVS Operator was registered') + .openapi({ + example: '2024-04-09T00:35:35.000Z' + }), + updatedAt: z + .string() + .describe('The time stamp at which the AVS Operator registration was last updated') + .openapi({ + example: '2024-04-09T00:35:35.000Z' + }), + operator: OperatorDetailsSchema +}) + +export const DetailedAvsRegistrationSchema = AvsRegistrationSchema.merge(AvsMetaDataFields).extend({ + curatedMetadata: CuratedMetadataSchema.omit({ avsAddress: true }) + .optional() + .describe('Curated metadata information for AVS'), + restakeableStrategies: z + .array(EthereumAddressSchema) + .describe('The list of restakeable strategies for AVS'), + totalStakers: z.number().describe('Total number of stakers').openapi({ example: 80000 }), + totalOperators: z.number().describe('Total number of operators').openapi({ example: 200 }), + tvlEth: z.string().describe('Total TVL in ETH').openapi({ example: '3000000' }), + createdAtBlock: z + .number() + .describe('The block number at which the AVS was created') + .openapi({ example: 19592323 }), + updatedAtBlock: z + .number() + .describe('The block number at which the AVS was last updated') + .openapi({ example: 19592323 }), + createdAt: z.string().describe('The time stamp at which the AVS was created').openapi({ + example: '2024-04-05T21:49:59.000Z' + }), + updatedAt: z.string().describe('The time stamp at which the AVS was last updated').openapi({ + example: '2024-04-05T21:49:59.000Z' + }), + address: EthereumAddressSchema.describe('AVS service manager contract address').openapi({ + example: '0x870679e138bcdf293b7ff14dd44b70fc97e12fc0' + }), + rewardsSubmissions: z + .array(RewardsSubmissionSchema) + .describe('List of rewards submissions associated with AVS'), + operators: z + .array(OperatorSchema) + .describe('List of operators associated with the AVS registration') +}) + +export const RewardsSchema = z.object({ + avs: z.array( + z.object({ + avsAddress: EthereumAddressSchema.describe('AVS service manager contract address').openapi({ + example: '0x870679e138bcdf293b7ff14dd44b70fc97e12fc0' + }), + apy: z + .number() + .describe('Latest APY recorded for the AVS') + .openapi({ example: 0.15973119826488588 }) + }) + ), + strategies: z.array( + z.object({ + strategyAddress: EthereumAddressSchema.describe( + 'The contract address of the restaking strategy' + ).openapi({ + example: '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' + }), + apy: z.number().describe('APY of the restaking strategy').openapi({ example: 0.1 }) + }) + ), + aggregateApy: z + .number() + .describe('The aggregate APY across all strategies') + .openapi({ example: 1.0 }), + operatorEarningsEth: z + .string() + .describe('Total earnings for the operator in ETH') + .openapi({ example: '1000000000000000000' }) +}) + +export const OperatorWithRewardsResponseSchema = OperatorResponseSchema.extend({ + avsRegistrations: z + .array(DetailedAvsRegistrationSchema) + .describe('Detailed AVS registrations information for the operator'), + rewards: RewardsSchema.describe('The rewards information for the operator') +}) diff --git a/packages/openapi/src/apiResponseSchema/stakerResponse.ts b/packages/openapi/src/apiResponseSchema/stakerResponse.ts index 53b6d952..4f233800 100644 --- a/packages/openapi/src/apiResponseSchema/stakerResponse.ts +++ b/packages/openapi/src/apiResponseSchema/stakerResponse.ts @@ -3,16 +3,13 @@ import { EthereumAddressSchema } from '../../../api/src/schema/zod/schemas/base/ import { TvlSchema } from './base/tvlResponses' export const StakerSharesSchema = z.object({ - stakerAddress: EthereumAddressSchema.describe('The contract address of the staker').openapi({ - example: '0x0000006c21964af0d420af8992851a30fa13c68a' - }), strategyAddress: EthereumAddressSchema.describe( 'The contract address of the restaking strategy' ).openapi({ example: '0x93c4b944d05dfe6df7645a86cd2206016c51564a' }), shares: z .string() .describe('The amount of shares held in the strategy') - .openapi({ example: '40888428658906049' }) + .openapi({ example: '40000000000000000' }) }) export const StakerResponseSchema = z.object({ diff --git a/packages/openapi/src/apiResponseSchema/withdrawals/withdrawalsResponseSchema.ts b/packages/openapi/src/apiResponseSchema/withdrawals/withdrawalsResponseSchema.ts index b8eca831..2cee065f 100644 --- a/packages/openapi/src/apiResponseSchema/withdrawals/withdrawalsResponseSchema.ts +++ b/packages/openapi/src/apiResponseSchema/withdrawals/withdrawalsResponseSchema.ts @@ -6,10 +6,6 @@ export const WithdrawalsResponseSchema = z.object({ example: '0x9e6728ef0a8ad6009107a886047aae35bc5ed7deaa68580b0d1f8f67e3e5ed31' }), nonce: z.number().describe('The nonce of the withdrawal').openapi({ example: 0 }), - isCompleted: z - .boolean() - .describe('Indicates if the withdrawal is completed') - .openapi({ example: false }), stakerAddress: z .string() .describe('The address of the staker') @@ -33,10 +29,6 @@ export const WithdrawalsResponseSchema = z.object({ } ] }), - startBlock: z - .number() - .describe('The block number when the withdrawal was queued') - .openapi({ example: 19912470 }), createdAtBlock: z .number() .describe('The block number when the withdrawal was recorded by EigenExplorer') @@ -52,10 +44,6 @@ export const WithdrawalsResponseWithUpdateFields = WithdrawalsResponseSchema.ext .number() .describe('The block number when the withdrawal was last updated') .openapi({ example: 19912470 }), - createdAt: z - .string() - .describe('The time stamp when the withdrawal was recorded by EigenExplorer') - .openapi({ example: '2024-07-07T23:53:35.000Z' }), updatedAt: z .string() .describe('The time stamp when the withdrawal was last updated') diff --git a/packages/openapi/src/documentBase.ts b/packages/openapi/src/documentBase.ts index d8989602..ceb3c136 100644 --- a/packages/openapi/src/documentBase.ts +++ b/packages/openapi/src/documentBase.ts @@ -8,6 +8,7 @@ import { withdrawalsRoutes } from './routes/withdrawals' import { stakersRoutes } from './routes/stakers' import { depositsRoutes } from './routes/deposits' import { historicalRoutes } from './routes/historical' +import { rewardsRoutes } from './routes/rewards' export const document = createDocument({ openapi: '3.0.3', @@ -34,7 +35,8 @@ export const document = createDocument({ ...operatorsRoutes, ...withdrawalsRoutes, ...stakersRoutes, - ...depositsRoutes + ...depositsRoutes, + ...rewardsRoutes }, components: { schemas: {}, diff --git a/packages/openapi/src/routes/avs/getAvsByAddress.ts b/packages/openapi/src/routes/avs/getAvsByAddress.ts index 03efbf69..e7064887 100644 --- a/packages/openapi/src/routes/avs/getAvsByAddress.ts +++ b/packages/openapi/src/routes/avs/getAvsByAddress.ts @@ -1,12 +1,17 @@ import { openApiErrorResponses } from '../../apiResponseSchema/base/errorResponses' import z from '../../../../api/src/schema/zod' import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/base/ethereumAddress' -import { AvsSchema } from '../../apiResponseSchema/avs/avsResponse' import { ZodOpenApiOperationObject } from 'zod-openapi' import { WithTvlQuerySchema } from '../../../../api/src/schema/zod/schemas/withTvlQuery' import { WithCuratedMetadata } from '../../../../api/src/schema/zod/schemas/withCuratedMetadataQuery' +import { WithRewardsQuerySchema } from '../../../../api/src/schema/zod/schemas/withRewardsQuery' +import { AvsWithRewardsSchema } from '../../apiResponseSchema/avs/avsWithRewardsResponse' -const CombinedQuerySchema = z.object({}).merge(WithTvlQuerySchema).merge(WithCuratedMetadata) +const CombinedQuerySchema = z + .object({}) + .merge(WithTvlQuerySchema) + .merge(WithCuratedMetadata) + .merge(WithRewardsQuerySchema) const AvsAddressParam = z.object({ address: EthereumAddressSchema.describe('AVS service manager contract address').openapi({ @@ -28,7 +33,7 @@ export const getAvsByAddress: ZodOpenApiOperationObject = { description: 'The AVS record found.', content: { 'application/json': { - schema: AvsSchema + schema: AvsWithRewardsSchema } } }, diff --git a/packages/openapi/src/routes/avs/getAvsOperatorsByAddress.ts b/packages/openapi/src/routes/avs/getAvsOperatorsByAddress.ts index b9e6b8d5..f8ee8f0a 100644 --- a/packages/openapi/src/routes/avs/getAvsOperatorsByAddress.ts +++ b/packages/openapi/src/routes/avs/getAvsOperatorsByAddress.ts @@ -3,7 +3,6 @@ import z from '../../../../api/src/schema/zod' import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/base/ethereumAddress' import { ZodOpenApiOperationObject } from 'zod-openapi' import { PaginationQuerySchema } from '../../../../api/src/schema/zod/schemas/paginationQuery' -import { OperatorResponseSchema } from '../../apiResponseSchema/operatorResponse' import { PaginationMetaResponsesSchema } from '../../apiResponseSchema/base/paginationMetaResponses' import { WithTvlQuerySchema } from '../../../../api/src/schema/zod/schemas/withTvlQuery' import { AvsOperatorResponseSchema } from '../../apiResponseSchema/avs/avsOperatorResponse' diff --git a/packages/openapi/src/routes/avs/getAvsRewards.ts b/packages/openapi/src/routes/avs/getAvsRewards.ts new file mode 100644 index 00000000..f3087063 --- /dev/null +++ b/packages/openapi/src/routes/avs/getAvsRewards.ts @@ -0,0 +1,32 @@ +import { openApiErrorResponses } from '../../apiResponseSchema/base/errorResponses' +import z from '../../../../api/src/schema/zod' +import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/base/ethereumAddress' +import { ZodOpenApiOperationObject } from 'zod-openapi' +import { AvsRewardsSchema } from '../../apiResponseSchema/avs/avsRewardsResponse' + +const AvsAddressParam = z.object({ + address: EthereumAddressSchema.describe('AVS service manager contract address').openapi({ + example: '0x870679e138bcdf293b7ff14dd44b70fc97e12fc0' + }) +}) + +export const getAvsRewards: ZodOpenApiOperationObject = { + operationId: 'getAvsRewards', + summary: 'Retrieve all rewards for a given AVS address', + description: 'Returns a list of all rewards for a given AVS address.', + tags: ['AVS'], + requestParams: { + path: AvsAddressParam + }, + responses: { + '200': { + description: 'The rewards found for the AVS.', + content: { + 'application/json': { + schema: AvsRewardsSchema + } + } + }, + ...openApiErrorResponses + } +} diff --git a/packages/openapi/src/routes/avs/getAvsStakersByAddress.ts b/packages/openapi/src/routes/avs/getAvsStakersByAddress.ts index a972244c..1c2effc0 100644 --- a/packages/openapi/src/routes/avs/getAvsStakersByAddress.ts +++ b/packages/openapi/src/routes/avs/getAvsStakersByAddress.ts @@ -4,9 +4,9 @@ import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/ba import { ZodOpenApiOperationObject } from 'zod-openapi' import { PaginationQuerySchema } from '../../../../api/src/schema/zod/schemas/paginationQuery' import { PaginationMetaResponsesSchema } from '../../apiResponseSchema/base/paginationMetaResponses' -import { StakerResponseSchema } from '../../apiResponseSchema/stakerResponse' import { UpdatedSinceQuerySchema } from '../../../../api/src/schema/zod/schemas/updatedSinceQuery' import { WithTvlQuerySchema } from '../../../../api/src/schema/zod/schemas/withTvlQuery' +import { AvsStakerSchema } from '../../apiResponseSchema/avs/avsStakerResponse' const AvsAddressParam = z.object({ address: EthereumAddressSchema.describe('AVS service manager contract address').openapi({ @@ -21,7 +21,7 @@ const CombinedQuerySchema = z .merge(PaginationQuerySchema) const AvsStakerResponseSchema = z.object({ - data: z.array(StakerResponseSchema), + data: z.array(AvsStakerSchema), meta: PaginationMetaResponsesSchema }) diff --git a/packages/openapi/src/routes/avs/index.ts b/packages/openapi/src/routes/avs/index.ts index 78b2266f..a54942d1 100644 --- a/packages/openapi/src/routes/avs/index.ts +++ b/packages/openapi/src/routes/avs/index.ts @@ -4,6 +4,7 @@ import { getAllAvs } from './getAllAvs' import { getAvsByAddress } from './getAvsByAddress' import { getAvsStakersByAddress } from './getAvsStakersByAddress' import { getAvsOperatorsByAddress } from './getAvsOperatorsByAddress' +import { getAvsRewards } from './getAVSRewards' export const avsRoutes: ZodOpenApiPathsObject = { '/avs': { get: getAllAvs }, @@ -12,5 +13,6 @@ export const avsRoutes: ZodOpenApiPathsObject = { }, '/avs/{address}': { get: getAvsByAddress }, '/avs/{address}/stakers': { get: getAvsStakersByAddress }, - '/avs/{address}/operators': { get: getAvsOperatorsByAddress } + '/avs/{address}/operators': { get: getAvsOperatorsByAddress }, + '/avs/{address}/rewards': { get: getAvsRewards } } diff --git a/packages/openapi/src/routes/historical/getHistoricalTvlRestaking.ts b/packages/openapi/src/routes/historical/getHistoricalTvlRestaking.ts index b5a7b37a..26fe424d 100644 --- a/packages/openapi/src/routes/historical/getHistoricalTvlRestaking.ts +++ b/packages/openapi/src/routes/historical/getHistoricalTvlRestaking.ts @@ -10,9 +10,11 @@ const HistoricalIndividualStrategyTvlCombinedResponseSchema = z.object({ }) const RestakingStrategyAddressParam = z.object({ - address: EthereumAddressSchema.describe('The address of the strategy').openapi({ - example: '0x54945180dB7943c0ed0FEE7EdaB2Bd24620256bc' - }) + address: EthereumAddressSchema.describe('The contract address of the restaking strategy').openapi( + { + example: '0x54945180dB7943c0ed0FEE7EdaB2Bd24620256bc' + } + ) }) export const getHistoricalTvlRestaking: ZodOpenApiOperationObject = { diff --git a/packages/openapi/src/routes/metrics/getTvlByStrategy.ts b/packages/openapi/src/routes/metrics/getTvlByStrategy.ts index bb64c652..88cfa75f 100644 --- a/packages/openapi/src/routes/metrics/getTvlByStrategy.ts +++ b/packages/openapi/src/routes/metrics/getTvlByStrategy.ts @@ -3,21 +3,24 @@ import { openApiErrorResponses } from '../../apiResponseSchema/base/errorRespons import { IndividualStrategyTvlResponseSchema } from '../../apiResponseSchema/metrics/tvlResponse' import z from '../../../../api/src/schema/zod' import { WithChangeQuerySchema } from '../../../../api/src/schema/zod/schemas/withChangeQuery' +import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/base/ethereumAddress' -const RestakingStrategyNameParam = z.object({ - strategy: z.string().describe('The name of the restaking strategy').openapi({ example: 'cbETH' }) +const RestakingStrategyAddressParam = z.object({ + strategy: EthereumAddressSchema.describe('The address of the restaking strategy').openapi({ + example: '0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6' + }) }) const QuerySchema = z.object({}).merge(WithChangeQuerySchema) export const getTvlRestakingMetricByStrategy: ZodOpenApiOperationObject = { operationId: 'getTvlRestakingMetricByStrategy', - summary: 'Retrieve a strategy TVL by name', + summary: 'Retrieve a strategy TVL by address', description: 'Returns the total value locked (TVL) in a specific LST strategy.', tags: ['Metrics'], requestParams: { query: QuerySchema, - path: RestakingStrategyNameParam + path: RestakingStrategyAddressParam }, responses: { '200': { diff --git a/packages/openapi/src/routes/operators/getAllOperatorAddresses.ts b/packages/openapi/src/routes/operators/getAllOperatorAddresses.ts new file mode 100644 index 00000000..25f9b3ed --- /dev/null +++ b/packages/openapi/src/routes/operators/getAllOperatorAddresses.ts @@ -0,0 +1,42 @@ +import { openApiErrorResponses } from '../../apiResponseSchema/base/errorResponses' +import z from '../../../../api/src/schema/zod' +import { ZodOpenApiOperationObject } from 'zod-openapi' +import { PaginationQuerySchema } from '../../../../api/src/schema/zod/schemas/paginationQuery' +import { PaginationMetaResponsesSchema } from '../../apiResponseSchema/base/paginationMetaResponses' +import { + SearchByText, + SearchMode +} from '../../../../api/src/schema/zod/schemas/separateSearchQueries' +import { OperatorAddressSchema } from '../../apiResponseSchema/operator/operatorAddressResponse' + +const OperatorAddressResponseSchema = z.object({ + data: z.array(OperatorAddressSchema), + meta: PaginationMetaResponsesSchema +}) + +const CombinedQuerySchema = z + .object({}) + .merge(SearchMode) + .merge(SearchByText) + .merge(PaginationQuerySchema) + +export const getAllOperatorAddresses: ZodOpenApiOperationObject = { + operationId: 'getAllOperatorAddresses', + summary: 'Retrieve all operator addresses', + description: 'Returns a list of all operator addresses. This page supports pagination.', + tags: ['Operators'], + requestParams: { + query: CombinedQuerySchema + }, + responses: { + '200': { + description: 'The list of operator addresses.', + content: { + 'application/json': { + schema: OperatorAddressResponseSchema + } + } + }, + ...openApiErrorResponses + } +} diff --git a/packages/openapi/src/routes/operators/getAllOperators.ts b/packages/openapi/src/routes/operators/getAllOperators.ts index 11b1dc35..de2e763b 100644 --- a/packages/openapi/src/routes/operators/getAllOperators.ts +++ b/packages/openapi/src/routes/operators/getAllOperators.ts @@ -3,7 +3,6 @@ import z from '../../../../api/src/schema/zod' import { PaginationQuerySchema } from '../../../../api/src/schema/zod/schemas/paginationQuery' import { openApiErrorResponses } from '../../apiResponseSchema/base/errorResponses' import { PaginationMetaResponsesSchema } from '../../apiResponseSchema/base/paginationMetaResponses' -import { OperatorResponseSchema } from '../../apiResponseSchema/operatorResponse' import { WithTvlQuerySchema } from '../../../../api/src/schema/zod/schemas/withTvlQuery' import { SortByApy, @@ -12,6 +11,7 @@ import { SortByTvl } from '../../../../api/src/schema/zod/schemas/separateSortingQueries' import { SearchByText } from '../../../../api/src/schema/zod/schemas/separateSearchQueries' +import { OperatorResponseSchema } from '../../apiResponseSchema/operator/operatorResponse' const AllOperatorsResponseSchema = z.object({ data: z.array(OperatorResponseSchema), diff --git a/packages/openapi/src/routes/operators/getOperatorByAddress.ts b/packages/openapi/src/routes/operators/getOperatorByAddress.ts index 1fb98d3b..75c6ea61 100644 --- a/packages/openapi/src/routes/operators/getOperatorByAddress.ts +++ b/packages/openapi/src/routes/operators/getOperatorByAddress.ts @@ -2,9 +2,10 @@ import { ZodOpenApiOperationObject } from 'zod-openapi' import { openApiErrorResponses } from '../../apiResponseSchema/base/errorResponses' import z from '../../../../api/src/schema/zod' import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/base/ethereumAddress' -import { OperatorResponseSchema } from '../../apiResponseSchema/operatorResponse' import { WithTvlQuerySchema } from '../../../../api/src/schema/zod/schemas/withTvlQuery' import { WithAdditionalDataQuerySchema } from '../../../../api/src/schema/zod/schemas/withAdditionalDataQuery' +import { WithRewardsQuerySchema } from '../../../../api/src/schema/zod/schemas/withRewardsQuery' +import { OperatorWithRewardsResponseSchema } from '../../apiResponseSchema/operator/operatorWithRewardsResponse' const OperatorAddressParam = z.object({ address: EthereumAddressSchema.describe('The address of the operator').openapi({ @@ -16,6 +17,7 @@ const CombinedQuerySchema = z .object({}) .merge(WithTvlQuerySchema) .merge(WithAdditionalDataQuerySchema) + .merge(WithRewardsQuerySchema) export const getOperatorByAddress: ZodOpenApiOperationObject = { operationId: 'getOperatorByAddress', @@ -31,7 +33,7 @@ export const getOperatorByAddress: ZodOpenApiOperationObject = { description: 'The record of the requested operator.', content: { 'application/json': { - schema: OperatorResponseSchema + schema: OperatorWithRewardsResponseSchema } } }, diff --git a/packages/openapi/src/routes/operators/getOperatorRewards.ts b/packages/openapi/src/routes/operators/getOperatorRewards.ts new file mode 100644 index 00000000..36b7e5eb --- /dev/null +++ b/packages/openapi/src/routes/operators/getOperatorRewards.ts @@ -0,0 +1,33 @@ +import { openApiErrorResponses } from '../../apiResponseSchema/base/errorResponses' +import z from '../../../../api/src/schema/zod' +import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/base/ethereumAddress' +import { ZodOpenApiOperationObject } from 'zod-openapi' +import { OperatorRewardsSchema } from '../../apiResponseSchema/operator/operatorRewardsResponse' + +const OperatorAddressParam = z.object({ + address: EthereumAddressSchema.describe('The address of the operator').openapi({ + example: '0x00107cfdeaddc0a3160ed2f6fedd627f313e7b1a' + }) +}) + +export const getOperatorRewards: ZodOpenApiOperationObject = { + operationId: 'getOperatorRewards', + summary: 'Retrieve rewards info for an operator', + description: + 'Returns a list of strategies that the Operator is rewarded for, and the tokens they are rewarded in.', + tags: ['Operators'], + requestParams: { + path: OperatorAddressParam + }, + responses: { + '200': { + description: 'The reward strategies and tokens found for the Operator.', + content: { + 'application/json': { + schema: OperatorRewardsSchema + } + } + }, + ...openApiErrorResponses + } +} diff --git a/packages/openapi/src/routes/operators/index.ts b/packages/openapi/src/routes/operators/index.ts index 1b689780..0b5fa5e0 100644 --- a/packages/openapi/src/routes/operators/index.ts +++ b/packages/openapi/src/routes/operators/index.ts @@ -1,8 +1,12 @@ import { ZodOpenApiPathsObject } from 'zod-openapi' import { getAllOperators } from './getAllOperators' import { getOperatorByAddress } from './getOperatorByAddress' +import { getAllOperatorAddresses } from './getAllOperatorAddresses' +import { getOperatorRewards } from './getOperatorRewards' export const operatorsRoutes: ZodOpenApiPathsObject = { '/operators': { get: getAllOperators }, - '/operators/{address}': { get: getOperatorByAddress } + '/operators/addresses': { get: getAllOperatorAddresses }, + '/operators/{address}': { get: getOperatorByAddress }, + '/operators/{address}/rewards': { get: getOperatorRewards } } diff --git a/packages/openapi/src/routes/rewards/getStrategies.ts b/packages/openapi/src/routes/rewards/getStrategies.ts new file mode 100644 index 00000000..17cc7ef7 --- /dev/null +++ b/packages/openapi/src/routes/rewards/getStrategies.ts @@ -0,0 +1,23 @@ +import { ZodOpenApiOperationObject } from 'zod-openapi' +import { openApiErrorResponses } from '../../apiResponseSchema/base/errorResponses' +import { RewardsTokenSchema } from '../../apiResponseSchema/base/rewardTokensResponse' + +export const getStrategies: ZodOpenApiOperationObject = { + operationId: 'getStrategies', + summary: 'Retrieve all strategies with their reward tokens', + description: + 'Returns a list of strategies with their corresponding reward tokens, including strategy addresses and associated token addresses.', + tags: ['Rewards'], + requestParams: {}, + responses: { + '200': { + description: 'List of strategies along with associated reward tokens.', + content: { + 'application/json': { + schema: RewardsTokenSchema + } + } + }, + ...openApiErrorResponses + } +} diff --git a/packages/openapi/src/routes/rewards/index.ts b/packages/openapi/src/routes/rewards/index.ts new file mode 100644 index 00000000..c42bf2da --- /dev/null +++ b/packages/openapi/src/routes/rewards/index.ts @@ -0,0 +1,6 @@ +import { ZodOpenApiPathsObject } from 'zod-openapi' +import { getStrategies } from './getStrategies' + +export const rewardsRoutes: ZodOpenApiPathsObject = { + '/rewards/strategies': { get: getStrategies } +} From 8068ef945fdd42152847f64dba59aa4f7bfa21f4 Mon Sep 17 00:00:00 2001 From: Surbhit Agrawal <82264758+surbhit14@users.noreply.github.com> Date: Tue, 3 Dec 2024 10:45:02 +0530 Subject: [PATCH 08/17] Use live TVL value (#288) --- packages/api/src/routes/metrics/metricController.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/api/src/routes/metrics/metricController.ts b/packages/api/src/routes/metrics/metricController.ts index f8bced06..d38adcd4 100644 --- a/packages/api/src/routes/metrics/metricController.ts +++ b/packages/api/src/routes/metrics/metricController.ts @@ -2094,13 +2094,9 @@ async function doGetRestakingRatio( async function doGetDeploymentRatio( withChange: boolean ): Promise { - const tvlRestaking = (await doGetTvl(withChange)).tvlRestaking - const tvlBeaconChain = await doGetTvlBeaconChain(withChange) - - const restakingTvlValue = extractTvlValue(tvlRestaking) - const beaconChainTvlValue = extractTvlValue(tvlBeaconChain) - - const totalTvl = restakingTvlValue + beaconChainTvlValue + const totalTvl = + extractTvlValue((await doGetTvl(false)).tvlRestaking) + + extractTvlValue(await doGetTvlBeaconChain(false)) const ethPrices = await fetchCurrentEthPrices() const lastMetricsTimestamps = await prisma.metricOperatorStrategyUnit.groupBy({ @@ -2134,6 +2130,9 @@ async function doGetDeploymentRatio( return currentDeploymentRatio as RatioWithoutChange } + const tvlRestaking = (await doGetTvl(withChange)).tvlRestaking + const tvlBeaconChain = await doGetTvlBeaconChain(withChange) + const tvlEth24hChange = (tvlRestaking as TvlWithChange).change24h.value + (tvlBeaconChain as TvlWithChange).change24h.value From ac6d6789b46315f36053ede3a939e99fdbb15814 Mon Sep 17 00:00:00 2001 From: Gowtham Sundaresan <131300352+gowthamsundaresan@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:37:28 +0530 Subject: [PATCH 09/17] [Feat] - Rewards API v2 (#281) * add staker rewards tracking and api * return token-wise apy for staker, operator and avs * chore: cleanup logs * update apy monitors to capture max apy instead of aggegate apy * add auth routes for tracking users * add auth routes for wallet login validation and retrieval * seed full staker snapshots and historical data for users * fix: accumulation of snapshot dates & double counting of reward records * update staker routes to match new schema * add signature verification for wallet signup * optimize response for operators with rewards * chore: cleanup stale imports --- .../api/src/routes/auth/authController.ts | 151 +++++++ packages/api/src/routes/auth/authRoutes.ts | 13 + packages/api/src/routes/avs/avsController.ts | 93 +++- packages/api/src/routes/index.ts | 2 + .../routes/operators/operatorController.ts | 159 ++++--- .../src/routes/stakers/stakerController.ts | 295 ++++++++++++- packages/api/src/schema/zod/schemas/auth.ts | 19 + .../migration.sql | 78 ++++ packages/prisma/schema.prisma | 55 ++- .../seedLogsDistributionRootSubmitted.ts | 90 ++++ .../src/events/seedLogsRewardsSubmissions.ts | 4 +- packages/seeder/src/index.ts | 8 +- .../src/metrics/seedMetricsStakerRewards.ts | 399 ++++++++++++++++++ packages/seeder/src/monitors/avsApy.ts | 48 +-- packages/seeder/src/monitors/operatorApy.ts | 43 +- packages/seeder/src/seedAvs.ts | 2 +- packages/seeder/src/seedOperators.ts | 2 +- .../seeder/src/seedStakerRewardSnapshots.ts | 178 ++++++++ 18 files changed, 1483 insertions(+), 156 deletions(-) create mode 100644 packages/api/src/routes/auth/authController.ts create mode 100644 packages/api/src/routes/auth/authRoutes.ts create mode 100644 packages/api/src/schema/zod/schemas/auth.ts create mode 100644 packages/prisma/migrations/20241128092502_include_staker_rewards/migration.sql create mode 100644 packages/seeder/src/events/seedLogsDistributionRootSubmitted.ts create mode 100644 packages/seeder/src/metrics/seedMetricsStakerRewards.ts create mode 100644 packages/seeder/src/seedStakerRewardSnapshots.ts diff --git a/packages/api/src/routes/auth/authController.ts b/packages/api/src/routes/auth/authController.ts new file mode 100644 index 00000000..e712f813 --- /dev/null +++ b/packages/api/src/routes/auth/authController.ts @@ -0,0 +1,151 @@ +import type { Request, Response } from 'express' +import { handleAndReturnErrorResponse } from '../../schema/errors' +import { EthereumAddressSchema } from '../../schema/zod/schemas/base/ethereumAddress' +import { RegisterUserBodySchema, RequestHeadersSchema } from '../../schema/zod/schemas/auth' +import { verifyMessage } from 'viem' +import prisma from '../../utils/prismaClient' +import crypto from 'node:crypto' + +/** + * Function for route /auth/users/:address/check-status + * Protected route, returns whether a given address is registered on EE, if they are an EL staker & if we track their rewards + * + * @param req + * @param res + * @returns + */ +export async function checkUserStatus(req: Request, res: Response) { + const headerCheck = RequestHeadersSchema.safeParse(req.headers) + if (!headerCheck.success) { + return handleAndReturnErrorResponse(req, res, headerCheck.error) + } + + const paramCheck = EthereumAddressSchema.safeParse(req.params.address) + if (!paramCheck.success) { + return handleAndReturnErrorResponse(req, res, paramCheck.error) + } + + try { + const apiToken = headerCheck.data['X-API-Token'] + const authToken = process.env.EE_AUTH_TOKEN + + if (!apiToken || apiToken !== authToken) { + throw new Error('Unauthorized access.') + } + + const { address } = req.params + + const [user, staker] = await Promise.all([ + prisma.user.findUnique({ + where: { address: address.toLowerCase() } + }), + prisma.staker.findUnique({ + where: { address: address.toLowerCase() } + }) + ]) + + const isRegistered = !!user + const isStaker = !!staker + const isTracked = !!user?.isTracked + + res.send({ isRegistered, isStaker, isTracked }) + } catch (error) { + handleAndReturnErrorResponse(req, res, error) + } +} + +/** + * Function for route /auth/users/:address/nonce + * Protected route, generates a nonce to be used by frontend for registering a new user via wallet + * + * @param req + * @param res + * @returns + */ +export async function generateNonce(req: Request, res: Response) { + const headerCheck = RequestHeadersSchema.safeParse(req.headers) + if (!headerCheck.success) { + return handleAndReturnErrorResponse(req, res, headerCheck.error) + } + + try { + const apiToken = headerCheck.data['X-API-Token'] + const authToken = process.env.EE_AUTH_TOKEN + + if (!apiToken || apiToken !== authToken) { + throw new Error('Unauthorized access.') + } + + const nonce = `0x${crypto.randomBytes(32).toString('hex')}` + + res.send({ nonce }) + } catch (error) { + handleAndReturnErrorResponse(req, res, error) + } +} + +/** + * Function for route /auth/users/:address/register + * Protected route, adds an address to the User table if it doesn't exist + * + * @param req + * @param res + * @returns + */ +export async function registerUser(req: Request, res: Response) { + const headerCheck = RequestHeadersSchema.safeParse(req.headers) + if (!headerCheck.success) { + return handleAndReturnErrorResponse(req, res, headerCheck.error) + } + + const paramCheck = EthereumAddressSchema.safeParse(req.params.address) + if (!paramCheck.success) { + return handleAndReturnErrorResponse(req, res, paramCheck.error) + } + + const bodyCheck = RegisterUserBodySchema.safeParse(req.body) + if (!bodyCheck.success) { + return handleAndReturnErrorResponse(req, res, bodyCheck.error) + } + + try { + const apiToken = headerCheck.data['X-API-Token'] + const authToken = process.env.EE_AUTH_TOKEN + + if (!apiToken || apiToken !== authToken) { + throw new Error('Unauthorized access.') + } + + const { address } = req.params + const { signature, nonce } = bodyCheck.data + + const message = `Welcome to EigenExplorer!\n\nPlease sign this message to verify your wallet ownership.\n\nNonce: ${nonce}` + + const isValid = await verifyMessage({ + address: address as `0x${string}`, + message, + signature: signature as `0x${string}` + }) + + if (!isValid) { + throw new Error('Invalid signature') + } + + const existingUser = await prisma.user.findUnique({ + where: { address: address.toLowerCase() } + }) + + if (!existingUser) { + await prisma.user.create({ + data: { + address: address.toLowerCase(), + isTracked: false + } + }) + } + + res.send({ isNewUser: !existingUser }) + } catch (error) { + handleAndReturnErrorResponse(req, res, error) + } +} diff --git a/packages/api/src/routes/auth/authRoutes.ts b/packages/api/src/routes/auth/authRoutes.ts new file mode 100644 index 00000000..dbf3bbbf --- /dev/null +++ b/packages/api/src/routes/auth/authRoutes.ts @@ -0,0 +1,13 @@ +import express from 'express' +import routeCache from 'route-cache' +import { checkUserStatus, generateNonce, registerUser } from './authController' + +const router = express.Router() + +// API routes for /auth + +router.get('/users/:address/check-status', routeCache.cacheSeconds(30), checkUserStatus) +router.get('/users/:address/nonce', routeCache.cacheSeconds(10), generateNonce) +router.post('/users/:address/register', routeCache.cacheSeconds(10), registerUser) + +export default router diff --git a/packages/api/src/routes/avs/avsController.ts b/packages/api/src/routes/avs/avsController.ts index 16c70934..f1c0e394 100644 --- a/packages/api/src/routes/avs/avsController.ts +++ b/packages/api/src/routes/avs/avsController.ts @@ -557,7 +557,7 @@ export async function getAVSRewards(req: Request, res: Response) { currentSubmission.rewardsSubmissionHash !== submission.rewardsSubmissionHash ) { if (currentSubmission) { - currentSubmission.totalAmount = currentTotalAmount.toString() + currentSubmission.totalAmount = currentTotalAmount.toFixed(0) result.submissions.push(currentSubmission) result.totalSubmissions++ } @@ -596,13 +596,13 @@ export async function getAVSRewards(req: Request, res: Response) { currentSubmission.strategies.push({ strategyAddress, multiplier: submission.multiplier?.toString() || '0', - amount: amount.toString() + amount: amount.toFixed(0) }) } // Add final submission if (currentSubmission) { - currentSubmission.totalAmount = currentTotalAmount.toString() + currentSubmission.totalAmount = currentTotalAmount.toFixed(0) result.submissions.push(currentSubmission) result.totalSubmissions++ result.totalRewards += currentTotalAmountEth.toNumber() // 18 decimals @@ -729,6 +729,14 @@ export function getAvsSearchQuery( // biome-ignore lint/suspicious/noExplicitAny: async function calculateAvsApy(avs: any) { try { + const strategyApyMap: Map< + string, + { + apy: number + tokens: Map + } + > = new Map() + const tokenPrices = await fetchTokenPrices() const strategiesWithSharesUnderlying = await getStrategiesWithShareUnderlying() @@ -741,12 +749,21 @@ async function calculateAvsApy(avs: any) { const tvlStrategiesEth = sharesToTVLStrategies(shares, strategiesWithSharesUnderlying) // Iterate through each strategy and calculate all its rewards - const strategiesApy = avs.restakeableStrategies.map((strategyAddress) => { + for (const strategyAddress of avs.restakeableStrategies) { const strategyTvl = tvlStrategiesEth[strategyAddress.toLowerCase()] || 0 - if (strategyTvl === 0) return { strategyAddress, apy: 0 } + if (strategyTvl === 0) continue + + const tokenApyMap: Map = new Map() + const tokenRewards: Map< + string, + { + totalRewardsEth: Prisma.Prisma.Decimal + totalDuration: number + } + > = new Map() - let totalRewardsEth = new Prisma.Prisma.Decimal(0) - let totalDuration = 0 + let strategyTotalRewardsEth = new Prisma.Prisma.Decimal(0) + let strategyTotalDuration = 0 // Find all reward submissions attributable to the strategy const relevantSubmissions = avs.rewardSubmissions.filter( @@ -773,28 +790,60 @@ async function calculateAvsApy(avs: any) { .mul(submission.multiplier) .div(new Prisma.Prisma.Decimal(10).pow(18)) - totalRewardsEth = totalRewardsEth.add(rewardIncrementEth) // No decimals - totalDuration += submission.duration - } + // Accumulate token-specific rewards and duration + const tokenData = tokenRewards.get(rewardTokenAddress) || { + totalRewardsEth: new Prisma.Prisma.Decimal(0), + totalDuration: 0 + } + tokenData.totalRewardsEth = tokenData.totalRewardsEth.add(rewardIncrementEth) + tokenData.totalDuration += submission.duration + tokenRewards.set(rewardTokenAddress, tokenData) - if (totalDuration === 0) { - return { strategyAddress, apy: 0 } + // Accumulate strategy totals + strategyTotalRewardsEth = strategyTotalRewardsEth.add(rewardIncrementEth) + strategyTotalDuration += submission.duration } - // Annualize the reward basis its duration to find yearly APY - const rewardRate = totalRewardsEth.toNumber() / strategyTvl - const annualizedRate = rewardRate * ((365 * 24 * 60 * 60) / totalDuration) - const apy = annualizedRate * 100 + if (strategyTotalDuration === 0) continue - return { strategyAddress, apy } - }) + // Calculate token APYs using accumulated values + tokenRewards.forEach((data, tokenAddress) => { + if (data.totalDuration !== 0) { + const tokenRewardRate = data.totalRewardsEth.toNumber() / strategyTvl + const tokenAnnualizedRate = tokenRewardRate * ((365 * 24 * 60 * 60) / data.totalDuration) + const tokenApy = tokenAnnualizedRate * 100 + + tokenApyMap.set(tokenAddress, tokenApy) + } + }) + + // Calculate overall strategy APY summing token APYs + const strategyApy = Array.from(tokenApyMap.values()).reduce((sum, apy) => sum + apy, 0) + + // Update strategy rewards map + const currentStrategyRewards = { + apy: 0, + tokens: new Map() + } - // Calculate aggregate APYs across strategies - const aggregateApy = strategiesApy.reduce((sum, strategy) => sum + strategy.apy, 0) + tokenApyMap.forEach((apy, tokenAddress) => { + currentStrategyRewards.tokens.set(tokenAddress, apy) + }) + currentStrategyRewards.apy += strategyApy + strategyApyMap.set(strategyAddress, currentStrategyRewards) + } return { - strategies: strategiesApy, - aggregateApy + strategyApys: Array.from(strategyApyMap.entries()).map(([strategyAddress, data]) => { + return { + strategyAddress, + apy: data.apy, + tokens: Array.from(data.tokens.entries()).map(([tokenAddress, apy]) => ({ + tokenAddress, + apy + })) + } + }) } } catch {} } diff --git a/packages/api/src/routes/index.ts b/packages/api/src/routes/index.ts index a900ffb1..c151d64c 100644 --- a/packages/api/src/routes/index.ts +++ b/packages/api/src/routes/index.ts @@ -7,6 +7,7 @@ import metricRoutes from './metrics/metricRoutes' import withdrawalRoutes from './withdrawals/withdrawalRoutes' import depositRoutes from './deposits/depositRoutes' import auxiliaryRoutes from './auxiliary/auxiliaryRoutes' +import authRoutes from './auth/authRoutes' import rewardRoutes from './rewards/rewardRoutes' import { rateLimiter } from '../utils/auth' @@ -29,6 +30,7 @@ apiRouter.use('/metrics', rateLimiter, metricRoutes) apiRouter.use('/withdrawals', rateLimiter, withdrawalRoutes) apiRouter.use('/deposits', rateLimiter, depositRoutes) apiRouter.use('/aux', rateLimiter, auxiliaryRoutes) +apiRouter.use('/auth', rateLimiter, authRoutes) apiRouter.use('/rewards', rateLimiter, rewardRoutes) export default apiRouter diff --git a/packages/api/src/routes/operators/operatorController.ts b/packages/api/src/routes/operators/operatorController.ts index dc178cb5..b9b9ff86 100644 --- a/packages/api/src/routes/operators/operatorController.ts +++ b/packages/api/src/routes/operators/operatorController.ts @@ -141,7 +141,6 @@ export async function getAllOperators(req: Request, res: Response) { * @param res */ export async function getOperator(req: Request, res: Response) { - // Validate pagination query const result = WithTvlQuerySchema.and(WithAdditionalDataQuerySchema) .and(WithRewardsQuerySchema) .safeParse(req.query) @@ -194,6 +193,7 @@ export async function getOperator(req: Request, res: Response) { ...(withRewards ? { address: true, + maxApy: true, rewardSubmissions: true, restakeableStrategies: true, operators: { @@ -221,7 +221,7 @@ export async function getOperator(req: Request, res: Response) { const avsRegistrations = operator.avs.map((registration) => ({ avsAddress: registration.avsAddress, isActive: registration.isActive, - ...(withAvsData && registration.avs ? registration.avs : {}) + ...(withAvsData && registration.avs ? { ...registration.avs, operators: undefined } : {}) })) const strategiesWithSharesUnderlying = withTvl ? await getStrategiesWithShareUnderlying() : [] @@ -551,8 +551,31 @@ export function getOperatorSearchQuery( // biome-ignore lint/suspicious/noExplicitAny: async function calculateOperatorApy(operator: any) { try { - const avsRewardsMap: Map = new Map() - const strategyRewardsMap: Map = new Map() + const avsApyMap: Map< + string, + { + avsAddress: string + maxApy: number + strategyApys: { + strategyAddress: string + apy: number + tokens: { + tokenAddress: string + apy: number + }[] + }[] + } + > = new Map() + const strategyApyMap: Map< + string, + { + apy: number + tokens: Map + } + > = new Map() + + const tokenPrices = await fetchTokenPrices() + const strategiesWithSharesUnderlying = await getStrategiesWithShareUnderlying() // Grab the all reward submissions that the Operator is eligible for basis opted strategies & AVSs const optedStrategyAddresses: Set = new Set( @@ -568,108 +591,114 @@ async function calculateOperatorApy(operator: any) { })) .filter((item) => item.eligibleRewards.length > 0) - if (!avsWithEligibleRewardSubmissions) { - return { - avs: [], - strategies: [], - aggregateApy: 0, - operatorEarningsEth: 0 - } - } - - let operatorEarningsEth = new Prisma.Prisma.Decimal(0) - - const tokenPrices = await fetchTokenPrices() - const strategiesWithSharesUnderlying = await getStrategiesWithShareUnderlying() + if (!avsWithEligibleRewardSubmissions || avsWithEligibleRewardSubmissions.length === 0) + return [] - // Calc aggregate APY for each AVS basis the opted-in strategies for (const avs of avsWithEligibleRewardSubmissions) { - let aggregateApy = 0 - - // Get share amounts for each restakeable strategy const shares = withOperatorShares(avs.avs.operators).filter( - (s) => avs.avs.restakeableStrategies.indexOf(s.strategyAddress.toLowerCase()) !== -1 + (s) => avs.avs.restakeableStrategies?.indexOf(s.strategyAddress.toLowerCase()) !== -1 ) // Fetch the AVS tvl for each strategy const tvlStrategiesEth = sharesToTVLStrategies(shares, strategiesWithSharesUnderlying) // Iterate through each strategy and calculate all its rewards - for (const strategyAddress of optedStrategyAddresses) { + for (const strategyAddress of avs.avs.restakeableStrategies) { const strategyTvl = tvlStrategiesEth[strategyAddress.toLowerCase()] || 0 if (strategyTvl === 0) continue - let totalRewardsEth = new Prisma.Prisma.Decimal(0) - let totalDuration = 0 + const tokenApyMap: Map = new Map() + const tokenRewards: Map< + string, + { + totalRewardsEth: Prisma.Prisma.Decimal + totalDuration: number + } + > = new Map() + + let strategyTotalRewardsEth = new Prisma.Prisma.Decimal(0) + let strategyTotalDuration = 0 - // Find all reward submissions attributable to the strategy - const relevantSubmissions = avs.eligibleRewards.filter( + // Find all reward submissions for the strategy + const relevantSubmissions = avs.avs.rewardSubmissions.filter( (submission) => submission.strategyAddress.toLowerCase() === strategyAddress.toLowerCase() ) + // Calculate each reward amount for the strategy for (const submission of relevantSubmissions) { let rewardIncrementEth = new Prisma.Prisma.Decimal(0) const rewardTokenAddress = submission.token.toLowerCase() + // Normalize reward amount to its ETH price if (rewardTokenAddress) { const tokenPrice = tokenPrices.find( (tp) => tp.address.toLowerCase() === rewardTokenAddress ) rewardIncrementEth = submission.amount .mul(new Prisma.Prisma.Decimal(tokenPrice?.ethPrice ?? 0)) - .div(new Prisma.Prisma.Decimal(10).pow(tokenPrice?.decimals ?? 18)) // No decimals + .div(new Prisma.Prisma.Decimal(10).pow(tokenPrice?.decimals ?? 18)) } - // Multiply reward amount in ETH by the strategy weight + // Apply operator commission (90% of rewards) rewardIncrementEth = rewardIncrementEth .mul(submission.multiplier) .div(new Prisma.Prisma.Decimal(10).pow(18)) + .mul(90) + .div(100) - // Operator takes 10% in commission - const operatorFeesEth = rewardIncrementEth.mul(10).div(100) // No decimals - - operatorEarningsEth = operatorEarningsEth.add( - operatorFeesEth.mul(new Prisma.Prisma.Decimal(10).pow(18)) - ) // 18 decimals + // Accumulate token-specific rewards and duration + const tokenData = tokenRewards.get(rewardTokenAddress) || { + totalRewardsEth: new Prisma.Prisma.Decimal(0), + totalDuration: 0 + } + tokenData.totalRewardsEth = tokenData.totalRewardsEth.add(rewardIncrementEth) + tokenData.totalDuration += submission.duration + tokenRewards.set(rewardTokenAddress, tokenData) - totalRewardsEth = totalRewardsEth.add(rewardIncrementEth).sub(operatorFeesEth) // No decimals - totalDuration += submission.duration + // Accumulate strategy totals + strategyTotalRewardsEth = strategyTotalRewardsEth.add(rewardIncrementEth) + strategyTotalDuration += submission.duration } - if (totalDuration === 0) continue + if (strategyTotalDuration === 0) continue + + // Calculate token APYs + tokenRewards.forEach((data, tokenAddress) => { + if (data.totalDuration !== 0) { + const tokenRewardRate = data.totalRewardsEth.toNumber() / strategyTvl + const tokenAnnualizedRate = + tokenRewardRate * ((365 * 24 * 60 * 60) / data.totalDuration) + const tokenApy = tokenAnnualizedRate * 100 - // Annualize the reward basis its duration to find yearly APY - const rewardRate = totalRewardsEth.toNumber() / strategyTvl // No decimals - const annualizedRate = rewardRate * ((365 * 24 * 60 * 60) / totalDuration) - const apy = annualizedRate * 100 - aggregateApy += apy + tokenApyMap.set(tokenAddress, tokenApy) + } + }) - // Add strategy's APY to common strategy rewards store (across all Avs) - const currentStrategyApy = strategyRewardsMap.get(strategyAddress) || 0 - strategyRewardsMap.set(strategyAddress, currentStrategyApy + apy) + // Calculate overall strategy APY + const strategyApy = Array.from(tokenApyMap.values()).reduce((sum, apy) => sum + apy, 0) + if (strategyApy > 0) { + strategyApyMap.set(strategyAddress, { + apy: strategyApy, + tokens: tokenApyMap + }) + } } - // Add aggregate APY to Avs rewards store - avsRewardsMap.set(avs.avs.address, aggregateApy) - } - const response = { - avs: Array.from(avsRewardsMap, ([avsAddress, apy]) => ({ - avsAddress, - apy - })), - strategies: Array.from(strategyRewardsMap, ([strategyAddress, apy]) => ({ - strategyAddress, - apy - })), - aggregateApy: 0, - operatorEarningsEth: new Prisma.Prisma.Decimal(0) + avsApyMap.set(avs.avs.address, { + avsAddress: avs.avs.address, + maxApy: avs.avs.maxApy, + strategyApys: Array.from(strategyApyMap.entries()).map(([strategyAddress, data]) => ({ + strategyAddress, + apy: data.apy, + tokens: Array.from(data.tokens.entries()).map(([tokenAddress, apy]) => ({ + tokenAddress, + apy + })) + })) + }) } - // Calculate aggregates across Avs and strategies - response.aggregateApy = response.avs.reduce((sum, avs) => sum + avs.apy, 0) - response.operatorEarningsEth = operatorEarningsEth - - return response + return Array.from(avsApyMap.values()) } catch {} } diff --git a/packages/api/src/routes/stakers/stakerController.ts b/packages/api/src/routes/stakers/stakerController.ts index 8d25257f..365f059b 100644 --- a/packages/api/src/routes/stakers/stakerController.ts +++ b/packages/api/src/routes/stakers/stakerController.ts @@ -1,12 +1,20 @@ import type { Request, Response } from 'express' import prisma from '../../utils/prismaClient' +import Prisma from '@prisma/client' 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 { getStrategiesWithShareUnderlying, sharesToTVL } from '../../utils/strategyShares' +import { + getStrategiesWithShareUnderlying, + sharesToTVL, + sharesToTVLStrategies +} from '../../utils/strategyShares' +import { fetchTokenPrices } from '../../utils/tokenPrices' +import { withOperatorShares } from '../../utils/operatorShares' import { EthereumAddressSchema } from '../../schema/zod/schemas/base/ethereumAddress' import { UpdatedSinceQuerySchema } from '../../schema/zod/schemas/updatedSinceQuery' +import { WithRewardsQuerySchema } from '../../schema/zod/schemas/withRewardsQuery' import { ActiveQuerySchema } from '../../schema/zod/schemas/activeQuery' /** @@ -97,8 +105,7 @@ export async function getAllStakers(req: Request, res: Response) { * @param res */ export async function getStaker(req: Request, res: Response) { - // Validate pagination query - const result = WithTvlQuerySchema.safeParse(req.query) + const result = WithTvlQuerySchema.and(WithRewardsQuerySchema).safeParse(req.query) if (!result.success) { return handleAndReturnErrorResponse(req, res, result.error) } @@ -108,7 +115,7 @@ export async function getStaker(req: Request, res: Response) { return handleAndReturnErrorResponse(req, res, paramCheck.error) } - const { withTvl } = result.data + const { withTvl, withRewards } = result.data try { const { address } = req.params @@ -118,15 +125,57 @@ export async function getStaker(req: Request, res: Response) { include: { shares: { select: { strategyAddress: true, shares: true } - } + }, + ...(withRewards + ? { + operator: { + include: { + avs: { + select: { + avsAddress: true, + isActive: true, + avs: { + select: { + address: true, + rewardSubmissions: true, + restakeableStrategies: true, + operators: { + where: { isActive: true }, + include: { + operator: { + include: { + shares: true + } + } + } + } + } + } + } + }, + shares: { select: { strategyAddress: true, shares: true } } + } + } + } + : {}) } }) - const strategiesWithSharesUnderlying = withTvl ? await getStrategiesWithShareUnderlying() : [] + const strategiesWithSharesUnderlying = + withTvl || withRewards ? await getStrategiesWithShareUnderlying() : [] + const tvl = withTvl ? sharesToTVL(staker.shares, strategiesWithSharesUnderlying) : undefined + const tvlStrategiesEth = withTvl + ? sharesToTVLStrategies(staker.shares, strategiesWithSharesUnderlying) + : null res.send({ ...staker, - tvl: withTvl ? sharesToTVL(staker.shares, strategiesWithSharesUnderlying) : undefined + tvl, + rewards: + withRewards && tvlStrategiesEth + ? await calculateStakerRewards(staker, tvlStrategiesEth) + : undefined, + operator: undefined }) } catch (error) { handleAndReturnErrorResponse(req, res, error) @@ -425,3 +474,235 @@ export async function getStakerDeposits(req: Request, res: Response) { handleAndReturnErrorResponse(req, res, error) } } + +// --- Helper functions --- + +async function calculateStakerRewards( + // biome-ignore lint/suspicious/noExplicitAny: + staker: any, + stakerTvlStrategiesEth: { [strategyAddress: string]: number } +) { + try { + const avsApyMap: Map< + string, + { + strategies: { + address: string + apy: number + tvlEth: number + tokens: Map + }[] + } + > = new Map() + const strategyApyMap: Map< + string, + { + apy: number + tvlEth: number + tokens: Map + } + > = new Map() + + // Grab the all reward submissions that the Staker is eligible for basis opted strategies & the Operator's opted AVSs + const operatorStrategyAddresses: Set = new Set( + staker.operator?.shares.map((share) => share.strategyAddress.toLowerCase()) || [] + ) + + const optedStrategyAddresses: Set = new Set( + Array.from(operatorStrategyAddresses).filter( + (strategyAddress) => Number(stakerTvlStrategiesEth[strategyAddress]) > 0 + ) + ) + + const avsWithEligibleRewardSubmissions = staker.operator?.avs + .filter((avsOp) => avsOp.avs.rewardSubmissions.length > 0) + .map((avsOp) => ({ + avs: avsOp.avs, + eligibleRewards: avsOp.avs.rewardSubmissions.filter((reward) => + optedStrategyAddresses.has(reward.strategyAddress.toLowerCase()) + ) + })) + .filter((item) => item.eligibleRewards.length > 0) + + if (!avsWithEligibleRewardSubmissions) { + return { + aggregateApy: '0', + tokenRewards: [], + strategyApys: [], + avsApys: [] + } + } + + const tokenPrices = await fetchTokenPrices() + const strategiesWithSharesUnderlying = await getStrategiesWithShareUnderlying() + + // Calc aggregate APY for each AVS basis the opted-in strategies + for (const avs of avsWithEligibleRewardSubmissions) { + const avsAddress = avs.avs.address.toLowerCase() + + // Get share amounts for each restakeable strategy + const shares = withOperatorShares(avs.avs.operators).filter( + (s) => avs.avs.restakeableStrategies.indexOf(s.strategyAddress.toLowerCase()) !== -1 + ) + + // Fetch the AVS tvl for each strategy + 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 + + const tokenApyMap: Map = new Map() + const tokenRewards: Map< + string, + { + totalRewardsEth: Prisma.Prisma.Decimal + totalDuration: number + } + > = new Map() + + let strategyTotalRewardsEth = new Prisma.Prisma.Decimal(0) + let strategyTotalDuration = 0 + + // Find all reward submissions attributable to the strategy + const relevantSubmissions = avs.eligibleRewards.filter( + (submission) => submission.strategyAddress.toLowerCase() === strategyAddress.toLowerCase() + ) + + for (const submission of relevantSubmissions) { + let rewardIncrementEth = new Prisma.Prisma.Decimal(0) + const rewardTokenAddress = submission.token.toLowerCase() + + if (rewardTokenAddress) { + const tokenPrice = tokenPrices.find( + (tp) => tp.address.toLowerCase() === rewardTokenAddress + ) + 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)) + + // Operator takes 10% in commission + const operatorFeesEth = rewardIncrementEth.mul(10).div(100) // No decimals + const netRewardEth = rewardIncrementEth.sub(operatorFeesEth) + + // Accumulate token-specific rewards and duration + const tokenData = tokenRewards.get(rewardTokenAddress) || { + totalRewardsEth: new Prisma.Prisma.Decimal(0), + totalDuration: 0 + } + tokenData.totalRewardsEth = tokenData.totalRewardsEth.add(netRewardEth) + tokenData.totalDuration += submission.duration + tokenRewards.set(rewardTokenAddress, tokenData) + + // Accumulate strategy totals + strategyTotalRewardsEth = strategyTotalRewardsEth.add(netRewardEth) + strategyTotalDuration += submission.duration + } + + if (strategyTotalDuration === 0) continue + + // Calculate token APYs using accumulated values + tokenRewards.forEach((data, tokenAddress) => { + if (data.totalDuration !== 0) { + const tokenRewardRate = data.totalRewardsEth.toNumber() / strategyTvl + const tokenAnnualizedRate = + tokenRewardRate * ((365 * 24 * 60 * 60) / data.totalDuration) + const tokenApy = tokenAnnualizedRate * 100 + + tokenApyMap.set(tokenAddress, tokenApy) + } + }) + + // Calculate overall strategy APY summing token APYs + const strategyApy = Array.from(tokenApyMap.values()).reduce((sum, apy) => sum + apy, 0) + + // Update strategy rewards map (across all AVSs) + const currentStrategyRewards = strategyApyMap.get(strategyAddress) || { + apy: 0, + tvlEth: strategyTvl, + tokens: new Map() + } + + tokenApyMap.forEach((apy, tokenAddress) => { + const currentTokenApy = currentStrategyRewards.tokens.get(tokenAddress) || 0 + currentStrategyRewards.tokens.set(tokenAddress, currentTokenApy + apy) + }) + currentStrategyRewards.apy += strategyApy + strategyApyMap.set(strategyAddress, currentStrategyRewards) + + // Update AVS rewards map + const currentAvsRewards = avsApyMap.get(avsAddress) || { strategies: [] } + currentAvsRewards.strategies.push({ + address: strategyAddress, + tvlEth: strategyTvl, + apy: strategyApy, + tokens: tokenApyMap + }) + avsApyMap.set(avsAddress, currentAvsRewards) + } + } + + // Build token amounts section + const stakerRewardRecords = await prisma.stakerRewardSnapshot.findMany({ + where: { + stakerAddress: staker.address.toLowerCase() + } + }) + + const tokenAmounts = stakerRewardRecords.map((record) => ({ + tokenAddress: record.tokenAddress.toLowerCase(), + cumulativeAmount: record.cumulativeAmount + })) + + // Build strategies section + const strategyApys = Array.from(strategyApyMap).map(([strategyAddress, data]) => ({ + strategyAddress, + apy: data.apy, + tokens: Array.from(data.tokens.entries()).map(([tokenAddress, apy]) => ({ + tokenAddress, + apy + })) + })) + + // Build Avs section + const avsApys = Array.from(avsApyMap).map(([avsAddress, data]) => { + const strategies = data.strategies.map((s) => ({ + strategyAddress: s.address, + apy: s.apy, + tokens: Array.from(s.tokens).map(([tokenAddress, apy]) => ({ + tokenAddress, + apy + })) + })) + + const totalTvl = strategies.reduce( + (sum, s) => sum + Number(stakerTvlStrategiesEth[s.strategyAddress.toLowerCase()] || 0), + 0 + ) + const weightedApy = strategies.reduce((sum, s) => { + const tvl = Number(stakerTvlStrategiesEth[s.strategyAddress.toLowerCase()] || 0) + return sum + Number(s.apy) * (tvl / totalTvl) + }, 0) + + return { + avsAddress, + apy: weightedApy, + strategies + } + }) + + return { + aggregateApy: avsApys.reduce((sum, avs) => sum + avs.apy, 0), + tokenAmounts, + strategyApys, + avsApys + } + } catch {} +} diff --git a/packages/api/src/schema/zod/schemas/auth.ts b/packages/api/src/schema/zod/schemas/auth.ts new file mode 100644 index 00000000..3db41c68 --- /dev/null +++ b/packages/api/src/schema/zod/schemas/auth.ts @@ -0,0 +1,19 @@ +import z from '..' + +export const RequestHeadersSchema = z + .object({ + 'x-api-token': z.string().optional() + }) + .transform((headers) => { + const token = Object.keys(headers).find((key) => key.toLowerCase() === 'x-api-token') + return token + ? { + 'X-API-Token': headers[token] + } + : {} + }) + +export const RegisterUserBodySchema = z.object({ + signature: z.string().startsWith('0x').length(132), + nonce: z.string().startsWith('0x').length(66) +}) diff --git a/packages/prisma/migrations/20241128092502_include_staker_rewards/migration.sql b/packages/prisma/migrations/20241128092502_include_staker_rewards/migration.sql new file mode 100644 index 00000000..4303acca --- /dev/null +++ b/packages/prisma/migrations/20241128092502_include_staker_rewards/migration.sql @@ -0,0 +1,78 @@ +/* + Warnings: + + - You are about to drop the column `apy` on the `Avs` table. All the data in the column will be lost. + - You are about to drop the column `apy` on the `Operator` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Avs" DROP COLUMN "apy", +ADD COLUMN "maxApy" DECIMAL(8,4) NOT NULL DEFAULT 0; + +-- AlterTable +ALTER TABLE "Operator" DROP COLUMN "apy", +ADD COLUMN "maxApy" DECIMAL(8,4) NOT NULL DEFAULT 0; + +-- CreateTable +CREATE TABLE "StakerRewardSnapshot" ( + "stakerAddress" TEXT NOT NULL, + "tokenAddress" TEXT NOT NULL, + "cumulativeAmount" DECIMAL(78,18) NOT NULL DEFAULT 0, + "timestamp" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "StakerRewardSnapshot_pkey" PRIMARY KEY ("stakerAddress","tokenAddress") +); + +-- CreateTable +CREATE TABLE "User" ( + "address" TEXT NOT NULL, + "isTracked" BOOLEAN NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "User_pkey" PRIMARY KEY ("address") +); + +-- CreateTable +CREATE TABLE "MetricStakerRewardUnit" ( + "id" SERIAL NOT NULL, + "stakerAddress" TEXT NOT NULL, + "tokenAddress" TEXT NOT NULL, + "cumulativeAmount" DECIMAL(78,18) NOT NULL DEFAULT 0, + "changeCumulativeAmount" DECIMAL(78,18) NOT NULL DEFAULT 0, + "timestamp" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "MetricStakerRewardUnit_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "EventLogs_DistributionRootSubmitted" ( + "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, + "root" TEXT NOT NULL, + "rewardsCalculationEndTimestamp" BIGINT NOT NULL, + "activatedAt" BIGINT NOT NULL, + + CONSTRAINT "EventLogs_DistributionRootSubmitted_pkey" PRIMARY KEY ("transactionHash","transactionIndex") +); + +-- CreateIndex +CREATE INDEX "StakerRewardSnapshot_stakerAddress_idx" ON "StakerRewardSnapshot"("stakerAddress"); + +-- CreateIndex +CREATE INDEX "User_address_idx" ON "User"("address"); + +-- CreateIndex +CREATE INDEX "MetricStakerRewardUnit_stakerAddress_idx" ON "MetricStakerRewardUnit"("stakerAddress"); + +-- CreateIndex +CREATE INDEX "MetricStakerRewardUnit_timestamp_idx" ON "MetricStakerRewardUnit"("timestamp"); + +-- CreateIndex +CREATE UNIQUE INDEX "MetricStakerRewardUnit_stakerAddress_tokenAddress_timestamp_key" ON "MetricStakerRewardUnit"("stakerAddress", "tokenAddress", "timestamp"); + +-- CreateIndex +CREATE INDEX "EventLogs_DistributionRootSubmitted_rewardsCalculationEndTi_idx" ON "EventLogs_DistributionRootSubmitted"("rewardsCalculationEndTimestamp"); diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index c767fd8b..e8fffdb8 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -29,7 +29,7 @@ model Avs { totalStakers Int @default(0) totalOperators Int @default(0) - apy Decimal @default(0) @db.Decimal(8, 4) + maxApy Decimal @default(0) @db.Decimal(8, 4) tvlEth Decimal @default(0) @db.Decimal(20, 8) sharesHash String? @@ -127,7 +127,7 @@ model Operator { totalStakers Int @default(0) totalAvs Int @default(0) - apy Decimal @default(0) @db.Decimal(8, 4) + maxApy Decimal @default(0) @db.Decimal(8, 4) tvlEth Decimal @default(0) @db.Decimal(20, 8) sharesHash String? @@ -176,6 +176,25 @@ model StakerStrategyShares { @@index([stakerAddress]) } +model StakerRewardSnapshot { + stakerAddress String + tokenAddress String + cumulativeAmount Decimal @default(0) @db.Decimal(78, 18) + timestamp DateTime + + @@id([stakerAddress, tokenAddress]) + @@index([stakerAddress]) +} + +model User { + address String @id + isTracked Boolean + + createdAt DateTime @default(now()) + + @@index([address]) +} + model Deposit { transactionHash String @id @unique stakerAddress String @@ -337,6 +356,21 @@ model MetricOperatorStrategyUnit { @@index([timestamp]) } +model MetricStakerRewardUnit { + id Int @id @default(autoincrement()) + stakerAddress String + tokenAddress String + + cumulativeAmount Decimal @default(0) @db.Decimal(78, 18) + changeCumulativeAmount Decimal @default(0) @db.Decimal(78, 18) + + timestamp DateTime + + @@unique([stakerAddress, tokenAddress, timestamp]) + @@index([stakerAddress]) + @@index([timestamp]) +} + model MetricStrategyUnit { id Int @id @default(autoincrement()) @@ -665,6 +699,23 @@ model EventLogs_StrategyRemovedFromDepositWhitelist { @@index([blockNumber]) } +model EventLogs_DistributionRootSubmitted { + address String + + transactionHash String + transactionIndex Int + blockNumber BigInt + blockHash String + blockTime DateTime + + root String + rewardsCalculationEndTimestamp BigInt + activatedAt BigInt + + @@id([transactionHash, transactionIndex]) + @@index([rewardsCalculationEndTimestamp]) +} + model EthPricesDaily { id Int @id @default(autoincrement()) diff --git a/packages/seeder/src/events/seedLogsDistributionRootSubmitted.ts b/packages/seeder/src/events/seedLogsDistributionRootSubmitted.ts new file mode 100644 index 00000000..e66d9850 --- /dev/null +++ b/packages/seeder/src/events/seedLogsDistributionRootSubmitted.ts @@ -0,0 +1,90 @@ +import prisma from '@prisma/client' +import { parseAbiItem } from 'viem' +import { getEigenContracts } from '../data/address' +import { getViemClient } from '../utils/viemClient' +import { + bulkUpdateDbTransactions, + fetchLastSyncBlock, + getBlockDataFromDb, + loopThroughBlocks +} from '../utils/seeder' +import { getPrismaClient } from '../utils/prismaClient' + +const blockSyncKeyLogs = 'lastSyncedBlock_logs_distributionRootSubmitted' + +/** + * Utility function to seed event logs + * + * @param fromBlock + * @param toBlock + */ +export async function seedLogsDistributionRootSubmitted(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 { + // biome-ignore lint/suspicious/noExplicitAny: + const dbTransactions: any[] = [] + + const logsDistributionRootSubmitted: prisma.EventLogs_DistributionRootSubmitted[] = [] + + const logs = await viemClient.getLogs({ + address: getEigenContracts().RewardsCoordinator, + events: [ + parseAbiItem([ + 'event DistributionRootSubmitted(uint32 indexed rootIndex, bytes32 indexed root, uint32 indexed rewardsCalculationEndTimestamp, uint32 activatedAt)' + ]) + ], + fromBlock, + toBlock + }) + + // Setup a list containing event data + for (const l in logs) { + const log = logs[l] + + logsDistributionRootSubmitted.push({ + address: log.address.toLowerCase(), + transactionHash: log.transactionHash.toLowerCase(), + transactionIndex: log.logIndex, + blockNumber: BigInt(log.blockNumber), + blockHash: log.blockHash.toLowerCase(), + blockTime: blockData.get(log.blockNumber) || new Date(0), + root: String(log.args.root), + rewardsCalculationEndTimestamp: BigInt(log.args.rewardsCalculationEndTimestamp || 0), + activatedAt: BigInt(log.args.activatedAt || 0) + }) + } + + dbTransactions.push( + prismaClient.eventLogs_DistributionRootSubmitted.createMany({ + data: logsDistributionRootSubmitted, + skipDuplicates: true + }) + ) + + // Store last synced block + dbTransactions.push( + prismaClient.settings.upsert({ + where: { key: blockSyncKeyLogs }, + update: { value: Number(toBlock) }, + create: { key: blockSyncKeyLogs, value: Number(toBlock) } + }) + ) + + // Update database + const seedLength = logsDistributionRootSubmitted.length + + await bulkUpdateDbTransactions( + dbTransactions, + `[Logs] Distribution Root Submitted from: ${fromBlock} to: ${toBlock} size: ${seedLength}` + ) + } catch (error) {} + }) +} diff --git a/packages/seeder/src/events/seedLogsRewardsSubmissions.ts b/packages/seeder/src/events/seedLogsRewardsSubmissions.ts index d001a1cb..ffa4aa62 100644 --- a/packages/seeder/src/events/seedLogsRewardsSubmissions.ts +++ b/packages/seeder/src/events/seedLogsRewardsSubmissions.ts @@ -103,8 +103,6 @@ export async function seedLogsAVSRewardsSubmission(toBlock?: bigint, fromBlock?: dbTransactions, `[Logs] AVS Rewards Submission from: ${fromBlock} to: ${toBlock} size: ${seedLength}` ) - } catch (error) { - console.log(error) - } + } catch (error) {} }) } diff --git a/packages/seeder/src/index.ts b/packages/seeder/src/index.ts index d1f4213e..21273fab 100644 --- a/packages/seeder/src/index.ts +++ b/packages/seeder/src/index.ts @@ -34,10 +34,13 @@ import { seedMetricsTvl } from './metrics/seedMetricsTvl' import { monitorAvsMetrics } from './monitors/avsMetrics' import { monitorOperatorMetrics } from './monitors/operatorMetrics' import { seedAvsStrategyRewards } from './seedAvsStrategyRewards' +import { seedStakerRewardSnapshots } from './seedStakerRewardSnapshots' import { seedLogsAVSRewardsSubmission } from './events/seedLogsRewardsSubmissions' import { monitorAvsApy } from './monitors/avsApy' import { monitorOperatorApy } from './monitors/operatorApy' import { seedLogStrategyWhitelist } from './events/seedLogStrategyWhitelist' +import { seedLogsDistributionRootSubmitted } from './events/seedLogsDistributionRootSubmitted' +import { seedMetricsStakerRewards } from './metrics/seedMetricsStakerRewards' console.log('Initializing Seeder ...') @@ -83,7 +86,8 @@ async function seedEigenData() { seedLogsDeposit(targetBlock), seedLogsPodSharesUpdated(targetBlock), seedLogsAVSRewardsSubmission(targetBlock), - seedLogStrategyWhitelist(targetBlock) + seedLogStrategyWhitelist(targetBlock), + seedLogsDistributionRootSubmitted(targetBlock) ]) await Promise.all([ @@ -112,6 +116,7 @@ async function seedEigenData() { await Promise.all([ // Rewards seedAvsStrategyRewards(), + seedStakerRewardSnapshots(), // Metrics monitorAvsMetrics(), @@ -156,6 +161,7 @@ async function seedEigenDailyData(retryCount = 0) { await seedMetricsRestaking() await seedMetricsEigenPods() await seedMetricsTvl() + await seedMetricsStakerRewards() console.timeEnd('Seeded daily data in') } catch (error) { diff --git a/packages/seeder/src/metrics/seedMetricsStakerRewards.ts b/packages/seeder/src/metrics/seedMetricsStakerRewards.ts new file mode 100644 index 00000000..93e034f4 --- /dev/null +++ b/packages/seeder/src/metrics/seedMetricsStakerRewards.ts @@ -0,0 +1,399 @@ +import prisma from '@prisma/client' +import { getPrismaClient } from '../utils/prismaClient' +import { getNetwork } from '../utils/viemClient' +import { bulkUpdateDbTransactions, fetchLastSyncTime } from '../utils/seeder' +import { fetchTokenPrices } from '../utils/tokenPrices' + +interface ClaimData { + earner: string + token: string + snapshot: number + cumulative_amount: string +} + +const timeSyncKey = 'lastSyncedTime_metrics_stakerRewards' + +/** + * Seeds the table MetricStakerRewardUnit to maintain historical rewards data of EE users who are stakers + * + * @returns + */ +export async function seedMetricsStakerRewards() { + const prismaClient = getPrismaClient() + + // Define start date + let startAt: Date | null = await fetchLastSyncTime(timeSyncKey) + const endAt: Date = new Date(new Date().setUTCHours(0, 0, 0, 0)) + let clearPrev = false + + if (!startAt) { + const firstLogTimestamp = await getFirstLogTimestamp() + if (firstLogTimestamp) { + startAt = new Date(new Date(firstLogTimestamp).setUTCHours(0, 0, 0, 0)) + } else { + startAt = new Date(new Date().setUTCHours(0, 0, 0, 0)) + } + clearPrev = true + } + + // Bail early if there is no time diff to sync + if (endAt.getTime() - startAt.getTime() <= 0) { + console.log(`[In Sync] [Metrics] Staker Rewards from: ${startAt} to: ${endAt}`) + return + } + + // Clear previous data + if (clearPrev) { + await prismaClient.metricStakerRewardUnit.deleteMany() + } + + const bucketUrl = getBucketUrl() + const BATCH_SIZE = 10_000 + + try { + const tokenPrices = await fetchTokenPrices() + + // Check if there are any untracked users + const untrackedUser = await prismaClient.user.findFirst({ + where: { + isTracked: false + } + }) + + const isTrackingRequired = !!untrackedUser + + const distributionRootsTracked = + await prismaClient.eventLogs_DistributionRootSubmitted.findMany({ + select: { + rewardsCalculationEndTimestamp: true + }, + where: { + rewardsCalculationEndTimestamp: { + gte: startAt.getTime() / 1000, + lt: endAt.getTime() / 1000 + } + } + }) + + const distributionRootsUntracked = isTrackingRequired + ? await prismaClient.eventLogs_DistributionRootSubmitted.findMany({ + select: { + rewardsCalculationEndTimestamp: true + } + }) + : undefined + + // Find timestamps of distributionRoots after the last seed. These are relevant for tracked & untracked users. + const timestampsForAllUsers = distributionRootsTracked.map( + (dr) => dr.rewardsCalculationEndTimestamp + ) + + if (isTrackingRequired && distributionRootsUntracked) { + // All timestamps before the last seed need to also be indexed for untracked users. + const timestampsForUntrackedUsers = distributionRootsUntracked.map( + (dr) => dr.rewardsCalculationEndTimestamp + ) + + // Seed data for untracked users until last seed, so they will be in sync with tracked users + for (const timestamp of timestampsForUntrackedUsers) { + const snapshotDate = new Date(Number(timestamp) * 1000).toISOString().split('T')[0] + const response = await fetch(`${bucketUrl}/${snapshotDate}/claim-amounts.json`) + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`) + } + + const reader = response.body?.getReader() + if (!reader) { + throw new Error('No readable stream available') + } + + let skip = 0 + const take = 10_000 + + while (true) { + const untrackedUsers = await prismaClient.user.findMany({ + select: { + address: true, + isTracked: true + }, + where: { + isTracked: false + }, + skip, + take + }) + + if (untrackedUsers.length === 0) break + + const untrackedUserAddresses = untrackedUsers.map((uu) => uu.address.toLowerCase()) + + await processSnapshot( + prismaClient, + reader, + untrackedUserAddresses, + tokenPrices, + BATCH_SIZE + ) + + skip += take + } + } + + // Mark all untracked users as tracked + await prismaClient.user.updateMany({ + where: { + isTracked: false + }, + data: { + isTracked: true + } + }) + } + + // All untracked users are now in-sync with tracked ones. Now seed data for timestamps after last seed for all users. + for (const timestamp of timestampsForAllUsers) { + const snapshotDate = new Date(Number(timestamp) * 1000).toISOString().split('T')[0] + const response = await fetch(`${bucketUrl}/${snapshotDate}/claim-amounts.json`) + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`) + } + + const reader = response.body?.getReader() + if (!reader) { + throw new Error('No readable stream available') + } + + let skip = 0 + const take = 10_000 + + while (true) { + const users = await prismaClient.user.findMany({ + select: { + address: true, + isTracked: true + }, + skip, + take + }) + + if (users.length === 0) break + + const userAddresses = users.map((uu) => uu.address.toLowerCase()) + + await processSnapshot(prismaClient, reader, userAddresses, tokenPrices, BATCH_SIZE) + + skip += take + } + } + + // Update last synced time + await prismaClient.settings.upsert({ + where: { key: timeSyncKey }, + update: { value: endAt.getTime() }, + create: { key: timeSyncKey, value: endAt.getTime() } + }) + } catch {} +} + +/** + * Processes snapshot data line-by-line from EL bucket's delimited JSON file + * + * @param prismaClient + * @param reader + * @param userAddresses + * @param tokenPrices + * @param BATCH_SIZE + */ +async function processSnapshot( + prismaClient: prisma.PrismaClient, + reader: ReadableStreamDefaultReader, + userAddresses: string[], + tokenPrices: Array<{ address: string; decimals: number }>, + BATCH_SIZE: number +) { + const latestMetricsFromDb = await getLatestMetricsFromDb(prismaClient, userAddresses) + let stakerRewardList: Omit[] = [] + let buffer = '' + const decoder = new TextDecoder() + + try { + while (true) { + const { done, value } = await reader.read() + + if (done) { + const line = buffer.trim() + if (line) { + processLine(line, userAddresses, stakerRewardList, latestMetricsFromDb, tokenPrices) + } + break + } + + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n') + + for (let i = 0; i < lines.length - 1; i++) { + const line = lines[i].trim() + if (line) { + processLine(line, userAddresses, stakerRewardList, latestMetricsFromDb, tokenPrices) + } + + if (stakerRewardList.length >= BATCH_SIZE) { + await writeToDb(prismaClient, stakerRewardList) + stakerRewardList = [] + } + } + + buffer = lines[lines.length - 1] + } + + if (stakerRewardList.length > 0) { + await writeToDb(prismaClient, stakerRewardList) + } + } finally { + reader.releaseLock() + } +} + +/** + * For every line in snapshot data, if the earner is a User on EE, prepares db object entry + * + * @param line + * @param addresses + * @param stakerRewardList + * @param latestMetricsFromDb + * @param tokenPrices + */ +function processLine( + line: string, + addresses: string[], + stakerRewardList: Omit[], + latestMetricsFromDb: { + getLatestAmount: (address: string, token: string) => prisma.Prisma.Decimal + }, + tokenPrices: Array<{ address: string; decimals: number }> +) { + const data = JSON.parse(line) as ClaimData + const earner = data.earner.toLowerCase() + const token = data.token.toLowerCase() + + if (addresses.includes(earner)) { + const tokenDecimals = + tokenPrices.find((tp) => tp.address.toLowerCase() === token)?.decimals || 18 + + const latestRecordInBatch = stakerRewardList + .filter( + (record) => + record.stakerAddress.toLowerCase() === earner && + record.tokenAddress.toLowerCase() === token + ) + .reduce((latest, current) => { + if (!latest || current.timestamp > latest.timestamp) { + return current + } + return latest + }, null as Omit | null) + + // To calculate change, find latest `cumulativeAmount` value (first check `stakerRewardList`, if not, get from db) + const lastCumulativeAmount = latestRecordInBatch + ? latestRecordInBatch.cumulativeAmount + : latestMetricsFromDb.getLatestAmount(earner, token) + + const cumulativeAmount = new prisma.Prisma.Decimal(data.cumulative_amount).div( + new prisma.Prisma.Decimal(10).pow(tokenDecimals) + ) + + stakerRewardList.push({ + stakerAddress: earner, + tokenAddress: token, + cumulativeAmount, + changeCumulativeAmount: cumulativeAmount.sub(lastCumulativeAmount), + timestamp: new Date(data.snapshot) + }) + } +} + +async function writeToDb( + prismaClient: prisma.PrismaClient, + batch: Omit[] +) { + // biome-ignore lint/suspicious/noExplicitAny: + const dbTransactions: any[] = [] + + // Add historical records to db + dbTransactions.push( + prismaClient.metricStakerRewardUnit.createMany({ + data: batch, + skipDuplicates: true + }) + ) + + if (dbTransactions.length > 0) { + await bulkUpdateDbTransactions(dbTransactions, `[Data] Staker Token Rewards: ${batch.length}`) + } +} + +function getBucketUrl(): string { + return getNetwork().testnet + ? 'https://eigenlabs-rewards-testnet-holesky.s3.amazonaws.com/testnet/holesky' + : 'https://eigenlabs-rewards-mainnet-ethereum.s3.amazonaws.com/mainnet/ethereum' +} + +/** + * Gets first log timestamp + * + * @returns + */ +async function getFirstLogTimestamp() { + const prismaClient = getPrismaClient() + + const firstLog = await prismaClient.eventLogs_DistributionRootSubmitted.findFirst({ + select: { rewardsCalculationEndTimestamp: true }, + orderBy: { rewardsCalculationEndTimestamp: 'asc' } + }) + + return firstLog?.rewardsCalculationEndTimestamp + ? new Date(Number(firstLog.rewardsCalculationEndTimestamp) * 1000) + : null +} + +/** + * Finds latest `cumulativeAmount` per token for a given set of addresses. Used to help calc `changeCumulativeAmount`. + * + * @param addresses + * @returns + */ +async function getLatestMetricsFromDb(prismaClient: prisma.PrismaClient, addresses: string[]) { + // First get the latest timestamp for each staker/token combination + const latestMetrics = await prismaClient.metricStakerRewardUnit.findMany({ + where: { + stakerAddress: { + in: addresses + } + }, + orderBy: { + timestamp: 'desc' + }, + distinct: ['stakerAddress', 'tokenAddress'], + select: { + stakerAddress: true, + tokenAddress: true, + cumulativeAmount: true + } + }) + + const metricsMap = new Map() + + for (const metric of latestMetrics) { + const key = `${metric.stakerAddress.toLowerCase()}-${metric.tokenAddress.toLowerCase()}` + metricsMap.set(key, metric.cumulativeAmount) + } + + return { + getLatestAmount: (address: string, token: string) => { + const key = `${address.toLowerCase()}-${token.toLowerCase()}` + return metricsMap.get(key) ?? new prisma.Prisma.Decimal(0) + } + } +} diff --git a/packages/seeder/src/monitors/avsApy.ts b/packages/seeder/src/monitors/avsApy.ts index 3fd61419..adfa2a79 100644 --- a/packages/seeder/src/monitors/avsApy.ts +++ b/packages/seeder/src/monitors/avsApy.ts @@ -12,7 +12,7 @@ export async function monitorAvsApy() { const dbTransactions: any[] = [] const data: { address: string - apy: Prisma.Prisma.Decimal + maxApy: Prisma.Prisma.Decimal }[] = [] let skip = 0 @@ -42,26 +42,24 @@ export async function monitorAvsApy() { take }) - if (avsMetrics.length === 0) { - break - } + if (avsMetrics.length === 0) break // Setup all db transactions for this iteration for (const avs of avsMetrics) { + const strategyRewardsMap: Map = new Map() + const shares = withOperatorShares(avs.operators).filter( (s) => avs.restakeableStrategies.indexOf(s.strategyAddress.toLowerCase()) !== -1 ) - let apy = new Prisma.Prisma.Decimal(0) - if (avs.rewardSubmissions.length > 0) { // Fetch the AVS tvl for each strategy const tvlStrategiesEth = sharesToTVLStrategies(shares, strategiesWithSharesUnderlying) // Iterate through each strategy and calculate all its rewards - const strategiesApy = avs.restakeableStrategies.map((strategyAddress) => { + for (const strategyAddress of avs.restakeableStrategies) { const strategyTvl = tvlStrategiesEth[strategyAddress.toLowerCase()] || 0 - if (strategyTvl === 0) return { strategyAddress, apy: 0 } + if (strategyTvl === 0) continue let totalRewardsEth = new Prisma.Prisma.Decimal(0) let totalDuration = 0 @@ -96,29 +94,27 @@ export async function monitorAvsApy() { totalDuration += submission.duration } - if (totalDuration === 0) { - return { strategyAddress, apy: 0 } - } + if (totalDuration === 0) continue // Annualize the reward basis its duration to find yearly APY const rewardRate = totalRewardsEth.toNumber() / strategyTvl const annualizedRate = rewardRate * ((365 * 24 * 60 * 60) / totalDuration) const apy = annualizedRate * 100 - return { strategyAddress, apy } - }) + strategyRewardsMap.set(strategyAddress, apy) + } - // Calculate aggregate APY across strategies - apy = new Prisma.Prisma.Decimal( - strategiesApy.reduce((sum, strategy) => sum + strategy.apy, 0) - ) - } + // Calculate max achievable APY + if (strategyRewardsMap.size > 0) { + const maxApy = new Prisma.Prisma.Decimal(Math.max(...strategyRewardsMap.values())) - if (avs.apy !== apy) { - data.push({ - address: avs.address, - apy - }) + if (avs.maxApy !== maxApy) { + data.push({ + address: avs.address, + maxApy + }) + } + } } } @@ -128,12 +124,12 @@ export async function monitorAvsApy() { const query = ` UPDATE "Avs" AS a SET - "apy" = a2."apy" + "maxApy" = a2."maxApy" FROM ( VALUES - ${data.map((d) => `('${d.address}', ${d.apy})`).join(', ')} - ) AS a2 (address, "apy") + ${data.map((d) => `('${d.address}', ${d.maxApy})`).join(', ')} + ) AS a2 (address, "maxApy") WHERE a2.address = a.address; ` diff --git a/packages/seeder/src/monitors/operatorApy.ts b/packages/seeder/src/monitors/operatorApy.ts index f5b2aadb..a4795a3b 100644 --- a/packages/seeder/src/monitors/operatorApy.ts +++ b/packages/seeder/src/monitors/operatorApy.ts @@ -12,7 +12,7 @@ export async function monitorOperatorApy() { const dbTransactions: any[] = [] const data: { address: string - apy: Prisma.Prisma.Decimal + maxApy: Prisma.Prisma.Decimal }[] = [] let skip = 0 @@ -60,15 +60,10 @@ export async function monitorOperatorApy() { take }) - if (operatorMetrics.length === 0) { - break - } + if (operatorMetrics.length === 0) break // Setup all db transactions for this iteration for (const operator of operatorMetrics) { - let apy = new Prisma.Prisma.Decimal(0) - - const avsRewardsMap: Map = new Map() const strategyRewardsMap: Map = new Map() // Grab the all reward submissions that the Operator is eligible for basis opted strategies & AVSs @@ -90,8 +85,6 @@ export async function monitorOperatorApy() { // Calc aggregate APY for each AVS basis the opted-in strategies for (const avs of avsWithEligibleRewardSubmissions) { - let aggregateApy = 0 - // Get share amounts for each restakeable strategy const shares = withOperatorShares(avs.avs.operators).filter( (s) => avs.avs.restakeableStrategies.indexOf(s.strategyAddress.toLowerCase()) !== -1 @@ -150,30 +143,24 @@ export async function monitorOperatorApy() { const rewardRate = totalRewardsEth.toNumber() / strategyTvl // No decimals const annualizedRate = rewardRate * ((365 * 24 * 60 * 60) / totalDuration) const apy = annualizedRate * 100 - aggregateApy += apy // Add strategy's APY to common strategy rewards store (across all Avs) const currentStrategyApy = strategyRewardsMap.get(strategyAddress) || 0 strategyRewardsMap.set(strategyAddress, currentStrategyApy + apy) } - // Add aggregate APY to Avs rewards store - avsRewardsMap.set(avs.avs.address, aggregateApy) } - const avs = Array.from(avsRewardsMap, ([avsAddress, apy]) => ({ - avsAddress, - apy - })) - - // Calculate aggregates across Avs and strategies - apy = new Prisma.Prisma.Decimal(avs.reduce((sum, avs) => sum + avs.apy, 0)) - } + // Calculate max achievable APY + if (strategyRewardsMap.size > 0) { + const maxApy = new Prisma.Prisma.Decimal(Math.max(...strategyRewardsMap.values())) - if (operator.apy !== apy) { - data.push({ - address: operator.address, - apy - }) + if (operator.maxApy !== maxApy) { + data.push({ + address: operator.address, + maxApy + }) + } + } } } @@ -183,12 +170,12 @@ export async function monitorOperatorApy() { const query = ` UPDATE "Operator" AS o SET - "apy" = o2."apy" + "maxApy" = o2."maxApy" FROM ( VALUES - ${data.map((d) => `('${d.address}', ${d.apy})`).join(', ')} - ) AS o2 (address, "apy") + ${data.map((d) => `('${d.address}', ${d.maxApy})`).join(', ')} + ) AS o2 (address, "maxApy") WHERE o2.address = o.address; ` diff --git a/packages/seeder/src/seedAvs.ts b/packages/seeder/src/seedAvs.ts index b5086fae..4acacf83 100644 --- a/packages/seeder/src/seedAvs.ts +++ b/packages/seeder/src/seedAvs.ts @@ -121,7 +121,7 @@ export async function seedAvs(toBlock?: bigint, fromBlock?: bigint) { isMetadataSynced, totalStakers: 0, totalOperators: 0, - apy: new prisma.Prisma.Decimal(0), + maxApy: new prisma.Prisma.Decimal(0), tvlEth: new prisma.Prisma.Decimal(0), sharesHash: '', createdAtBlock, diff --git a/packages/seeder/src/seedOperators.ts b/packages/seeder/src/seedOperators.ts index 247752d8..b944174e 100644 --- a/packages/seeder/src/seedOperators.ts +++ b/packages/seeder/src/seedOperators.ts @@ -120,7 +120,7 @@ export async function seedOperators(toBlock?: bigint, fromBlock?: bigint) { isMetadataSynced, totalStakers: 0, totalAvs: 0, - apy: new prisma.Prisma.Decimal(0), + maxApy: new prisma.Prisma.Decimal(0), tvlEth: new prisma.Prisma.Decimal(0), sharesHash: '', createdAtBlock, diff --git a/packages/seeder/src/seedStakerRewardSnapshots.ts b/packages/seeder/src/seedStakerRewardSnapshots.ts new file mode 100644 index 00000000..a6ca1114 --- /dev/null +++ b/packages/seeder/src/seedStakerRewardSnapshots.ts @@ -0,0 +1,178 @@ +import prisma from '@prisma/client' +import { getPrismaClient } from './utils/prismaClient' +import { getNetwork } from './utils/viemClient' +import { bulkUpdateDbTransactions } from './utils/seeder' +import { fetchTokenPrices } from './utils/tokenPrices' + +interface ClaimData { + earner: string + token: string + snapshot: number + cumulative_amount: string +} + +/** + * Seeds the StakerRewardSnapshot table to maintain latest state of all EL stakers + * + * @returns + */ +export async function seedStakerRewardSnapshots() { + const prismaClient = getPrismaClient() + const bucketUrl = getBucketUrl() + const BATCH_SIZE = 10_000 + + try { + const tokenPrices = await fetchTokenPrices() + + // Find latest snapshot timestamp + const latestLog = await prismaClient.eventLogs_DistributionRootSubmitted.findFirstOrThrow({ + select: { + rewardsCalculationEndTimestamp: true + }, + orderBy: { + rewardsCalculationEndTimestamp: 'desc' + } + }) + const latestSnapshotTimestamp = new Date( + Number(latestLog.rewardsCalculationEndTimestamp) * 1000 + ) + .toISOString() + .split('T')[0] + + // Find snapshot date of existing data + const snapshotRecord = await prismaClient.stakerRewardSnapshot.findFirst({ + select: { + timestamp: true + }, + orderBy: { + timestamp: 'asc' // All snapshots should ideally have the same timestamp, but we check for earliest in case of sync issues + } + }) + + const snapshotTimestamp = snapshotRecord?.timestamp?.toISOString()?.split('T')[0] + + if (latestSnapshotTimestamp === snapshotTimestamp) { + console.log('[In Sync] [Data] Staker Reward Snapshots') + return + } + + // Fetch latest snapshot from EL bucket + const response = await fetch(`${bucketUrl}/${latestSnapshotTimestamp}/claim-amounts.json`) + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`) + } + + const reader = response.body?.getReader() + if (!reader) { + throw new Error('No readable stream available') + } + + // Delete existing snapshots + await prismaClient.stakerRewardSnapshot.deleteMany() + + // Write new snapshots batch-wise + let buffer = '' + const decoder = new TextDecoder() + + let snapshotList: prisma.StakerRewardSnapshot[] = [] + + try { + while (true) { + const { done, value } = await reader.read() + + if (done) { + // Process any remaining data in buffer + const line = buffer.trim() + if (line) { + const data = JSON.parse(line) as ClaimData + + const tokenDecimals = + tokenPrices.find((tp) => tp.address.toLowerCase() === data.token.toLowerCase()) + ?.decimals || 18 + + snapshotList.push({ + stakerAddress: data.earner.toLowerCase(), + tokenAddress: data.token.toLowerCase(), + cumulativeAmount: new prisma.Prisma.Decimal(data.cumulative_amount).div( + new prisma.Prisma.Decimal(10).pow(tokenDecimals) + ), + timestamp: new Date(data.snapshot) + }) + } + break + } + + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n') + + // Process all complete lines + for (let i = 0; i < lines.length - 1; i++) { + const line = lines[i].trim() + if (line) { + const data = JSON.parse(line) as ClaimData + + const tokenDecimals = + tokenPrices.find((tp) => tp.address.toLowerCase() === data.token.toLowerCase()) + ?.decimals || 18 + + snapshotList.push({ + stakerAddress: data.earner.toLowerCase(), + tokenAddress: data.token.toLowerCase(), + cumulativeAmount: new prisma.Prisma.Decimal(data.cumulative_amount).div( + new prisma.Prisma.Decimal(10).pow(tokenDecimals) + ), + timestamp: new Date(data.snapshot) + }) + } + + // If batch is full, write to database + if (snapshotList.length >= BATCH_SIZE) { + // biome-ignore lint/suspicious/noExplicitAny: + const dbTransactions: any[] = [] + dbTransactions.push( + prismaClient.stakerRewardSnapshot.createMany({ + data: snapshotList, + skipDuplicates: true + }) + ) + + await bulkUpdateDbTransactions( + dbTransactions, + `[Data] Staker Reward Snapshots: ${snapshotList.length}` + ) + + snapshotList = [] + } + + // Keep the last incomplete line in buffer + buffer = lines[lines.length - 1] + } + } + + // Save any remaining batch + if (snapshotList.length > 0) { + // biome-ignore lint/suspicious/noExplicitAny: + const dbTransactions: any[] = [] + dbTransactions.push( + prismaClient.stakerRewardSnapshot.createMany({ + data: snapshotList, + skipDuplicates: true + }) + ) + + await bulkUpdateDbTransactions( + dbTransactions, + `[Data] Staker Reward Snapshots: ${snapshotList.length}` + ) + } + } finally { + reader.releaseLock() + } + } catch {} +} + +function getBucketUrl(): string { + return getNetwork().testnet + ? 'https://eigenlabs-rewards-testnet-holesky.s3.amazonaws.com/testnet/holesky' + : 'https://eigenlabs-rewards-mainnet-ethereum.s3.amazonaws.com/mainnet/ethereum' +} From 47d0c84347e474fc72a8f560995145fd040a22c6 Mon Sep 17 00:00:00 2001 From: Gowtham Sundaresan <131300352+gowthamsundaresan@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:03:26 +0530 Subject: [PATCH 10/17] [Feat] - Server Auth: Phase 1, v1.5 (#290) * implement cache store for auth tokens * api support for multiple token updates via supabase webhook * implement refresh store on server boot using supabase edge fn * use edge functions for auth store refresh signal * update .env.example * swap auth via jwt to access token * build: include node-cache package * add auth token check to rate limiter * add route caching to auth route * add authentication middleware * tracka and sync monthly user requests * separate request collection and auth data caches * remove dev route * setup plans config * fix: init rate limiter during server boot * rollover requests count to next update if sync fails --- package-lock.json | 1 + packages/api/.env.example | 6 +- packages/api/package.json | 1 + packages/api/src/index.ts | 3 + .../api/src/routes/auth/authController.ts | 35 +++ packages/api/src/routes/auth/authRoutes.ts | 2 + packages/api/src/routes/avs/avsController.ts | 13 + packages/api/src/routes/avs/avsRoutes.ts | 8 +- packages/api/src/routes/index.ts | 24 +- .../routes/operators/operatorController.ts | 13 + .../src/routes/operators/operatorRoutes.ts | 8 +- packages/api/src/utils/auth.ts | 14 - packages/api/src/utils/authCache.ts | 16 ++ packages/api/src/utils/authMiddleware.ts | 254 ++++++++++++++++++ packages/api/src/utils/jwtUtils.ts | 20 -- packages/api/src/utils/request.ts | 9 + packages/api/src/utils/userRequestsSync.ts | 93 +++++++ 17 files changed, 459 insertions(+), 61 deletions(-) delete mode 100644 packages/api/src/utils/auth.ts create mode 100644 packages/api/src/utils/authCache.ts create mode 100644 packages/api/src/utils/authMiddleware.ts delete mode 100644 packages/api/src/utils/jwtUtils.ts create mode 100644 packages/api/src/utils/request.ts create mode 100644 packages/api/src/utils/userRequestsSync.ts diff --git a/package-lock.json b/package-lock.json index 50ed17e0..fc7eb9c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2665,6 +2665,7 @@ "joi": "^17.12.2", "jsonwebtoken": "^9.0.2", "morgan": "~1.9.1", + "node-cache": "^5.1.2", "route-cache": "^0.7.0", "viem": "^2.8.14", "zod": "^3.23.4", diff --git a/packages/api/.env.example b/packages/api/.env.example index f20d3168..3570f3ea 100644 --- a/packages/api/.env.example +++ b/packages/api/.env.example @@ -5,4 +5,8 @@ NETWORK_CHAIN_WSS_URL = "" DATABASE_URL = "" DIRECT_URL = "" CMC_API_KEY = "" -JWT_SECRET = "" \ No newline at end of file +EE_AUTH_TOKEN = "" +SUPABASE_SERVICE_ROLE_KEY = "" +SUPABASE_FETCH_ALL_USERS_URL = "https://.supabase.co/functions/v1/fetch-all-users" +SUPABASE_FETCH_ACCESS_LEVEL_URL = "https://.supabase.co/functions/v1/fetch-access-level" +SUPABASE_POST_REQUESTS_URL = "https://.supabase.co/functions/v1/post-requests" \ No newline at end of file diff --git a/packages/api/package.json b/packages/api/package.json index 2fbe01ed..f2cc9f53 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -23,6 +23,7 @@ "joi": "^17.12.2", "jsonwebtoken": "^9.0.2", "morgan": "~1.9.1", + "node-cache": "^5.1.2", "route-cache": "^0.7.0", "viem": "^2.8.14", "zod": "^3.23.4", diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 547fb009..9c9a08d0 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -8,6 +8,7 @@ import helmet from 'helmet' import cors from 'cors' import apiRouter from './routes' import { EigenExplorerApiError, handleAndReturnErrorResponse } from './schema/errors' +import { startUserRequestsSync } from './utils/userRequestsSync' const PORT = process.env.PORT ? Number.parseInt(process.env.PORT) : 3002 @@ -50,4 +51,6 @@ app.use((err: Error, req: Request, res: Response) => { // Start the server app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`) + + startUserRequestsSync() }) diff --git a/packages/api/src/routes/auth/authController.ts b/packages/api/src/routes/auth/authController.ts index e712f813..c66af47c 100644 --- a/packages/api/src/routes/auth/authController.ts +++ b/packages/api/src/routes/auth/authController.ts @@ -1,6 +1,7 @@ import type { Request, Response } from 'express' import { handleAndReturnErrorResponse } from '../../schema/errors' import { EthereumAddressSchema } from '../../schema/zod/schemas/base/ethereumAddress' +import { refreshAuthStore } from '../../utils/authMiddleware' import { RegisterUserBodySchema, RequestHeadersSchema } from '../../schema/zod/schemas/auth' import { verifyMessage } from 'viem' import prisma from '../../utils/prismaClient' @@ -149,3 +150,37 @@ export async function registerUser(req: Request, res: Response) { handleAndReturnErrorResponse(req, res, error) } } + +/** + * Protected route, refreshes the server's entire auth store. Called by Supabase edge fn signal-refresh. + * This function will fail if the caller does not use admin-level auth token + * + * @param req + * @param res + * @returns + */ +export async function signalRefreshAuthStore(req: Request, res: Response) { + const headerCheck = RequestHeadersSchema.safeParse(req.headers) + if (!headerCheck.success) { + return handleAndReturnErrorResponse(req, res, headerCheck.error) + } + + try { + const apiToken = headerCheck.data['X-API-Token'] + const authToken = process.env.EE_AUTH_TOKEN + + if (!apiToken || apiToken !== authToken) { + throw new Error('Unauthorized access.') + } + + const status = await refreshAuthStore() + + if (!status) { + throw new Error('Refresh auth store failed.') + } + + res.status(200).json({ message: 'Auth store refreshed.' }) + } catch (error) { + handleAndReturnErrorResponse(req, res, error) + } +} diff --git a/packages/api/src/routes/auth/authRoutes.ts b/packages/api/src/routes/auth/authRoutes.ts index dbf3bbbf..cfbddca4 100644 --- a/packages/api/src/routes/auth/authRoutes.ts +++ b/packages/api/src/routes/auth/authRoutes.ts @@ -1,11 +1,13 @@ import express from 'express' import routeCache from 'route-cache' +import { signalRefreshAuthStore } from './authController' import { checkUserStatus, generateNonce, registerUser } from './authController' const router = express.Router() // API routes for /auth +router.get('/refresh-store', routeCache.cacheSeconds(5), signalRefreshAuthStore) router.get('/users/:address/check-status', routeCache.cacheSeconds(30), checkUserStatus) router.get('/users/:address/nonce', routeCache.cacheSeconds(10), generateNonce) router.post('/users/:address/register', routeCache.cacheSeconds(10), registerUser) diff --git a/packages/api/src/routes/avs/avsController.ts b/packages/api/src/routes/avs/avsController.ts index f1c0e394..bdbb4a0b 100644 --- a/packages/api/src/routes/avs/avsController.ts +++ b/packages/api/src/routes/avs/avsController.ts @@ -8,6 +8,7 @@ import { UpdatedSinceQuerySchema } from '../../schema/zod/schemas/updatedSinceQu import { SortByQuerySchema } from '../../schema/zod/schemas/sortByQuery' import { SearchByTextQuerySchema } from '../../schema/zod/schemas/searchByTextQuery' import { WithRewardsQuerySchema } from '../../schema/zod/schemas/withRewardsQuery' +import { RequestHeadersSchema } from '../../schema/zod/schemas/auth' import { getOperatorSearchQuery } from '../operators/operatorController' import { handleAndReturnErrorResponse } from '../../schema/errors' import { @@ -630,7 +631,19 @@ export async function invalidateMetadata(req: Request, res: Response) { return handleAndReturnErrorResponse(req, res, paramCheck.error) } + const headerCheck = RequestHeadersSchema.safeParse(req.headers) + if (!headerCheck.success) { + return handleAndReturnErrorResponse(req, res, headerCheck.error) + } + try { + const apiToken = headerCheck.data['X-API-Token'] + const authToken = process.env.EE_AUTH_TOKEN + + if (!apiToken || apiToken !== authToken) { + throw new Error('Unauthorized access.') + } + const { address } = req.params const updateResult = await prisma.avs.updateMany({ diff --git a/packages/api/src/routes/avs/avsRoutes.ts b/packages/api/src/routes/avs/avsRoutes.ts index 354a03ff..697c278c 100644 --- a/packages/api/src/routes/avs/avsRoutes.ts +++ b/packages/api/src/routes/avs/avsRoutes.ts @@ -8,7 +8,6 @@ import { getAVSRewards, invalidateMetadata } from './avsController' -import { authenticateJWT } from '../../utils/jwtUtils' import routeCache from 'route-cache' @@ -28,11 +27,6 @@ router.get('/:address/operators', routeCache.cacheSeconds(120), getAVSOperators) router.get('/:address/rewards', routeCache.cacheSeconds(120), getAVSRewards) // Protected routes -router.get( - '/:address/invalidate-metadata', - authenticateJWT, - routeCache.cacheSeconds(120), - invalidateMetadata -) +router.get('/:address/invalidate-metadata', routeCache.cacheSeconds(120), invalidateMetadata) export default router diff --git a/packages/api/src/routes/index.ts b/packages/api/src/routes/index.ts index c151d64c..a9d40e04 100644 --- a/packages/api/src/routes/index.ts +++ b/packages/api/src/routes/index.ts @@ -7,9 +7,9 @@ import metricRoutes from './metrics/metricRoutes' import withdrawalRoutes from './withdrawals/withdrawalRoutes' import depositRoutes from './deposits/depositRoutes' import auxiliaryRoutes from './auxiliary/auxiliaryRoutes' -import authRoutes from './auth/authRoutes' import rewardRoutes from './rewards/rewardRoutes' -import { rateLimiter } from '../utils/auth' +import authRoutes from './auth/authRoutes' +import { authenticator, rateLimiter } from '../utils/authMiddleware' const apiRouter = express.Router() @@ -22,15 +22,15 @@ apiRouter.get('/version', (_, res) => ) // Remaining routes -apiRouter.use('/avs', rateLimiter, avsRoutes) -apiRouter.use('/strategies', rateLimiter, strategiesRoutes) -apiRouter.use('/operators', rateLimiter, operatorRoutes) -apiRouter.use('/stakers', rateLimiter, stakerRoutes) -apiRouter.use('/metrics', rateLimiter, metricRoutes) -apiRouter.use('/withdrawals', rateLimiter, withdrawalRoutes) -apiRouter.use('/deposits', rateLimiter, depositRoutes) -apiRouter.use('/aux', rateLimiter, auxiliaryRoutes) -apiRouter.use('/auth', rateLimiter, authRoutes) -apiRouter.use('/rewards', rateLimiter, rewardRoutes) +apiRouter.use('/avs', authenticator, rateLimiter, avsRoutes) +apiRouter.use('/strategies', authenticator, rateLimiter, strategiesRoutes) +apiRouter.use('/operators', authenticator, rateLimiter, operatorRoutes) +apiRouter.use('/stakers', authenticator, rateLimiter, stakerRoutes) +apiRouter.use('/metrics', authenticator, rateLimiter, metricRoutes) +apiRouter.use('/withdrawals', authenticator, rateLimiter, withdrawalRoutes) +apiRouter.use('/deposits', authenticator, rateLimiter, depositRoutes) +apiRouter.use('/aux', authenticator, rateLimiter, auxiliaryRoutes) +apiRouter.use('/rewards', authenticator, rateLimiter, rewardRoutes) +apiRouter.use('/auth', authenticator, rateLimiter, authRoutes) export default apiRouter diff --git a/packages/api/src/routes/operators/operatorController.ts b/packages/api/src/routes/operators/operatorController.ts index b9b9ff86..dc7e0f94 100644 --- a/packages/api/src/routes/operators/operatorController.ts +++ b/packages/api/src/routes/operators/operatorController.ts @@ -6,6 +6,7 @@ import { WithAdditionalDataQuerySchema } from '../../schema/zod/schemas/withAddi import { SortByQuerySchema } from '../../schema/zod/schemas/sortByQuery' import { SearchByTextQuerySchema } from '../../schema/zod/schemas/searchByTextQuery' import { WithRewardsQuerySchema } from '../../schema/zod/schemas/withRewardsQuery' +import { RequestHeadersSchema } from '../../schema/zod/schemas/auth' import { OperatorEventQuerySchema } from '../../schema/zod/schemas/operatorEvents' import { handleAndReturnErrorResponse } from '../../schema/errors' import { @@ -500,7 +501,19 @@ export async function invalidateMetadata(req: Request, res: Response) { return handleAndReturnErrorResponse(req, res, paramCheck.error) } + const headerCheck = RequestHeadersSchema.safeParse(req.headers) + if (!headerCheck.success) { + return handleAndReturnErrorResponse(req, res, headerCheck.error) + } + try { + const apiToken = headerCheck.data['X-API-Token'] + const authToken = process.env.EE_AUTH_TOKEN + + if (!apiToken || apiToken !== authToken) { + throw new Error('Unauthorized access.') + } + const { address } = req.params const updateResult = await prisma.operator.updateMany({ diff --git a/packages/api/src/routes/operators/operatorRoutes.ts b/packages/api/src/routes/operators/operatorRoutes.ts index 43053c45..a3f48ed7 100644 --- a/packages/api/src/routes/operators/operatorRoutes.ts +++ b/packages/api/src/routes/operators/operatorRoutes.ts @@ -7,7 +7,6 @@ import { getOperatorEvents, invalidateMetadata } from './operatorController' -import { authenticateJWT } from '../../utils/jwtUtils' import routeCache from 'route-cache' @@ -112,11 +111,6 @@ router.get('/:address/rewards', routeCache.cacheSeconds(120), getOperatorRewards router.get('/:address/events/delegation', routeCache.cacheSeconds(120), getOperatorEvents) // Protected routes -router.get( - '/:address/invalidate-metadata', - authenticateJWT, - routeCache.cacheSeconds(120), - invalidateMetadata -) +router.get('/:address/invalidate-metadata', routeCache.cacheSeconds(120), invalidateMetadata) export default router diff --git a/packages/api/src/utils/auth.ts b/packages/api/src/utils/auth.ts deleted file mode 100644 index ffeeed58..00000000 --- a/packages/api/src/utils/auth.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Request } from 'express' -import rateLimit from 'express-rate-limit' -import { getNetwork } from '../viem/viemClient' - -export const rateLimiter = rateLimit({ - windowMs: 1 * 60 * 1000, - max: getNetwork().testnet ? 30 : 9999, - standardHeaders: true, - legacyHeaders: false, - keyGenerator: (req: Request): string => { - return req.ip ?? 'unknown' - }, - message: "You've reached the limit of 30 requests per minute. Contact us for increased limits." -}) diff --git a/packages/api/src/utils/authCache.ts b/packages/api/src/utils/authCache.ts new file mode 100644 index 00000000..807e412a --- /dev/null +++ b/packages/api/src/utils/authCache.ts @@ -0,0 +1,16 @@ +import { refreshAuthStore } from './authMiddleware' +import NodeCache from 'node-cache' + +/** + * Init cache that stores `accessLevel` & `accountRestricted` per api token. + * On server boot, load it up with all auth data from db. + * + */ +export const authStore = new NodeCache({ stdTTL: 7 * 24 * 60 * 60 }) // 1 week +refreshAuthStore() + +/** + * Init cache that collects `newRequests` per api token. + * + */ +export const requestsStore = new NodeCache({ stdTTL: 7 * 24 * 60 * 60 }) // 1 week diff --git a/packages/api/src/utils/authMiddleware.ts b/packages/api/src/utils/authMiddleware.ts new file mode 100644 index 00000000..83686c5c --- /dev/null +++ b/packages/api/src/utils/authMiddleware.ts @@ -0,0 +1,254 @@ +import 'dotenv/config' + +import type { NextFunction, Request, Response } from 'express' +import { authStore, requestsStore } from './authCache' +import rateLimit from 'express-rate-limit' + +// --- Types --- + +export interface User { + id: string + accessLevel: number + apiTokens: string[] + requests: number +} + +interface Plan { + name: string + requestsPerMin?: number + requestsPerMonth?: number +} + +// --- Config for plans we offer --- + +const PLANS: Record = { + 0: { + name: 'Unauthenticated', + requestsPerMin: 30, // Remove in v2 + requestsPerMonth: 1_000 // Remove in v2 + }, + 1: { + name: 'Free', + requestsPerMin: 30, + requestsPerMonth: 1_000 + }, + 2: { + name: 'Basic', + requestsPerMin: 1_000, + requestsPerMonth: 10_000 + }, + 999: { + name: 'Admin' + } +} + +// --- Authentication --- + +/** + * Authenticates the user via API Token and handles any limit imposition basis access level + * Designed for speed over strictness, always giving user benefit of the doubt + * + * -1 -> Account restricted (monthly limit hit) + * 0 -> No API token (req will be blocked in v2) + * 1 -> Hobby plan or server/db error + * 2 -> Basic plan + * 998 -> Fallback to db to in case auth store is updating (temp state, gets re-assigned to another value) + * 999 -> Admin access + * + * @param req + * @param res + * @param next + * @returns + */ +export const authenticator = async (req: Request, res: Response, next: NextFunction) => { + const apiToken = req.header('X-API-Token') + let accessLevel: number + + // Find access level + if (!apiToken) { + accessLevel = 0 + } else { + const updatedAt: number | undefined = authStore.get('updatedAt') + + if (!updatedAt && !authStore.get('isRefreshing')) refreshAuthStore() + const accountRestricted = authStore.get(`apiToken:${apiToken}:accountRestricted`) || 0 // Benefit of the doubt + + if (process.env.EE_AUTH_TOKEN === apiToken) { + accessLevel = 999 + } else if (accountRestricted === 0) { + accessLevel = authStore.get(`apiToken:${apiToken}:accessLevel`) ?? 998 + } else { + accessLevel = -1 + } + } + + // Handle limiting basis access level + if (accessLevel === 998) { + const response = await fetch(`${process.env.SUPABASE_FETCH_ACCESS_LEVEL_URL}/${apiToken}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${process.env.SUPABASE_SERVICE_ROLE_KEY}`, + 'Content-Type': 'application/json' + } + }) + const payload = await response.json() + accessLevel = response.ok ? Number(payload?.data?.accessLevel) : 1 // Benefit of the doubt + } + + // --- LIMITING TO BE ACTIVATED IN V2 --- + if (accessLevel === 0) accessLevel = 1 + if (accessLevel === -1) accessLevel = 1 + + /* + if (accessLevel === 0) { + return res.status(401).json({ + error: `Missing or invalid API token. Please generate a valid token on https://dev.eigenexplorer.com and attach it with header 'X-API-Token'.` + }) + } + + if (accessLevel === -1) { + return res.status(401).json({ + error: 'You have reached your monthly limit. Please contact us to increase limits.' + }) + } + */ + // --- LIMITING TO BE ACTIVATED IN V2 --- + + req.accessLevel = accessLevel + next() +} + +// --- Rate Limiting --- + +/** + * Create rate limiters for each Plan + * Note: In v2, we remove the check for `accessLevel === 0` because unauthenticated users would not have passed `authenticator` + * + */ + +const rateLimiters: Record> = {} + +for (const [level, plan] of Object.entries(PLANS)) { + const accessLevel = Number(level) + + if (accessLevel === 999) continue + + rateLimiters[accessLevel] = rateLimit({ + windowMs: 1 * 60 * 1000, + max: plan.requestsPerMin, + standardHeaders: true, + legacyHeaders: false, + keyGenerator: (req: Request): string => + accessLevel === 0 ? req.ip ?? 'unknown' : req.header('X-API-Token') || '', + message: `You've reached the limit of ${plan.requestsPerMin} requests per minute. ${ + accessLevel === 0 + ? 'Sign up for a plan on https://dev.eigenexplorer.com for increased limits.' + : 'Upgrade your plan for increased limits.' + }` + }) +} + +/** + * Return a rate limiter basis the caller's access level + * + * @param req + * @param res + * @param next + * @returns + */ +export const rateLimiter = (req: Request, res: Response, next: NextFunction) => { + const accessLevel = req.accessLevel || 0 + + // No rate limiting for admin + if (accessLevel === 999) { + return next() + } + + // Apply rate limiting + const limiter = rateLimiters[accessLevel] + + // Increment `requestsStore` for successful requests + const originalEnd = res.end + + // biome-ignore lint/suspicious/noExplicitAny: + res.end = function (chunk?: any, encoding?: any, cb?: any) { + try { + if (res.statusCode >= 200 && res.statusCode < 300) { + const apiToken = req.header('X-API-Token') + if (apiToken) { + const key = `apiToken:${apiToken}:newRequests` + const currentCalls: number = requestsStore.get(key) || 0 + requestsStore.set(key, currentCalls + 1) + } + } + } catch {} + + res.end = originalEnd + return originalEnd.call(this, chunk, encoding, cb) + } + + return limiter(req, res, next) +} + +// --- Auth store management --- + +/** + * Fetch all user auth data from Supabase edge function and refresh auth store. + * + */ +export async function refreshAuthStore() { + if (authStore.get('isRefreshing')) { + return false + } + + try { + authStore.flushAll() + authStore.set('isRefreshing', true) + + let skip = 0 + const take = 10_000 + + while (true) { + const response = await fetch( + `${process.env.SUPABASE_FETCH_ALL_USERS_URL}?skip=${skip}&take=${take}`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${process.env.SUPABASE_SERVICE_ROLE_KEY}`, + 'Content-Type': 'application/json' + } + } + ) + + if (!response.ok) { + throw new Error() + } + + const users = (await response.json()).data as User[] + + if (users.length === 0) break + + for (const user of users) { + const accessLevel = user.accessLevel || 0 + const apiTokens = user.apiTokens || [] + const requests = user.requests || 0 + + for (const apiToken of apiTokens) { + authStore.set(`apiToken:${apiToken}:accessLevel`, accessLevel) + authStore.set( + `apiToken:${apiToken}:accountRestricted`, + requests <= (PLANS[accessLevel].requestsPerMonth ?? Number.POSITIVE_INFINITY) ? 0 : 1 + ) + } + } + skip += take + } + + authStore.set('updatedAt', Date.now()) + return true + } catch { + return false + } finally { + authStore.set('isRefreshing', false) + } +} diff --git a/packages/api/src/utils/jwtUtils.ts b/packages/api/src/utils/jwtUtils.ts deleted file mode 100644 index 86ef553c..00000000 --- a/packages/api/src/utils/jwtUtils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import 'dotenv/config' -import jwt from 'jsonwebtoken' -import type { Request, Response, NextFunction } from 'express' - -const JWT_SECRET = process.env.JWT_SECRET || '' - -export function authenticateJWT(req: Request, res: Response, next: NextFunction) { - const token = req.header('Authorization')?.split(' ')[1] - - if (!token) { - return res.status(401).json({ message: 'Access denied. No token provided.' }) - } - - try { - jwt.verify(token, JWT_SECRET) - next() - } catch (error) { - res.status(400).json({ message: 'Invalid token.' }) - } -} diff --git a/packages/api/src/utils/request.ts b/packages/api/src/utils/request.ts new file mode 100644 index 00000000..aed37e71 --- /dev/null +++ b/packages/api/src/utils/request.ts @@ -0,0 +1,9 @@ +import * as express from 'express' + +declare global { + namespace Express { + interface Request { + accessLevel?: number + } + } +} diff --git a/packages/api/src/utils/userRequestsSync.ts b/packages/api/src/utils/userRequestsSync.ts new file mode 100644 index 00000000..9e659794 --- /dev/null +++ b/packages/api/src/utils/userRequestsSync.ts @@ -0,0 +1,93 @@ +import type { User } from './authMiddleware' +import { requestsStore } from './authCache' +import cron from 'node-cron' + +/** + * Send updates to DB with number of requests in the past hour per user + * Cron job runs at the start of every hour + * + */ +export function startUserRequestsSync() { + cron.schedule('0 * * * *', async () => { + console.time('[Data] User requests sync') + + let isUpdateSuccess = true + let skip = 0 + const take = 10_000 + + while (true) { + try { + const getResponse = await fetch( + `${process.env.SUPABASE_FETCH_ALL_USERS_URL}?skip=${skip}&take=${take}`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${process.env.SUPABASE_SERVICE_ROLE_KEY}`, + 'Content-Type': 'application/json' + } + } + ) + + if (!getResponse.ok) { + throw new Error() + } + + const users = (await getResponse.json()).data as User[] + + if (users.length === 0) break + + const updateList: { id: string; requests: number }[] = [] + for (const user of users) { + const apiTokens = user.apiTokens ?? [] + let totalNewRequests = 0 + + for (const apiToken of apiTokens) { + const key = `apiToken:${apiToken}:newRequests` + const newRequests = Number(requestsStore.get(key)) || 0 + if (newRequests > 0) totalNewRequests += newRequests + requestsStore.del(key) + } + + if (totalNewRequests > 0) { + updateList.push({ + id: user.id, + requests: user.requests + totalNewRequests + }) + } + } + + if (updateList.length > 0) { + const postResponse = await fetch(`${process.env.SUPABASE_POST_REQUESTS_URL}`, { + method: 'POST', + headers: { + Authorization: `Bearer ${process.env.SUPABASE_SERVICE_ROLE_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(updateList) + }) + + if (!postResponse.ok) { + throw new Error() + } + + for (const user of users) { + const apiTokens = user.apiTokens ?? [] + + for (const apiToken of apiTokens) { + requestsStore.del(`apiToken:${apiToken}:newRequests`) + } + } + + console.log(`[Data] User requests sync: size: ${updateList.length}`) + } + } catch { + isUpdateSuccess = false + } + + skip += take + } + + if (isUpdateSuccess) requestsStore.flushAll() // Delete remaining (stale) keys once full update is successful + console.timeEnd('[Data] User requests sync') + }) +} From 2ee74cbbb5ac72a31a775837b0cb49dc814f829d Mon Sep 17 00:00:00 2001 From: Udit Veerwani <25996904+uditdc@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:10:56 +0530 Subject: [PATCH 11/17] FIx issues with dependencies for node-cron (#295) --- packages/api/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/api/package.json b/packages/api/package.json index f2cc9f53..543fac60 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -24,6 +24,7 @@ "jsonwebtoken": "^9.0.2", "morgan": "~1.9.1", "node-cache": "^5.1.2", + "node-cron": "^3.0.3", "route-cache": "^0.7.0", "viem": "^2.8.14", "zod": "^3.23.4", @@ -36,6 +37,7 @@ "@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/route-cache": "^0.5.5", "prisma": "^5.11.0", From 4ba28f91b70d95929760d05cb183a1fba5672d4d Mon Sep 17 00:00:00 2001 From: Surbhit Agrawal <82264758+surbhit14@users.noreply.github.com> Date: Mon, 9 Dec 2024 21:22:11 +0530 Subject: [PATCH 12/17] [Feat] - Events API V2 (#293) * Add WithIndividualAmountQuerySchema * Add the events route * Add events/rewards route for AVS * Add event routes for staker * Rename function to getOperatorDelegationEvents * Add all the event routes * Rename function to getOperatorDelegationEvents * Remove the unnecessary file * Add the event related schemas * Add all the util and helper functions realted to events * Use fetchRewardsEvents * Use the event util functions for delegation * Add all the functions for the global event routes * Use a combined reduce * Modify description of schema * Resolve the conflict issues * Move import to top --- packages/api/src/routes/avs/avsController.ts | 49 + packages/api/src/routes/avs/avsRoutes.ts | 3 + .../api/src/routes/events/eventsController.ts | 204 ++++ .../api/src/routes/events/eventsRoutes.ts | 22 + packages/api/src/routes/index.ts | 2 + .../routes/operators/operatorController.ts | 232 +---- .../src/routes/operators/operatorRoutes.ts | 4 +- .../src/routes/stakers/stakerController.ts | 174 ++++ .../api/src/routes/stakers/stakerRoutes.ts | 9 +- .../src/schema/zod/schemas/eventSchemas.ts | 235 +++++ .../src/schema/zod/schemas/operatorEvents.ts | 177 ---- .../schema/zod/schemas/withTokenDataQuery.ts | 9 + packages/api/src/utils/eventUtils.ts | 949 ++++++++++++++++++ 13 files changed, 1687 insertions(+), 382 deletions(-) create mode 100644 packages/api/src/routes/events/eventsController.ts create mode 100644 packages/api/src/routes/events/eventsRoutes.ts create mode 100644 packages/api/src/schema/zod/schemas/eventSchemas.ts delete mode 100644 packages/api/src/schema/zod/schemas/operatorEvents.ts create mode 100644 packages/api/src/utils/eventUtils.ts diff --git a/packages/api/src/routes/avs/avsController.ts b/packages/api/src/routes/avs/avsController.ts index bdbb4a0b..2f5c89b5 100644 --- a/packages/api/src/routes/avs/avsController.ts +++ b/packages/api/src/routes/avs/avsController.ts @@ -20,6 +20,8 @@ import Prisma from '@prisma/client' import prisma from '../../utils/prismaClient' import { fetchTokenPrices } from '../../utils/tokenPrices' import { withOperatorShares } from '../../utils/operatorShares' +import { fetchRewardsEvents } from '../../utils/eventUtils' +import { RewardsEventQuerySchema } from '../../schema/zod/schemas/eventSchemas' /** * Function for route /avs @@ -617,6 +619,53 @@ export async function getAVSRewards(req: Request, res: Response) { handleAndReturnErrorResponse(req, res, error) } } +/** + * Function for route /avs/:address/events/rewards + * Fetches and returns a list of rewards-related events for a specific AVS + * + * @param req + * @param res + */ +export async function getAVSRewardsEvents(req: Request, res: Response) { + const result = RewardsEventQuerySchema.and(PaginationQuerySchema).safeParse(req.query) + if (!result.success) return handleAndReturnErrorResponse(req, res, result.error) + + try { + const { address } = req.params + + const { + rewardsSubmissionToken, + rewardsSubmissionHash, + startAt, + endAt, + withEthValue, + withIndividualAmount, + skip, + take + } = result.data + + const response = await fetchRewardsEvents({ + avsAddress: address, + rewardsSubmissionToken, + rewardsSubmissionHash, + startAt, + endAt, + withEthValue, + withIndividualAmount, + skip, + take + }) + + response.eventRecords.forEach((event) => 'avs' in event.args && (event.args.avs = undefined)) + + res.send({ + data: response.eventRecords, + meta: { total: response.total, skip, take } + }) + } catch (error) { + handleAndReturnErrorResponse(req, res, error) + } +} /** * Function for route /avs/:address/invalidate-metadata diff --git a/packages/api/src/routes/avs/avsRoutes.ts b/packages/api/src/routes/avs/avsRoutes.ts index 697c278c..3f89dd1b 100644 --- a/packages/api/src/routes/avs/avsRoutes.ts +++ b/packages/api/src/routes/avs/avsRoutes.ts @@ -6,6 +6,7 @@ import { getAVSOperators, getAVSStakers, getAVSRewards, + getAVSRewardsEvents, invalidateMetadata } from './avsController' @@ -26,6 +27,8 @@ router.get('/:address/operators', routeCache.cacheSeconds(120), getAVSOperators) router.get('/:address/rewards', routeCache.cacheSeconds(120), getAVSRewards) +router.get('/:address/events/rewards', routeCache.cacheSeconds(120), getAVSRewardsEvents) + // Protected routes router.get('/:address/invalidate-metadata', routeCache.cacheSeconds(120), invalidateMetadata) diff --git a/packages/api/src/routes/events/eventsController.ts b/packages/api/src/routes/events/eventsController.ts new file mode 100644 index 00000000..718fe29b --- /dev/null +++ b/packages/api/src/routes/events/eventsController.ts @@ -0,0 +1,204 @@ +import type { Request, Response } from 'express' +import { handleAndReturnErrorResponse } from '../../schema/errors' +import { PaginationQuerySchema } from '../../schema/zod/schemas/paginationQuery' +import { + DelegationEventQuerySchema, + DepositEventQuerySchema, + RewardsEventQuerySchema, + WithdrawalEventQuerySchema +} from '../../schema/zod/schemas/eventSchemas' +import { + fetchDelegationEvents, + fetchDepositEvents, + fetchGlobalWithdrawalEvents, + fetchRewardsEvents +} from '../../utils/eventUtils' +import { + WithEthValueQuerySchema, + WithIndividualAmountQuerySchema +} from '../../schema/zod/schemas/withTokenDataQuery' + +/** + * Function for route /events/delegation + * Fetches and returns a list of delegation-related events + * + * @param req + * @param res + */ +export async function getDelegationEvents(req: Request, res: Response) { + const result = DelegationEventQuerySchema.and(PaginationQuerySchema).safeParse(req.query) + if (!result.success) return handleAndReturnErrorResponse(req, res, result.error) + + try { + const { + type, + strategyAddress, + txHash, + startAt, + endAt, + withTokenData, + withEthValue, + skip, + take + } = result.data + + const response = await fetchDelegationEvents({ + type, + strategyAddress, + txHash, + startAt, + endAt, + withTokenData, + withEthValue, + skip, + take + }) + + res.send({ + data: response.eventRecords, + meta: { total: response.total, skip, take } + }) + } catch (error) { + handleAndReturnErrorResponse(req, res, error) + } +} + +/** + * Function for route /events/rewards + * Fetches and returns a list of rewards-related events + * + * @param req + * @param res + */ +export async function getRewardsEvents(req: Request, res: Response) { + const result = RewardsEventQuerySchema.and(WithEthValueQuerySchema) + .and(WithIndividualAmountQuerySchema) + .and(PaginationQuerySchema) + .safeParse(req.query) + if (!result.success) return handleAndReturnErrorResponse(req, res, result.error) + + try { + const { + rewardsSubmissionToken, + rewardsSubmissionHash, + startAt, + endAt, + withEthValue, + withIndividualAmount, + skip, + take + } = result.data + + const response = await fetchRewardsEvents({ + rewardsSubmissionToken, + rewardsSubmissionHash, + startAt, + endAt, + withEthValue, + withIndividualAmount, + skip, + take + }) + + res.send({ + data: response.eventRecords, + meta: { total: response.total, skip, take } + }) + } catch (error) { + handleAndReturnErrorResponse(req, res, error) + } +} + +/** + * Function for route /events/deposit + * Fetches and returns a list of deposit-related events + * + * @param req + * @param res + */ +export async function getDepositEvents(req: Request, res: Response) { + const result = DepositEventQuerySchema.and(PaginationQuerySchema).safeParse(req.query) + if (!result.success) return handleAndReturnErrorResponse(req, res, result.error) + + try { + const { + tokenAddress, + strategyAddress, + txHash, + startAt, + endAt, + withTokenData, + withEthValue, + skip, + take + } = result.data + + const response = await fetchDepositEvents({ + tokenAddress, + strategyAddress, + txHash, + startAt, + endAt, + withTokenData, + withEthValue, + skip, + take + }) + + res.send({ + data: response.eventRecords, + meta: { total: response.total, skip, take } + }) + } catch (error) { + handleAndReturnErrorResponse(req, res, error) + } +} + +/** + * Function for route /events/withdrawal + * Fetches and returns a list of withdrawal-related events + * + * @param req + * @param res + */ +export async function getWithdrawalEvents(req: Request, res: Response) { + const result = WithdrawalEventQuerySchema.and(PaginationQuerySchema).safeParse(req.query) + if (!result.success) return handleAndReturnErrorResponse(req, res, result.error) + + try { + const { + type, + txHash, + startAt, + endAt, + withdrawalRoot, + delegatedTo, + withdrawer, + skip, + take, + withTokenData, + withEthValue + } = result.data + + const response = await fetchGlobalWithdrawalEvents({ + type, + txHash, + startAt, + endAt, + withdrawalRoot, + delegatedTo, + withdrawer, + skip, + take, + withTokenData, + withEthValue + }) + + res.send({ + data: response.eventRecords, + meta: { total: response.total, skip, take } + }) + } catch (error) { + handleAndReturnErrorResponse(req, res, error) + } +} diff --git a/packages/api/src/routes/events/eventsRoutes.ts b/packages/api/src/routes/events/eventsRoutes.ts new file mode 100644 index 00000000..f9646775 --- /dev/null +++ b/packages/api/src/routes/events/eventsRoutes.ts @@ -0,0 +1,22 @@ +import express from 'express' +import routeCache from 'route-cache' +import { + getDelegationEvents, + getRewardsEvents, + getDepositEvents, + getWithdrawalEvents +} from './eventsController' + +const router = express.Router() + +// API routes for /events + +router.get('/delegation', routeCache.cacheSeconds(120), getDelegationEvents) + +router.get('/rewards', routeCache.cacheSeconds(120), getRewardsEvents) + +router.get('/deposit', routeCache.cacheSeconds(120), getDepositEvents) + +router.get('/withdrawal', routeCache.cacheSeconds(120), getWithdrawalEvents) + +export default router diff --git a/packages/api/src/routes/index.ts b/packages/api/src/routes/index.ts index a9d40e04..0f8e24ae 100644 --- a/packages/api/src/routes/index.ts +++ b/packages/api/src/routes/index.ts @@ -8,6 +8,7 @@ import withdrawalRoutes from './withdrawals/withdrawalRoutes' import depositRoutes from './deposits/depositRoutes' import auxiliaryRoutes from './auxiliary/auxiliaryRoutes' import rewardRoutes from './rewards/rewardRoutes' +import eventRoutes from './events/eventsRoutes' import authRoutes from './auth/authRoutes' import { authenticator, rateLimiter } from '../utils/authMiddleware' @@ -31,6 +32,7 @@ apiRouter.use('/withdrawals', authenticator, rateLimiter, withdrawalRoutes) apiRouter.use('/deposits', authenticator, rateLimiter, depositRoutes) apiRouter.use('/aux', authenticator, rateLimiter, auxiliaryRoutes) apiRouter.use('/rewards', authenticator, rateLimiter, rewardRoutes) +apiRouter.use('/events', authenticator, rateLimiter, eventRoutes) apiRouter.use('/auth', authenticator, rateLimiter, authRoutes) export default apiRouter diff --git a/packages/api/src/routes/operators/operatorController.ts b/packages/api/src/routes/operators/operatorController.ts index dc7e0f94..4e1e5cf5 100644 --- a/packages/api/src/routes/operators/operatorController.ts +++ b/packages/api/src/routes/operators/operatorController.ts @@ -6,8 +6,8 @@ import { WithAdditionalDataQuerySchema } from '../../schema/zod/schemas/withAddi import { SortByQuerySchema } from '../../schema/zod/schemas/sortByQuery' import { SearchByTextQuerySchema } from '../../schema/zod/schemas/searchByTextQuery' import { WithRewardsQuerySchema } from '../../schema/zod/schemas/withRewardsQuery' +import { OperatorDelegationEventQuerySchema } from '../../schema/zod/schemas/eventSchemas' import { RequestHeadersSchema } from '../../schema/zod/schemas/auth' -import { OperatorEventQuerySchema } from '../../schema/zod/schemas/operatorEvents' import { handleAndReturnErrorResponse } from '../../schema/errors' import { getStrategiesWithShareUnderlying, @@ -18,23 +18,7 @@ import { withOperatorShares } from '../../utils/operatorShares' import Prisma from '@prisma/client' import prisma from '../../utils/prismaClient' import { fetchTokenPrices } from '../../utils/tokenPrices' - -type EventRecordArgs = { - staker: string - strategy?: string - shares?: number -} - -type EventRecord = { - type: 'SHARES_INCREASED' | 'SHARES_DECREASED' | 'DELEGATION' | 'UNDELEGATION' - tx: string - blockNumber: number - blockTime: Date - args: EventRecordArgs - underlyingToken?: string - underlyingValue?: number - ethValue?: number -} +import { fetchDelegationEvents } from '../../utils/eventUtils' /** * Function for route /operators @@ -390,23 +374,23 @@ export async function getOperatorRewards(req: Request, res: Response) { } /** - * Function for route /operators/:address/events - * Fetches and returns a list of events for a specific operator with optional filters + * Function for route /operators/:address/events/delegation + * Fetches and returns a list of delegation-related events for a specific operator * * @param req * @param res */ -export async function getOperatorEvents(req: Request, res: Response) { - const result = OperatorEventQuerySchema.and(PaginationQuerySchema).safeParse(req.query) - if (!result.success) { - return handleAndReturnErrorResponse(req, res, result.error) - } +export async function getOperatorDelegationEvents(req: Request, res: Response) { + const result = OperatorDelegationEventQuerySchema.and(PaginationQuerySchema).safeParse(req.query) + if (!result.success) return handleAndReturnErrorResponse(req, res, result.error) try { + const { address } = req.params + const { type, - stakerAddress, strategyAddress, + stakerAddress, txHash, startAt, endAt, @@ -415,74 +399,29 @@ export async function getOperatorEvents(req: Request, res: Response) { skip, take } = result.data - const { address } = req.params - const baseFilterQuery = { - operator: { - contains: address, - mode: 'insensitive' - }, - ...(stakerAddress && { - staker: { - contains: stakerAddress, - mode: 'insensitive' - } - }), - ...(strategyAddress && { - strategy: { - contains: strategyAddress, - mode: 'insensitive' - } - }), - ...(txHash && { - transactionHash: { - contains: txHash, - mode: 'insensitive' - } - }), - blockTime: { - gte: new Date(startAt), - ...(endAt ? { lte: new Date(endAt) } : {}) - } - } - - let eventRecords: EventRecord[] = [] - let eventCount = 0 - - const eventTypesToFetch = type - ? [type] - : strategyAddress - ? ['SHARES_INCREASED', 'SHARES_DECREASED'] - : ['SHARES_INCREASED', 'SHARES_DECREASED', 'DELEGATION', 'UNDELEGATION'] - - const fetchEventsForTypes = async (types: string[]) => { - const results = await Promise.all( - types.map((eventType) => - fetchAndMapEvents(eventType, baseFilterQuery, withTokenData, withEthValue, skip, take) - ) - ) - return results - } - - const results = await fetchEventsForTypes(eventTypesToFetch) - - eventRecords = results.flatMap((result) => result.eventRecords) - eventRecords = eventRecords - .sort((a, b) => (b.blockNumber > a.blockNumber ? 1 : -1)) - .slice(0, take) - - eventCount = results.reduce((acc, result) => acc + result.eventCount, 0) + const response = await fetchDelegationEvents({ + operatorAddress: address, + stakerAddress, + type, + strategyAddress, + txHash, + startAt, + endAt, + withTokenData, + withEthValue, + skip, + take + }) - const response = { - data: eventRecords, - meta: { - total: eventCount, - skip, - take - } - } + response.eventRecords.forEach( + (event) => 'operator' in event.args && (event.args.operator = undefined) + ) - res.send(response) + res.send({ + data: response.eventRecords, + meta: { total: response.total, skip, take } + }) } catch (error) { handleAndReturnErrorResponse(req, res, error) } @@ -714,114 +653,3 @@ async function calculateOperatorApy(operator: any) { return Array.from(avsApyMap.values()) } catch {} } - -/** - * Utility function to fetch and map event records from the database. - * - * @param eventType - * @param baseFilterQuery - * @param skip - * @param take - * @returns - */ -async function fetchAndMapEvents( - eventType: string, - baseFilterQuery: any, - withTokenData: boolean, - withEthValue: boolean, - skip: number, - take: number -): Promise<{ eventRecords: EventRecord[]; eventCount: number }> { - const modelName = (() => { - switch (eventType) { - case 'SHARES_INCREASED': - return 'eventLogs_OperatorSharesIncreased' - case 'SHARES_DECREASED': - return 'eventLogs_OperatorSharesDecreased' - case 'DELEGATION': - return 'eventLogs_StakerDelegated' - case 'UNDELEGATION': - return 'eventLogs_StakerUndelegated' - default: - throw new Error(`Unknown event type: ${eventType}`) - } - })() - - const model = prisma[modelName] as any - - const eventCount = await model.count({ - where: baseFilterQuery - }) - - const eventLogs = await model.findMany({ - where: baseFilterQuery, - skip, - take, - orderBy: { blockNumber: 'desc' } - }) - - const strategiesWithSharesUnderlying = withTokenData - ? await getStrategiesWithShareUnderlying() - : undefined - - const eventRecords = await Promise.all( - eventLogs.map(async (event) => { - let underlyingToken: string | undefined - let underlyingValue: number | undefined - let ethValue: number | undefined - - if ( - withTokenData && - (eventType === 'SHARES_INCREASED' || eventType === 'SHARES_DECREASED') && - event.strategy - ) { - const strategy = await prisma.strategies.findUnique({ - where: { - address: event.strategy.toLowerCase() - } - }) - - if (strategy && strategiesWithSharesUnderlying) { - underlyingToken = strategy.underlyingToken - - const sharesUnderlying = strategiesWithSharesUnderlying.find( - (s) => s.strategyAddress.toLowerCase() === event.strategy.toLowerCase() - ) - - if (sharesUnderlying) { - underlyingValue = - Number( - (BigInt(event.shares) * BigInt(sharesUnderlying.sharesToUnderlying)) / BigInt(1e18) - ) / 1e18 - - if (withEthValue && sharesUnderlying.ethPrice) { - ethValue = underlyingValue * sharesUnderlying.ethPrice - } - } - } - } - - return { - type: eventType, - tx: event.transactionHash, - blockNumber: event.blockNumber, - blockTime: event.blockTime, - args: { - staker: event.staker.toLowerCase(), - strategy: event.strategy?.toLowerCase(), - shares: event.shares - }, - ...(withTokenData && { - underlyingToken: underlyingToken?.toLowerCase(), - underlyingValue - }), - ...(withEthValue && { ethValue }) - } - }) - ) - - return { - eventRecords, - eventCount - } -} diff --git a/packages/api/src/routes/operators/operatorRoutes.ts b/packages/api/src/routes/operators/operatorRoutes.ts index a3f48ed7..51c894fd 100644 --- a/packages/api/src/routes/operators/operatorRoutes.ts +++ b/packages/api/src/routes/operators/operatorRoutes.ts @@ -4,7 +4,7 @@ import { getOperator, getAllOperatorAddresses, getOperatorRewards, - getOperatorEvents, + getOperatorDelegationEvents, invalidateMetadata } from './operatorController' @@ -108,7 +108,7 @@ router.get('/:address', routeCache.cacheSeconds(120), getOperator) router.get('/:address/rewards', routeCache.cacheSeconds(120), getOperatorRewards) -router.get('/:address/events/delegation', routeCache.cacheSeconds(120), getOperatorEvents) +router.get('/:address/events/delegation', routeCache.cacheSeconds(120), getOperatorDelegationEvents) // Protected routes router.get('/:address/invalidate-metadata', routeCache.cacheSeconds(120), invalidateMetadata) diff --git a/packages/api/src/routes/stakers/stakerController.ts b/packages/api/src/routes/stakers/stakerController.ts index 365f059b..2c680704 100644 --- a/packages/api/src/routes/stakers/stakerController.ts +++ b/packages/api/src/routes/stakers/stakerController.ts @@ -16,6 +16,16 @@ import { EthereumAddressSchema } from '../../schema/zod/schemas/base/ethereumAdd import { UpdatedSinceQuerySchema } from '../../schema/zod/schemas/updatedSinceQuery' import { WithRewardsQuerySchema } from '../../schema/zod/schemas/withRewardsQuery' import { ActiveQuerySchema } from '../../schema/zod/schemas/activeQuery' +import { + fetchDelegationEvents, + fetchDepositEvents, + fetchStakerWithdrawalEvents +} from '../../utils/eventUtils' +import { + StakerDelegationEventQuerySchema, + DepositEventQuerySchema, + WithdrawalEventQuerySchema +} from '../../schema/zod/schemas/eventSchemas' /** * Route to get a list of all stakers @@ -475,6 +485,170 @@ export async function getStakerDeposits(req: Request, res: Response) { } } +/** + * Function for route /stakers/:address/events/delegation + * Fetches and returns a list of delegation-related events for a specific staker + * + * @param req + * @param res + */ +export async function getStakerDelegationEvents(req: Request, res: Response) { + const result = StakerDelegationEventQuerySchema.and(PaginationQuerySchema).safeParse(req.query) + if (!result.success) return handleAndReturnErrorResponse(req, res, result.error) + + try { + const { address } = req.params + + const { + type, + strategyAddress, + operatorAddress, + txHash, + startAt, + endAt, + withTokenData, + withEthValue, + skip, + take + } = result.data + + const response = await fetchDelegationEvents({ + stakerAddress: address, + operatorAddress, + type, + strategyAddress, + txHash, + startAt, + endAt, + withTokenData, + withEthValue, + skip, + take + }) + + response.eventRecords.forEach( + (event) => 'staker' in event.args && (event.args.staker = undefined) + ) + + res.send({ + data: response.eventRecords, + meta: { total: response.total, skip, take } + }) + } catch (error) { + handleAndReturnErrorResponse(req, res, error) + } +} + +/** + * Function for route /stakers/:address/events/deposit + * Fetches and returns a list of deposit-related events for a specific staker + * + * @param req + * @param res + */ +export async function getStakerDepositEvents(req: Request, res: Response) { + const result = DepositEventQuerySchema.and(PaginationQuerySchema).safeParse(req.query) + if (!result.success) return handleAndReturnErrorResponse(req, res, result.error) + + try { + const { address } = req.params + + const { + tokenAddress, + strategyAddress, + txHash, + startAt, + endAt, + withTokenData, + withEthValue, + skip, + take + } = result.data + + const response = await fetchDepositEvents({ + stakerAddress: address, + tokenAddress, + strategyAddress, + txHash, + startAt, + endAt, + withTokenData, + withEthValue, + skip, + take + }) + + response.eventRecords.forEach((event) => { + if ('staker' in event.args) { + event.args.staker = undefined + } + }) + + res.send({ + data: response.eventRecords, + meta: { total: response.total, skip, take } + }) + } catch (error) { + handleAndReturnErrorResponse(req, res, error) + } +} + +/** + * Function for route /stakers/:address/events/withdrawal + * Fetches and returns a list of withdrawal-related events for a specific staker with optional filters + * + * @param req + * @param res + */ +export async function getStakerWithdrawalEvents(req: Request, res: Response) { + const result = WithdrawalEventQuerySchema.and(PaginationQuerySchema).safeParse(req.query) + if (!result.success) return handleAndReturnErrorResponse(req, res, result.error) + + try { + const { address } = req.params + + const { + type, + txHash, + startAt, + endAt, + withdrawalRoot, + delegatedTo, + withdrawer, + skip, + take, + withTokenData, + withEthValue + } = result.data + + const response = await fetchStakerWithdrawalEvents({ + stakerAddress: address, + type, + txHash, + startAt, + endAt, + withdrawalRoot, + delegatedTo, + withdrawer, + skip, + take, + withTokenData, + withEthValue + }) + + response.eventRecords.forEach( + (event) => 'staker' in event.args && (event.args.staker = undefined) + ) + + res.send({ + data: response.eventRecords, + meta: { total: response.total, skip, take } + }) + } catch (error) { + handleAndReturnErrorResponse(req, res, error) + } +} + // --- Helper functions --- async function calculateStakerRewards( diff --git a/packages/api/src/routes/stakers/stakerRoutes.ts b/packages/api/src/routes/stakers/stakerRoutes.ts index 868da07d..9453c56f 100644 --- a/packages/api/src/routes/stakers/stakerRoutes.ts +++ b/packages/api/src/routes/stakers/stakerRoutes.ts @@ -6,7 +6,10 @@ import { getStakerWithdrawalsCompleted, getStakerWithdrawalsQueued, getStakerWithdrawalsWithdrawable, - getStakerDeposits + getStakerDeposits, + getStakerDelegationEvents, + getStakerDepositEvents, + getStakerWithdrawalEvents } from './stakerController' const router = express.Router() @@ -22,4 +25,8 @@ router.get('/:address/withdrawals/completed', getStakerWithdrawalsCompleted) router.get('/:address/deposits', getStakerDeposits) +router.get('/:address/events/delegation', getStakerDelegationEvents) +router.get('/:address/events/deposit', getStakerDepositEvents) +router.get('/:address/events/withdrawal', getStakerWithdrawalEvents) + export default router diff --git a/packages/api/src/schema/zod/schemas/eventSchemas.ts b/packages/api/src/schema/zod/schemas/eventSchemas.ts new file mode 100644 index 00000000..eff6c8c4 --- /dev/null +++ b/packages/api/src/schema/zod/schemas/eventSchemas.ts @@ -0,0 +1,235 @@ +import z from '../' +import { getValidatedDates, validateDateRange } from '../../../utils/eventUtils' +import { + WithTokenDataQuerySchema, + WithEthValueQuerySchema, + WithIndividualAmountQuerySchema +} from './withTokenDataQuery' + +const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ +const yyyymmddRegex = /^\d{4}-\d{2}-\d{2}$/ + +// Reusable refinement functions +const refineWithEthValueRequiresTokenData = (schema: z.ZodTypeAny) => + schema.refine( + (data) => { + if (data.withEthValue && !data.withTokenData) { + return false + } + return true + }, + { + message: "'withEthValue' requires 'withTokenData' to be enabled.", + path: ['withEthValue'] + } + ) + +const refineStartEndDates = (schema: z.ZodTypeAny) => + schema + .refine( + (data) => { + if (data.startAt && data.endAt) { + return new Date(data.endAt).getTime() >= new Date(data.startAt).getTime() + } + return true + }, + { + message: 'endAt must be after startAt', + path: ['endAt'] + } + ) + .refine( + (data) => { + try { + const dates = getValidatedDates(data.startAt, data.endAt) + Object.assign(data, dates) + + return validateDateRange(data.startAt, data.endAt) + } catch { + return false + } + }, + { + message: 'Duration between startAt and endAt exceeds the allowed limit of 30 days.', + path: ['startAt', 'endAt'] + } + ) + +const refineDelegationTypeRestrictions = (schema: z.ZodTypeAny) => + schema.refine( + (data) => { + if (data.type === 'DELEGATION' || data.type === 'UNDELEGATION') { + if (data.strategyAddress || data.withTokenData || data.withEthValue) { + return false + } + } + return true + }, + { + message: + "'strategyAddress', 'withTokenData', and 'withEthValue' filters are not supported for DELEGATION or UNDELEGATION event types.", + path: ['type'] + } + ) + +const refineWithdrawalTypeRestrictions = (schema: z.ZodTypeAny) => + schema.refine( + (data) => { + if (data.type === 'WITHDRAWAL_COMPLETED') { + if (data.withdrawer || data.delegatedTo || data.withTokenData || data.withEthValue) { + return false + } + } + return true + }, + { + message: + "'withdrawer', 'delegatedTo','withTokenData' and 'withEthValue' filters are not supported for WITHDRAWAL_COMPLETED event type.", + path: ['type'] + } + ) + +// Base schema for shared fields +const BaseEventQuerySchema = z.object({ + txHash: z + .string() + .regex(/^0x([A-Fa-f0-9]{64})$/, 'Invalid transaction hash') + .optional() + .describe('The transaction hash associated with the event'), + startAt: z + .string() + .optional() + .refine( + (val) => + !val || + ((isoRegex.test(val) || yyyymmddRegex.test(val)) && !Number.isNaN(new Date(val).getTime())), + { + message: 'Invalid date format for startAt. Use YYYY-MM-DD or ISO 8601 format.' + } + ) + .default('') + .describe('Start date in ISO string format'), + endAt: z + .string() + .optional() + .refine( + (val) => + !val || + ((isoRegex.test(val) || yyyymmddRegex.test(val)) && !Number.isNaN(new Date(val).getTime())), + { + message: 'Invalid date format for endAt. Use YYYY-MM-DD or ISO 8601 format.' + } + ) + .default('') + .describe('End date in ISO string format') +}) + +const WithdrawalEventQuerySchemaBase = BaseEventQuerySchema.extend({ + type: z + .enum(['WITHDRAWAL_QUEUED', 'WITHDRAWAL_COMPLETED']) + .optional() + .describe('The type of the withdrawal event'), + withdrawalRoot: z + .string() + .regex(/^0x[a-fA-F0-9]{64}$/, 'Invalid withdrawal root format') + .optional() + .describe('The withdrawal root associated with the event'), + delegatedTo: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address') + .optional() + .describe('The address to which funds were delegated'), + withdrawer: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address') + .optional() + .describe('The address of the withdrawer') +}) + .merge(WithTokenDataQuerySchema) + .merge(WithEthValueQuerySchema) + +export const WithdrawalEventQuerySchema = refineWithdrawalTypeRestrictions( + refineWithEthValueRequiresTokenData(refineStartEndDates(WithdrawalEventQuerySchemaBase)) +) + +const DepositEventQuerySchemaBase = BaseEventQuerySchema.extend({ + tokenAddress: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid token address') + .optional() + .describe('The contract address of the token'), + strategyAddress: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid strategy address') + .optional() + .describe('The contract address of the restaking strategy') +}) + .merge(WithTokenDataQuerySchema) + .merge(WithEthValueQuerySchema) + +export const DepositEventQuerySchema = refineWithEthValueRequiresTokenData( + refineStartEndDates(DepositEventQuerySchemaBase) +) + +const DelegationEventQuerySchemaBase = BaseEventQuerySchema.extend({ + type: z + .enum(['SHARES_INCREASED', 'SHARES_DECREASED', 'DELEGATION', 'UNDELEGATION']) + .optional() + .describe('The type of the delegation event'), + strategyAddress: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address') + .optional() + .describe('The contract address of the restaking strategy') +}) + .merge(WithTokenDataQuerySchema) + .merge(WithEthValueQuerySchema) + +export const DelegationEventQuerySchema = refineDelegationTypeRestrictions( + refineWithEthValueRequiresTokenData(refineStartEndDates(DelegationEventQuerySchemaBase)) +) + +export const OperatorDelegationEventQuerySchema = refineDelegationTypeRestrictions( + refineWithEthValueRequiresTokenData( + refineStartEndDates( + DelegationEventQuerySchemaBase.extend({ + stakerAddress: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address') + .optional() + .describe('The address of the staker') + }) + ) + ) +) + +export const StakerDelegationEventQuerySchema = refineDelegationTypeRestrictions( + refineWithEthValueRequiresTokenData( + refineStartEndDates( + DelegationEventQuerySchemaBase.extend({ + operatorAddress: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address') + .optional() + .describe('The address of the operator') + }) + ) + ) +) + +const RewardsEventQuerySchemaBase = BaseEventQuerySchema.extend({ + rewardsSubmissionHash: z + .string() + .regex(/^0x([A-Fa-f0-9]{64})$/, 'Invalid reward submission hash') + .optional() + .describe('The reward submission hash associated with the event'), + rewardsSubmissionToken: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address') + .optional() + .describe('The token address used for the rewards submission') +}) + .merge(WithIndividualAmountQuerySchema) + .merge(WithEthValueQuerySchema) + +export const RewardsEventQuerySchema = refineStartEndDates(RewardsEventQuerySchemaBase) diff --git a/packages/api/src/schema/zod/schemas/operatorEvents.ts b/packages/api/src/schema/zod/schemas/operatorEvents.ts deleted file mode 100644 index 21063d09..00000000 --- a/packages/api/src/schema/zod/schemas/operatorEvents.ts +++ /dev/null @@ -1,177 +0,0 @@ -import z from '../' -import { WithTokenDataQuerySchema, WithEthValueQuerySchema } from './withTokenDataQuery' - -const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ -const yyyymmddRegex = /^\d{4}-\d{2}-\d{2}$/ -const maxDuration = 30 * 24 * 60 * 60 * 1000 // 30 days -const defaultDuration = 7 * 24 * 60 * 60 * 1000 // 7 days - -/** - * Validates that the given time range doesn't exceed the max allowed duration. - * - * @param startAt - * @param endAt - * @returns - */ -const validateDateRange = (startAt: string, endAt: string) => { - const start = new Date(startAt) - const end = new Date(endAt || new Date()) - const durationMs = end.getTime() - start.getTime() - return durationMs <= maxDuration -} - -/** - * Utility to get default dates if not provided. - * Default to last 7 days - * - * @param startAt - * @param endAt - * @returns - */ -const getValidatedDates = (startAt?: string, endAt?: string) => { - const now = new Date() - - if (!startAt && !endAt) { - return { - startAt: new Date(now.getTime() - defaultDuration).toISOString(), - endAt: null - } - } - - if (startAt && !endAt) { - const start = new Date(startAt) - return { - startAt, - endAt: new Date(Math.min(start.getTime() + defaultDuration, now.getTime())).toISOString() - } - } - - if (!startAt && endAt) { - const end = new Date(endAt) - return { - startAt: new Date(end.getTime() - defaultDuration).toISOString(), - endAt - } - } - - return { startAt, endAt } -} - -export const OperatorEventQuerySchema = z - .object({ - stakerAddress: z - .string() - .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address') - .optional() - .describe('The address of the staker') - .openapi({ example: '0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd' }), - - strategyAddress: z - .string() - .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address') - .optional() - .describe('The contract address of the restaking strategy') - .openapi({ example: '0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6' }), - - txHash: z - .string() - .regex(/^0x([A-Fa-f0-9]{64})$/, 'Invalid transaction hash') - .optional() - .describe('The transaction hash associated with the event') - .openapi({ example: '0xe95a203b1a91a908f9b9ce46459d101078c2c3cb' }), - - type: z - .enum(['SHARES_INCREASED', 'SHARES_DECREASED', 'DELEGATION', 'UNDELEGATION']) - .optional() - .describe('The type of the operator event') - .openapi({ example: 'SHARES_INCREASED' }), - - startAt: z - .string() - .optional() - .refine( - (val) => - !val || - ((isoRegex.test(val) || yyyymmddRegex.test(val)) && - !Number.isNaN(new Date(val).getTime())), - { - message: 'Invalid date format for startAt. Use YYYY-MM-DD or ISO 8601 format.' - } - ) - .default('') - .describe('Start date in ISO string format') - .openapi({ example: '2024-04-11T08:31:11.000' }), - - endAt: z - .string() - .optional() - .refine( - (val) => - !val || - ((isoRegex.test(val) || yyyymmddRegex.test(val)) && - !Number.isNaN(new Date(val).getTime())), - { - message: 'Invalid date format for endAt. Use YYYY-MM-DD or ISO 8601 format.' - } - ) - .default('') - .describe('End date in ISO string format') - .openapi({ example: '2024-04-12T08:31:11.000' }) - }) - .merge(WithTokenDataQuerySchema) - .merge(WithEthValueQuerySchema) - .refine( - (data) => { - if (data.withEthValue && !data.withTokenData) { - return false - } - return true - }, - { - message: "'withEthValue' requires 'withTokenData' to be enabled.", - path: ['withEthValue'] - } - ) - .refine( - (data) => { - if ((data.type === 'DELEGATION' || data.type === 'UNDELEGATION') && data.strategyAddress) { - return false - } - return true - }, - { - message: - 'strategyAddress filter is not supported for DELEGATION or UNDELEGATION event types.', - path: ['strategyAddress'] - } - ) - .refine( - (data) => { - if (data.startAt && data.endAt) { - return new Date(data.endAt).getTime() >= new Date(data.startAt).getTime() - } - return true - }, - { - message: 'endAt must be after startAt', - path: ['endAt'] - } - ) - .refine( - (data) => { - try { - const dates = getValidatedDates(data.startAt, data.endAt) - Object.assign(data, dates) - - return validateDateRange(data.startAt, data.endAt) - } catch { - return false - } - }, - { - message: 'Duration between startAt and endAt exceeds the allowed limit of 30 days.', - path: ['startAt', 'endAt'] - } - ) - -export default OperatorEventQuerySchema diff --git a/packages/api/src/schema/zod/schemas/withTokenDataQuery.ts b/packages/api/src/schema/zod/schemas/withTokenDataQuery.ts index 2048ceb9..35556580 100644 --- a/packages/api/src/schema/zod/schemas/withTokenDataQuery.ts +++ b/packages/api/src/schema/zod/schemas/withTokenDataQuery.ts @@ -19,3 +19,12 @@ export const WithEthValueQuerySchema = z.object({ .transform((val) => val === 'true') .openapi({ example: 'false' }) }) + +export const WithIndividualAmountQuerySchema = z.object({ + withIndividualAmount: z + .enum(['true', 'false']) + .default('false') + .describe('Toggle whether the route should return individual share amount for each strategy') + .transform((val) => val === 'true') + .openapi({ example: 'false' }) +}) diff --git a/packages/api/src/utils/eventUtils.ts b/packages/api/src/utils/eventUtils.ts new file mode 100644 index 00000000..b719f922 --- /dev/null +++ b/packages/api/src/utils/eventUtils.ts @@ -0,0 +1,949 @@ +import prisma from './prismaClient' +import { getStrategiesWithShareUnderlying } from './strategyShares' +import { fetchTokenPrices } from './tokenPrices' +import Prisma from '@prisma/client' + +type EventRecord = { + type: string + tx: string + blockNumber: number + blockTime: Date + args: EventArgs +} + +type UnderlyingTokenDetails = { + underlyingToken?: string + underlyingValue?: number + ethValue?: number +} + +type StrategyData = { + strategy: string + shares: number +} & UnderlyingTokenDetails + +export type EventArgs = DelegationArgs | DepositArgs | WithdrawalArgs | RewardArgs + +type DelegationArgs = { + operator?: string + staker?: string + strategy?: string + shares?: number +} & UnderlyingTokenDetails + +type DepositArgs = { + token: string + strategy: string + shares: number + staker?: string +} & UnderlyingTokenDetails + +type WithdrawalArgs = { + withdrawalRoot: string + staker?: string + delegatedTo?: string + withdrawer?: string + nonce?: number + startBlock?: number + strategies?: StrategyData[] +} + +type RewardArgs = { + avs?: string + submissionNonce: number + rewardsSubmissionHash: string + rewardsSubmissionToken: string + rewardsSubmissionAmount: string + rewardsSubmissionStartTimeStamp: number + rewardsSubmissionDuration: number + strategies: { + strategy: string + multiplier: string + amount?: string + amountEthValue?: number + }[] + ethValue?: number +} + +/** + * Utility function to fetch delegation events. + * + * @param operatorAddress + * @param stakerAddress + * @param type + * @param strategyAddress + * @param txHash + * @param startAt + * @param endAt + * @param withTokenData + * @param withEthValue + * @param skip + * @param take + * @returns + */ +export async function fetchDelegationEvents({ + operatorAddress, + stakerAddress, + type, + strategyAddress, + txHash, + startAt, + endAt, + withTokenData, + withEthValue, + skip, + take +}: { + operatorAddress?: string + stakerAddress?: string + type?: string + strategyAddress?: string + txHash?: string + startAt?: string + endAt?: string + withTokenData: boolean + withEthValue: boolean + skip: number + take: number +}): Promise<{ eventRecords: EventRecord[]; total: number }> { + const eventTypes = strategyAddress + ? ['SHARES_INCREASED', 'SHARES_DECREASED'] + : ['SHARES_INCREASED', 'SHARES_DECREASED', 'DELEGATION', 'UNDELEGATION'] + + const typesToFetch = type ? [type] : eventTypes + + const baseFilterQuery = { + ...(strategyAddress && { + strategy: { + contains: strategyAddress, + mode: 'insensitive' + } + }), + ...(operatorAddress && { + operator: { + contains: operatorAddress, + mode: 'insensitive' + } + }), + ...(stakerAddress && { + staker: { + contains: stakerAddress, + mode: 'insensitive' + } + }), + ...(txHash && { + transactionHash: { + contains: txHash, + mode: 'insensitive' + } + }), + blockTime: { + gte: new Date(startAt as string), + ...(endAt ? { lte: new Date(endAt as string) } : {}) + } + } + + const results = await Promise.all( + typesToFetch.map((eventType) => fetchAndMapEvents(eventType, baseFilterQuery, 0, skip + take)) + ) + + const { allEvents, totalCount } = results.reduce<{ + allEvents: EventRecord[] + totalCount: number + }>( + (acc, result) => { + acc.allEvents.push(...result.eventRecords) + acc.totalCount += result.eventCount + return acc + }, + { allEvents: [], totalCount: 0 } + ) + + const sortedEvents = sortEvents(allEvents) + const paginatedEvents = sortedEvents.slice(skip, skip + take) + + const enrichedEvents = await enrichEventsWithTokenData( + paginatedEvents, + withTokenData, + withEthValue + ) + + return { + eventRecords: enrichedEvents, + total: totalCount + } +} + +/** + * Utility function to fetch deposit events. + * + * @param stakerAddress + * @param tokenAddress + * @param strategyAddress + * @param txHash + * @param startAt + * @param endAt + * @param withTokenData + * @param withEthValue + * @param skip + * @param take + * @returns + */ +export async function fetchDepositEvents({ + stakerAddress, + tokenAddress, + strategyAddress, + txHash, + startAt, + endAt, + withTokenData, + withEthValue, + skip, + take +}: { + stakerAddress?: string + tokenAddress?: string + strategyAddress?: string + txHash?: string + startAt?: string + endAt?: string + withTokenData: boolean + withEthValue: boolean + skip: number + take: number +}): Promise<{ eventRecords: EventRecord[]; total: number }> { + const baseFilterQuery = { + ...(stakerAddress && { + staker: { + contains: stakerAddress, + mode: 'insensitive' + } + }), + ...(tokenAddress && { + token: { + contains: tokenAddress, + mode: 'insensitive' + } + }), + ...(strategyAddress && { + strategy: { + contains: strategyAddress, + mode: 'insensitive' + } + }), + ...(txHash && { + transactionHash: { + contains: txHash, + mode: 'insensitive' + } + }), + blockTime: { + gte: new Date(startAt as string), + ...(endAt ? { lte: new Date(endAt as string) } : {}) + } + } + + const results = await fetchAndMapEvents('DEPOSIT', baseFilterQuery, skip, take) + + const enrichedEvents = await enrichEventsWithTokenData( + results.eventRecords, + withTokenData, + withEthValue + ) + + return { + eventRecords: enrichedEvents, + total: results.eventCount + } +} + +/** + * Utility function to fetch rewards events. + * + * @param avsAddress + * @param rewardsSubmissionToken + * @param rewardsSubmissionHash + * @param startAt + * @param endAt + * @param withIndividualAmount + * @param withEthValue + * @param skip + * @param take + * @returns + */ +export async function fetchRewardsEvents({ + avsAddress, + rewardsSubmissionToken, + rewardsSubmissionHash, + startAt, + endAt, + withIndividualAmount, + withEthValue, + skip, + take +}: { + withEthValue: boolean + withIndividualAmount: boolean + skip: number + take: number + avsAddress?: string + rewardsSubmissionToken?: string + rewardsSubmissionHash?: string + startAt?: string + endAt?: string +}): Promise<{ eventRecords: EventRecord[]; total: number }> { + const baseFilterQuery = { + ...(avsAddress && { avs: { contains: avsAddress, mode: 'insensitive' } }), + ...(rewardsSubmissionToken && { + rewardsSubmission_token: { contains: rewardsSubmissionToken, mode: 'insensitive' } + }), + ...(rewardsSubmissionHash && { + rewardsSubmissionHash: { contains: rewardsSubmissionHash, mode: 'insensitive' } + }), + blockTime: { + gte: new Date(startAt as string), + ...(endAt ? { lte: new Date(endAt as string) } : {}) + } + } + + const { eventRecords: rawEventRecords, eventCount: totalRecords } = await fetchAndMapEvents( + 'REWARDS', + baseFilterQuery, + skip, + take + ) + + const tokenPrices = withEthValue ? await fetchTokenPrices() : [] + + const enrichedEvents = rawEventRecords.map((event) => { + const args = event.args as RewardArgs + const totalAmount = new Prisma.Prisma.Decimal(args.rewardsSubmissionAmount) + + const tokenPrice = tokenPrices.find( + (tp) => tp.address.toLowerCase() === args.rewardsSubmissionToken.toLowerCase() + ) + + const ethPrice = tokenPrice?.ethPrice ?? 0 + const decimals = tokenPrice?.decimals ?? 18 + + const ethValue = withEthValue + ? totalAmount + .div(new Prisma.Prisma.Decimal(10).pow(decimals)) + .mul(new Prisma.Prisma.Decimal(ethPrice)) + .toNumber() + : undefined + + if (withIndividualAmount) { + const strategies = args.strategies as Array<{ strategy: string; multiplier: string }> + + const totalMultiplier = strategies + .map((s) => new Prisma.Prisma.Decimal(s.multiplier)) + .reduce((acc, m) => acc.add(m), new Prisma.Prisma.Decimal(0)) + + args.strategies = strategies.map((strategy) => { + const multiplier = new Prisma.Prisma.Decimal(strategy.multiplier) + const individualAmount = totalAmount + .mul(multiplier) + .div(totalMultiplier) + .toNumber() + .toFixed(0) + + const amountEthValue = withEthValue + ? new Prisma.Prisma.Decimal(individualAmount) + .div(new Prisma.Prisma.Decimal(10).pow(decimals)) + .mul(new Prisma.Prisma.Decimal(ethPrice)) + .toNumber() + : undefined + + return { + ...strategy, + amount: individualAmount, + ...(withEthValue && { amountEthValue }) + } + }) + } + + return { + ...event, + ...(withEthValue && { ethValue }) + } + }) + + return { + eventRecords: enrichedEvents, + total: totalRecords + } +} + +/** + * Utility function to fetch all withdrawal events. + * + * @param type + * @param txHash + * @param startAt + * @param endAt + * @param withdrawalRoot + * @param delegatedTo + * @param withdrawer + * @param skip + * @param take + * @param withTokenData + * @param withEthValue + */ +export async function fetchGlobalWithdrawalEvents({ + type, + txHash, + startAt, + endAt, + withdrawalRoot, + delegatedTo, + withdrawer, + skip, + take, + withTokenData, + withEthValue +}: { + type?: string + txHash?: string + startAt?: string + endAt?: string + withdrawalRoot?: string + delegatedTo?: string + withdrawer?: string + skip: number + take: number + withTokenData: boolean + withEthValue: boolean +}): Promise<{ eventRecords: EventRecord[]; total: number }> { + const eventTypes = + delegatedTo || withdrawer + ? ['WITHDRAWAL_QUEUED'] + : ['WITHDRAWAL_QUEUED', 'WITHDRAWAL_COMPLETED'] + + const typesToFetch = type ? [type] : eventTypes + + const baseFilterQuery = { + ...(withdrawalRoot && { + withdrawalRoot: { + contains: withdrawalRoot, + mode: 'insensitive' + } + }), + ...(delegatedTo && { + delegatedTo: { + contains: delegatedTo, + mode: 'insensitive' + } + }), + ...(withdrawer && { + withdrawer: { + contains: withdrawer, + mode: 'insensitive' + } + }), + ...(txHash && { + transactionHash: { + contains: txHash, + mode: 'insensitive' + } + }), + blockTime: { + gte: new Date(startAt as string), + ...(endAt ? { lte: new Date(endAt as string) } : {}) + } + } + + const results = await Promise.all( + typesToFetch.map((eventType) => fetchAndMapEvents(eventType, baseFilterQuery, 0, skip + take)) + ) + + const { allEvents, totalCount } = results.reduce<{ + allEvents: EventRecord[] + totalCount: number + }>( + (acc, result) => { + acc.allEvents.push(...result.eventRecords) + acc.totalCount += result.eventCount + return acc + }, + { allEvents: [], totalCount: 0 } + ) + + const sortedEvents = sortEvents(allEvents) + const paginatedEvents = sortedEvents.slice(skip, skip + take) + + const enrichedEvents = await enrichEventsWithTokenData( + paginatedEvents, + withTokenData, + withEthValue + ) + + return { + eventRecords: enrichedEvents, + total: totalCount + } +} + +/** + * Utility function to fetch withdrawal events for a specific staker. + * + * @param stakerAddress + * @param type + * @param txHash + * @param startAt + * @param endAt + * @param withdrawalRoot + * @param delegatedTo + * @param withdrawer + * @param skip + * @param take + * @param withTokenData + * @param withEthValue + */ +export async function fetchStakerWithdrawalEvents({ + stakerAddress, + type, + txHash, + startAt, + endAt, + withdrawalRoot, + delegatedTo, + withdrawer, + skip, + take, + withTokenData, + withEthValue +}: { + stakerAddress: string + type?: string + txHash?: string + startAt?: string + endAt?: string + withdrawalRoot?: string + delegatedTo?: string + withdrawer?: string + skip: number + take: number + withTokenData: boolean + withEthValue: boolean +}): Promise<{ eventRecords: EventRecord[]; total: number }> { + let queuedEvents: EventRecord[] = [] + let completedEvents: EventRecord[] = [] + + const queuedFilterQuery = { + staker: { + contains: stakerAddress, + mode: 'insensitive' + }, + ...(withdrawalRoot && { + withdrawalRoot: { + contains: withdrawalRoot, + mode: 'insensitive' + } + }), + ...(delegatedTo && { + delegatedTo: { + contains: delegatedTo, + mode: 'insensitive' + } + }), + ...(withdrawer && { + withdrawer: { + contains: withdrawer, + mode: 'insensitive' + } + }), + ...(txHash && { + transactionHash: { + contains: txHash, + mode: 'insensitive' + } + }) + } + + const completedFilterQuery = { + ...(withdrawalRoot && { + withdrawalRoot: { + contains: withdrawalRoot, + mode: 'insensitive' + } + }), + ...(txHash && { + transactionHash: { + contains: txHash, + mode: 'insensitive' + } + }), + blockTime: { + gte: new Date(startAt as string), + ...(endAt ? { lte: new Date(endAt as string) } : {}) + } + } + + const queuedResult = await fetchAndMapEvents('WITHDRAWAL_QUEUED', queuedFilterQuery, 0, undefined) + const filteredQueuedEvents = queuedResult.eventRecords.filter((event) => { + const blockTime = new Date(event.blockTime) + return (!startAt || blockTime >= new Date(startAt)) && (!endAt || blockTime <= new Date(endAt)) + }) + + if (type === 'WITHDRAWAL_QUEUED') { + const paginatedEvents = filteredQueuedEvents.slice(skip, skip + take) + const enrichedEvents = await enrichEventsWithTokenData( + paginatedEvents, + withTokenData, + withEthValue + ) + return { + eventRecords: enrichedEvents, + total: filteredQueuedEvents.length + } + } + + queuedEvents = queuedResult.eventRecords + if (txHash || withdrawalRoot) { + const completedResult = await fetchAndMapEvents( + 'WITHDRAWAL_COMPLETED', + completedFilterQuery, + 0, + undefined + ) + completedEvents = completedResult.eventRecords + } else { + const withdrawalRoots = queuedEvents + .map((event) => (event.args as WithdrawalArgs).withdrawalRoot) + .filter((root): root is string => root !== undefined) + if (withdrawalRoots && withdrawalRoots.length) { + const completedResult = await fetchAndMapEvents( + 'WITHDRAWAL_COMPLETED', + { + withdrawalRoot: { in: withdrawalRoots }, + blockTime: { + gte: new Date(startAt as string), + ...(endAt ? { lte: new Date(endAt as string) } : {}) + } + }, + 0, + undefined + ) + completedEvents = completedResult.eventRecords + } + } + const allEvents = + type === 'WITHDRAWAL_COMPLETED' + ? completedEvents + : [...filteredQueuedEvents, ...completedEvents] + + const sortedEvents = sortEvents(allEvents) + const paginatedEvents = sortedEvents.slice(skip, skip + take) + + const enrichedEvents = await enrichEventsWithTokenData( + paginatedEvents, + withTokenData, + withEthValue + ) + + return { + eventRecords: enrichedEvents, + total: allEvents.length + } +} + +// Helper Functions +const maxDuration = 30 * 24 * 60 * 60 * 1000 // 30 days +const defaultDuration = 7 * 24 * 60 * 60 * 1000 // 7 days + +/** + * Validates that the given time range doesn't exceed the max allowed duration. + * + * @param startAt + * @param endAt + * @returns + */ +export const validateDateRange = (startAt: string, endAt: string) => { + const start = new Date(startAt) + const end = new Date(endAt || new Date()) + const durationMs = end.getTime() - start.getTime() + return durationMs <= maxDuration +} + +/** + * Helper function to get default dates if not provided. + * Default to last 7 days + * + * @param startAt + * @param endAt + * @returns + */ +export const getValidatedDates = (startAt?: string, endAt?: string) => { + const now = new Date() + + if (!startAt && !endAt) { + return { + startAt: new Date(now.getTime() - defaultDuration).toISOString(), + endAt: null + } + } + + if (startAt && !endAt) { + const start = new Date(startAt) + return { + startAt, + endAt: new Date(Math.min(start.getTime() + defaultDuration, now.getTime())).toISOString() + } + } + + if (!startAt && endAt) { + const end = new Date(endAt) + return { + startAt: new Date(end.getTime() - defaultDuration).toISOString(), + endAt + } + } + + return { startAt, endAt } +} + +/** + * Enrich events with `withTokenData` and `withEthValue` logic. + * + * @param events + * @param withTokenData + * @param withEthValue + * @returns + */ +async function enrichEventsWithTokenData( + events: EventRecord[], + withTokenData: boolean, + withEthValue: boolean +): Promise { + if (!withTokenData) { + return events + } + + return Promise.all( + events.map(async (event) => { + const detailedStrategies: StrategyData[] = [] + let underlyingToken: string | undefined + let underlyingValue: number | undefined + let ethValue: number | undefined + + if ( + event.type === 'WITHDRAWAL_QUEUED' && + 'strategies' in event.args && + event.args.strategies + ) { + for (const strategy of event.args.strategies) { + if ('shares' in strategy) { + const detailedData = await calculateStrategyData( + strategy.strategy, + BigInt(strategy.shares) + ) + + detailedStrategies.push({ + strategy: strategy.strategy, + shares: strategy.shares, + underlyingToken: detailedData.underlyingToken, + underlyingValue: detailedData.underlyingValue, + ...(withEthValue ? { ethValue: detailedData.ethValue } : {}) + }) + } + } + ;(event.args as WithdrawalArgs).strategies = detailedStrategies + } else if ( + ['SHARES_INCREASED', 'SHARES_DECREASED', 'DEPOSIT'].includes(event.type) && + 'strategy' in event.args && + event.args.strategy + ) { + const detailedData = await calculateStrategyData( + event.args.strategy, + BigInt(event.args.shares ?? 0) + ) + + underlyingToken = detailedData.underlyingToken + underlyingValue = detailedData.underlyingValue + ethValue = detailedData.ethValue + } + + return { + ...event, + ...(underlyingToken && { underlyingToken }), + ...(underlyingValue !== undefined && { underlyingValue }), + ...(withEthValue && ethValue !== undefined && { ethValue }) + } + }) + ) +} + +/** + * Helper function to calculate underlying token data and ETH values for a strategy. + * + * @param strategyAddress + * @param shares + * @returns + */ +async function calculateStrategyData( + strategyAddress: string, + shares: bigint +): Promise<{ underlyingToken?: string; underlyingValue?: number; ethValue?: number }> { + let underlyingToken: string | undefined + let underlyingValue: number | undefined + let ethValue: number | undefined + + const strategy = await prisma.strategies.findUnique({ + where: { address: strategyAddress.toLowerCase() } + }) + + const strategiesWithSharesUnderlying = await getStrategiesWithShareUnderlying() + + if (strategy && strategiesWithSharesUnderlying) { + underlyingToken = strategy.underlyingToken + const sharesUnderlying = strategiesWithSharesUnderlying.find( + (s) => s.strategyAddress.toLowerCase() === strategyAddress.toLowerCase() + ) + + if (sharesUnderlying) { + underlyingValue = + Number((shares * BigInt(sharesUnderlying.sharesToUnderlying)) / BigInt(1e18)) / 1e18 + + if (sharesUnderlying.ethPrice) { + ethValue = underlyingValue * sharesUnderlying.ethPrice + } + } + } + + return { underlyingToken, underlyingValue, ethValue } +} + +/** + * Helper function to fetch and map event records from the database. + * + * @param eventType + * @param baseFilterQuery + * @param skip + * @param take + * @returns + */ +export async function fetchAndMapEvents( + eventType: string, + baseFilterQuery: any, + skip: number, + take?: number +): Promise<{ eventRecords: EventRecord[]; eventCount: number }> { + const modelName = (() => { + switch (eventType) { + case 'DEPOSIT': + return 'eventLogs_Deposit' + case 'SHARES_INCREASED': + return 'eventLogs_OperatorSharesIncreased' + case 'SHARES_DECREASED': + return 'eventLogs_OperatorSharesDecreased' + case 'DELEGATION': + return 'eventLogs_StakerDelegated' + case 'UNDELEGATION': + return 'eventLogs_StakerUndelegated' + case 'WITHDRAWAL_QUEUED': + return 'eventLogs_WithdrawalQueued' + case 'WITHDRAWAL_COMPLETED': + return 'eventLogs_WithdrawalCompleted' + case 'REWARDS': + return 'eventLogs_AVSRewardsSubmission' + default: + throw new Error(`Unknown event type: ${eventType}`) + } + })() + + const model = prisma[modelName] as any + + const eventCount = await model.count({ where: baseFilterQuery }) + + const eventRecords = await model.findMany({ + where: baseFilterQuery, + skip, + take, + orderBy: { blockNumber: 'desc' } + }) + + const mappedRecords = eventRecords.map((event) => ({ + type: eventType, + tx: event.transactionHash, + blockNumber: event.blockNumber, + blockTime: event.blockTime, + args: mapEventArgs(event, eventType) + })) + + return { + eventRecords: mappedRecords, + eventCount + } +} + +/** + * Helper function to map raw database event data to structured event arguments. + * + * @param event + * @param eventType + * @returns + */ +function mapEventArgs(event: any, eventType: string): EventArgs { + switch (eventType) { + case 'DEPOSIT': + return { + staker: event.staker, + token: event.token, + strategy: event.strategy, + shares: event.shares + } + case 'WITHDRAWAL_QUEUED': + return { + staker: event.staker, + withdrawalRoot: event.withdrawalRoot, + delegatedTo: event.delegatedTo, + withdrawer: event.withdrawer, + nonce: event.nonce, + startBlock: event.startBlock, + strategies: event.strategies?.map((strategy: string, index: number) => ({ + strategy, + shares: event.shares?.[index] + })) + } + case 'WITHDRAWAL_COMPLETED': + return { withdrawalRoot: event.withdrawalRoot } + case 'REWARDS': + return { + avs: event.avs, + submissionNonce: event.submissionNonce, + rewardsSubmissionHash: event.rewardsSubmissionHash, + rewardsSubmissionToken: event.rewardsSubmission_token.toLowerCase(), + rewardsSubmissionAmount: event.rewardsSubmission_amount, + rewardsSubmissionStartTimeStamp: event.rewardsSubmission_startTimestamp, + rewardsSubmissionDuration: event.rewardsSubmission_duration, + strategies: event.strategiesAndMultipliers_strategies.map( + (strategy: string, index: number) => ({ + strategy: strategy.toLowerCase(), + multiplier: event.strategiesAndMultipliers_multipliers[index] + }) + ) + } + default: + return { + operator: event.operator, + staker: event.staker, + strategy: event.strategy, + shares: event.shares + } + } +} + +/** + * Helper function to sort events by blockNumber in descending order. + * + * @param events + * @returns + */ +export function sortEvents(events: T[]): T[] { + return events.sort((a, b) => { + if (b.blockNumber > a.blockNumber) return 1 + if (b.blockNumber < a.blockNumber) return -1 + return 0 + }) +} From 2ec438637d05336932cb63eee7761b912122e121 Mon Sep 17 00:00:00 2001 From: Gowtham Sundaresan <131300352+gowthamsundaresan@users.noreply.github.com> Date: Fri, 13 Dec 2024 02:54:06 +0530 Subject: [PATCH 13/17] fix: use settings key to sync reward snapshots (#297) --- .../src/events/seedLogsRewardsSubmissions.ts | 4 +-- packages/seeder/src/monitors/avsApy.ts | 5 +++ .../seeder/src/seedStakerRewardSnapshots.ts | 31 +++++++++++-------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/packages/seeder/src/events/seedLogsRewardsSubmissions.ts b/packages/seeder/src/events/seedLogsRewardsSubmissions.ts index ffa4aa62..f6ba59f9 100644 --- a/packages/seeder/src/events/seedLogsRewardsSubmissions.ts +++ b/packages/seeder/src/events/seedLogsRewardsSubmissions.ts @@ -57,7 +57,7 @@ export async function seedLogsAVSRewardsSubmission(toBlock?: bigint, fromBlock?: if (log.args.rewardsSubmission?.strategiesAndMultipliers) { for (const strategyAndMultiplier of log.args.rewardsSubmission.strategiesAndMultipliers) { strategies.push(strategyAndMultiplier.strategy.toLowerCase()) - multipliers.push(String(strategyAndMultiplier.multiplier.toString())) + multipliers.push(strategyAndMultiplier.multiplier.toString()) } logsAvsRewardsSubmissions.push({ @@ -103,6 +103,6 @@ export async function seedLogsAVSRewardsSubmission(toBlock?: bigint, fromBlock?: dbTransactions, `[Logs] AVS Rewards Submission from: ${fromBlock} to: ${toBlock} size: ${seedLength}` ) - } catch (error) {} + } catch {} }) } diff --git a/packages/seeder/src/monitors/avsApy.ts b/packages/seeder/src/monitors/avsApy.ts index adfa2a79..0a41d499 100644 --- a/packages/seeder/src/monitors/avsApy.ts +++ b/packages/seeder/src/monitors/avsApy.ts @@ -25,6 +25,11 @@ export async function monitorAvsApy() { try { // Fetch totalStakers, totalOperators & rewards data for all avs in this iteration const avsMetrics = await prismaClient.avs.findMany({ + where: { + rewardSubmissions: { + some: {} + } + }, include: { operators: { where: { isActive: true }, diff --git a/packages/seeder/src/seedStakerRewardSnapshots.ts b/packages/seeder/src/seedStakerRewardSnapshots.ts index a6ca1114..6bdcb1fc 100644 --- a/packages/seeder/src/seedStakerRewardSnapshots.ts +++ b/packages/seeder/src/seedStakerRewardSnapshots.ts @@ -1,7 +1,7 @@ import prisma from '@prisma/client' import { getPrismaClient } from './utils/prismaClient' import { getNetwork } from './utils/viemClient' -import { bulkUpdateDbTransactions } from './utils/seeder' +import { bulkUpdateDbTransactions, fetchLastSyncTime } from './utils/seeder' import { fetchTokenPrices } from './utils/tokenPrices' interface ClaimData { @@ -11,12 +11,14 @@ interface ClaimData { cumulative_amount: string } +const timeSyncKey = 'lastSyncedTimestamp_stakerRewardSnapshot' + /** * Seeds the StakerRewardSnapshot table to maintain latest state of all EL stakers * * @returns */ -export async function seedStakerRewardSnapshots() { +export async function seedStakerRewardSnapshots(timestamp?: Date) { const prismaClient = getPrismaClient() const bucketUrl = getBucketUrl() const BATCH_SIZE = 10_000 @@ -40,18 +42,11 @@ export async function seedStakerRewardSnapshots() { .split('T')[0] // Find snapshot date of existing data - const snapshotRecord = await prismaClient.stakerRewardSnapshot.findFirst({ - select: { - timestamp: true - }, - orderBy: { - timestamp: 'asc' // All snapshots should ideally have the same timestamp, but we check for earliest in case of sync issues - } - }) - - const snapshotTimestamp = snapshotRecord?.timestamp?.toISOString()?.split('T')[0] + const lastSyncedTimestamp = timestamp + ? timestamp?.toISOString()?.split('T')[0] + : (await fetchLastSyncTime(timeSyncKey))?.toISOString()?.split('T')[0] - if (latestSnapshotTimestamp === snapshotTimestamp) { + if (latestSnapshotTimestamp === lastSyncedTimestamp) { console.log('[In Sync] [Data] Staker Reward Snapshots') return } @@ -168,6 +163,16 @@ export async function seedStakerRewardSnapshots() { } finally { reader.releaseLock() } + + // Update latest time sync key + await prismaClient.settings.upsert({ + where: { key: timeSyncKey }, + update: { value: Number(latestLog.rewardsCalculationEndTimestamp) * 1000 }, + create: { + key: timeSyncKey, + value: Number(latestLog.rewardsCalculationEndTimestamp) * 1000 + } + }) } catch {} } From f87ba2a47ce62755403a751215658721644f51ac Mon Sep 17 00:00:00 2001 From: Surbhit Agrawal <82264758+surbhit14@users.noreply.github.com> Date: Fri, 13 Dec 2024 02:54:48 +0530 Subject: [PATCH 14/17] 294 feat add event routes for OperatorAvsRegistrationStatusUpdated (#298) * Add the registration routes * Use getOperatorRegistrationEvents * Add the RegistrationEventQuery Schemas * Add the util function fetchRegistrationEvents * Add getRegistrationEvents * Add getAvsRegistrationEvents * Add getOperatorRegistrationEvents * Comments cleanup for fetchRegistrationEvents * Change route to /registration-status * Add toLowerCase() to responses --- packages/api/src/routes/avs/avsController.ts | 45 ++++++- packages/api/src/routes/avs/avsRoutes.ts | 9 +- .../api/src/routes/events/eventsController.ts | 36 +++++- .../api/src/routes/events/eventsRoutes.ts | 5 +- .../routes/operators/operatorController.ts | 49 ++++++- .../src/routes/operators/operatorRoutes.ts | 7 + .../src/schema/zod/schemas/eventSchemas.ts | 26 ++++ packages/api/src/utils/eventUtils.ts | 121 +++++++++++++++--- 8 files changed, 274 insertions(+), 24 deletions(-) diff --git a/packages/api/src/routes/avs/avsController.ts b/packages/api/src/routes/avs/avsController.ts index 2f5c89b5..eb1efb74 100644 --- a/packages/api/src/routes/avs/avsController.ts +++ b/packages/api/src/routes/avs/avsController.ts @@ -20,8 +20,11 @@ import Prisma from '@prisma/client' import prisma from '../../utils/prismaClient' import { fetchTokenPrices } from '../../utils/tokenPrices' import { withOperatorShares } from '../../utils/operatorShares' -import { fetchRewardsEvents } from '../../utils/eventUtils' -import { RewardsEventQuerySchema } from '../../schema/zod/schemas/eventSchemas' +import { fetchRegistrationEvents, fetchRewardsEvents } from '../../utils/eventUtils' +import { + AvsRegistrationEventQuerySchema, + RewardsEventQuerySchema +} from '../../schema/zod/schemas/eventSchemas' /** * Function for route /avs @@ -667,6 +670,44 @@ export async function getAVSRewardsEvents(req: Request, res: Response) { } } +/** + * Function for route avs/:address/events/registration + * Fetches and returns a list of operator-avs registration event for a specific Avs + * + * @param req + * @param res + */ +export async function getAvsRegistrationEvents(req: Request, res: Response) { + const result = AvsRegistrationEventQuerySchema.and(PaginationQuerySchema).safeParse(req.query) + if (!result.success) return handleAndReturnErrorResponse(req, res, result.error) + + try { + const { address } = req.params + + const { operatorAddress, txHash, status, startAt, endAt, skip, take } = result.data + + const response = await fetchRegistrationEvents({ + avsAddress: address, + operatorAddress, + txHash, + status, + startAt, + endAt, + skip, + take + }) + + response.eventRecords.forEach((event) => 'avs' in event.args && (event.args.avs = undefined)) + + res.send({ + data: response.eventRecords, + meta: { total: response.total, skip, take } + }) + } catch (error) { + handleAndReturnErrorResponse(req, res, error) + } +} + /** * Function for route /avs/:address/invalidate-metadata * Protected route to invalidate the metadata of a given AVS diff --git a/packages/api/src/routes/avs/avsRoutes.ts b/packages/api/src/routes/avs/avsRoutes.ts index 3f89dd1b..f9035c60 100644 --- a/packages/api/src/routes/avs/avsRoutes.ts +++ b/packages/api/src/routes/avs/avsRoutes.ts @@ -7,7 +7,8 @@ import { getAVSStakers, getAVSRewards, getAVSRewardsEvents, - invalidateMetadata + invalidateMetadata, + getAvsRegistrationEvents } from './avsController' import routeCache from 'route-cache' @@ -29,6 +30,12 @@ router.get('/:address/rewards', routeCache.cacheSeconds(120), getAVSRewards) router.get('/:address/events/rewards', routeCache.cacheSeconds(120), getAVSRewardsEvents) +router.get( + '/:address/events/registration-status', + routeCache.cacheSeconds(120), + getAvsRegistrationEvents +) + // Protected routes router.get('/:address/invalidate-metadata', routeCache.cacheSeconds(120), invalidateMetadata) diff --git a/packages/api/src/routes/events/eventsController.ts b/packages/api/src/routes/events/eventsController.ts index 718fe29b..da5a50dd 100644 --- a/packages/api/src/routes/events/eventsController.ts +++ b/packages/api/src/routes/events/eventsController.ts @@ -4,6 +4,7 @@ import { PaginationQuerySchema } from '../../schema/zod/schemas/paginationQuery' import { DelegationEventQuerySchema, DepositEventQuerySchema, + RegistrationEventQuerySchema, RewardsEventQuerySchema, WithdrawalEventQuerySchema } from '../../schema/zod/schemas/eventSchemas' @@ -11,7 +12,8 @@ import { fetchDelegationEvents, fetchDepositEvents, fetchGlobalWithdrawalEvents, - fetchRewardsEvents + fetchRewardsEvents, + fetchRegistrationEvents } from '../../utils/eventUtils' import { WithEthValueQuerySchema, @@ -202,3 +204,35 @@ export async function getWithdrawalEvents(req: Request, res: Response) { handleAndReturnErrorResponse(req, res, error) } } + +/** + * Function for route /events/registration + * Fetches and returns a list of operator-avs registration event + * + * @param req + * @param res + */ +export async function getRegistrationEvents(req: Request, res: Response) { + const result = RegistrationEventQuerySchema.and(PaginationQuerySchema).safeParse(req.query) + if (!result.success) return handleAndReturnErrorResponse(req, res, result.error) + + try { + const { txHash, status, startAt, endAt, skip, take } = result.data + + const response = await fetchRegistrationEvents({ + txHash, + status, + startAt, + endAt, + skip, + take + }) + + res.send({ + data: response.eventRecords, + meta: { total: response.total, skip, take } + }) + } catch (error) { + handleAndReturnErrorResponse(req, res, error) + } +} diff --git a/packages/api/src/routes/events/eventsRoutes.ts b/packages/api/src/routes/events/eventsRoutes.ts index f9646775..e640ee60 100644 --- a/packages/api/src/routes/events/eventsRoutes.ts +++ b/packages/api/src/routes/events/eventsRoutes.ts @@ -4,7 +4,8 @@ import { getDelegationEvents, getRewardsEvents, getDepositEvents, - getWithdrawalEvents + getWithdrawalEvents, + getRegistrationEvents } from './eventsController' const router = express.Router() @@ -19,4 +20,6 @@ router.get('/deposit', routeCache.cacheSeconds(120), getDepositEvents) router.get('/withdrawal', routeCache.cacheSeconds(120), getWithdrawalEvents) +router.get('/registration-status', routeCache.cacheSeconds(120), getRegistrationEvents) + export default router diff --git a/packages/api/src/routes/operators/operatorController.ts b/packages/api/src/routes/operators/operatorController.ts index 4e1e5cf5..d77bd1fc 100644 --- a/packages/api/src/routes/operators/operatorController.ts +++ b/packages/api/src/routes/operators/operatorController.ts @@ -6,7 +6,10 @@ import { WithAdditionalDataQuerySchema } from '../../schema/zod/schemas/withAddi import { SortByQuerySchema } from '../../schema/zod/schemas/sortByQuery' import { SearchByTextQuerySchema } from '../../schema/zod/schemas/searchByTextQuery' import { WithRewardsQuerySchema } from '../../schema/zod/schemas/withRewardsQuery' -import { OperatorDelegationEventQuerySchema } from '../../schema/zod/schemas/eventSchemas' +import { + OperatorDelegationEventQuerySchema, + OperatorRegistrationEventQuerySchema +} from '../../schema/zod/schemas/eventSchemas' import { RequestHeadersSchema } from '../../schema/zod/schemas/auth' import { handleAndReturnErrorResponse } from '../../schema/errors' import { @@ -18,7 +21,7 @@ import { withOperatorShares } from '../../utils/operatorShares' import Prisma from '@prisma/client' import prisma from '../../utils/prismaClient' import { fetchTokenPrices } from '../../utils/tokenPrices' -import { fetchDelegationEvents } from '../../utils/eventUtils' +import { fetchDelegationEvents, fetchRegistrationEvents } from '../../utils/eventUtils' /** * Function for route /operators @@ -427,6 +430,48 @@ export async function getOperatorDelegationEvents(req: Request, res: Response) { } } +/** + * Function for route operators/:address/events/registration + * Fetches and returns a list of operator-avs registration event for a specific operator + * + * @param req + * @param res + */ +export async function getOperatorRegistrationEvents(req: Request, res: Response) { + const result = OperatorRegistrationEventQuerySchema.and(PaginationQuerySchema).safeParse( + req.query + ) + if (!result.success) return handleAndReturnErrorResponse(req, res, result.error) + + try { + const { address } = req.params + + const { avsAddress, txHash, status, startAt, endAt, skip, take } = result.data + + const response = await fetchRegistrationEvents({ + operatorAddress: address, + avsAddress, + txHash, + status, + startAt, + endAt, + skip, + take + }) + + response.eventRecords.forEach( + (event) => 'operator' in event.args && (event.args.operator = undefined) + ) + + res.send({ + data: response.eventRecords, + meta: { total: response.total, skip, take } + }) + } catch (error) { + handleAndReturnErrorResponse(req, res, error) + } +} + /** * Function for route /operators/:address/invalidate-metadata * Protected route to invalidate the metadata of a given Operator diff --git a/packages/api/src/routes/operators/operatorRoutes.ts b/packages/api/src/routes/operators/operatorRoutes.ts index 51c894fd..d52f4cf5 100644 --- a/packages/api/src/routes/operators/operatorRoutes.ts +++ b/packages/api/src/routes/operators/operatorRoutes.ts @@ -5,6 +5,7 @@ import { getAllOperatorAddresses, getOperatorRewards, getOperatorDelegationEvents, + getOperatorRegistrationEvents, invalidateMetadata } from './operatorController' @@ -110,6 +111,12 @@ router.get('/:address/rewards', routeCache.cacheSeconds(120), getOperatorRewards router.get('/:address/events/delegation', routeCache.cacheSeconds(120), getOperatorDelegationEvents) +router.get( + '/:address/events/registration-status', + routeCache.cacheSeconds(120), + getOperatorRegistrationEvents +) + // Protected routes router.get('/:address/invalidate-metadata', routeCache.cacheSeconds(120), invalidateMetadata) diff --git a/packages/api/src/schema/zod/schemas/eventSchemas.ts b/packages/api/src/schema/zod/schemas/eventSchemas.ts index eff6c8c4..0b9984a4 100644 --- a/packages/api/src/schema/zod/schemas/eventSchemas.ts +++ b/packages/api/src/schema/zod/schemas/eventSchemas.ts @@ -233,3 +233,29 @@ const RewardsEventQuerySchemaBase = BaseEventQuerySchema.extend({ .merge(WithEthValueQuerySchema) export const RewardsEventQuerySchema = refineStartEndDates(RewardsEventQuerySchemaBase) + +const RegistrationEventQuerySchemaBase = BaseEventQuerySchema.extend({ + status: z.enum(['REGISTERED', 'DEREGISTERED']).optional().describe('The status of Registration') +}) + +export const RegistrationEventQuerySchema = refineStartEndDates(RegistrationEventQuerySchemaBase) + +export const OperatorRegistrationEventQuerySchema = refineStartEndDates( + RegistrationEventQuerySchemaBase.extend({ + avsAddress: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address') + .optional() + .describe('The address of the avs') + }) +) + +export const AvsRegistrationEventQuerySchema = refineStartEndDates( + RegistrationEventQuerySchemaBase.extend({ + operatorAddress: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address') + .optional() + .describe('The address of the operator') + }) +) diff --git a/packages/api/src/utils/eventUtils.ts b/packages/api/src/utils/eventUtils.ts index b719f922..c1032e4d 100644 --- a/packages/api/src/utils/eventUtils.ts +++ b/packages/api/src/utils/eventUtils.ts @@ -22,7 +22,12 @@ type StrategyData = { shares: number } & UnderlyingTokenDetails -export type EventArgs = DelegationArgs | DepositArgs | WithdrawalArgs | RewardArgs +export type EventArgs = + | DelegationArgs + | DepositArgs + | WithdrawalArgs + | RewardArgs + | RegistrationArgs type DelegationArgs = { operator?: string @@ -65,6 +70,12 @@ type RewardArgs = { ethValue?: number } +type RegistrationArgs = { + operator?: string + avs?: string + status: string +} + /** * Utility function to fetch delegation events. * @@ -648,6 +659,74 @@ export async function fetchStakerWithdrawalEvents({ } } +/** + * Utility function to fetch registration events. + * + * @param operatorAddress + * @param avsAddress + * @param txHash + * @param status + * @param startAt + * @param endAt + * @param skip + * @param take + * @returns + */ +export async function fetchRegistrationEvents({ + operatorAddress, + avsAddress, + txHash, + status, + startAt, + endAt, + skip, + take +}: { + operatorAddress?: string + avsAddress?: string + txHash?: string + status?: string + startAt?: string + endAt?: string + skip: number + take: number +}): Promise<{ eventRecords: EventRecord[]; total: number }> { + const baseFilterQuery = { + ...(operatorAddress && { + operator: { + contains: operatorAddress, + mode: 'insensitive' + } + }), + ...(avsAddress && { + avs: { + contains: avsAddress, + mode: 'insensitive' + } + }), + ...(txHash && { + transactionHash: { + contains: txHash, + mode: 'insensitive' + } + }), + ...(status && { + status: status === 'REGISTERED' ? 1 : status === 'DEREGISTERED' ? 0 : undefined + }), + blockTime: { + gte: new Date(startAt as string), + ...(endAt ? { lte: new Date(endAt as string) } : {}) + } + } + + const results = await fetchAndMapEvents('REGISTRATION_STATUS', baseFilterQuery, skip, take) + + return { + eventRecords: results.eventRecords, + total: results.eventCount + } +} + // Helper Functions const maxDuration = 30 * 24 * 60 * 60 * 1000 // 30 days const defaultDuration = 7 * 24 * 60 * 60 * 1000 // 7 days @@ -740,9 +819,9 @@ async function enrichEventsWithTokenData( ) detailedStrategies.push({ - strategy: strategy.strategy, + strategy: strategy.strategy?.toLowerCase(), shares: strategy.shares, - underlyingToken: detailedData.underlyingToken, + underlyingToken: detailedData.underlyingToken?.toLowerCase(), underlyingValue: detailedData.underlyingValue, ...(withEthValue ? { ethValue: detailedData.ethValue } : {}) }) @@ -759,7 +838,7 @@ async function enrichEventsWithTokenData( BigInt(event.args.shares ?? 0) ) - underlyingToken = detailedData.underlyingToken + underlyingToken = detailedData.underlyingToken?.toLowerCase() underlyingValue = detailedData.underlyingValue ethValue = detailedData.ethValue } @@ -847,6 +926,8 @@ export async function fetchAndMapEvents( return 'eventLogs_WithdrawalCompleted' case 'REWARDS': return 'eventLogs_AVSRewardsSubmission' + case 'REGISTRATION_STATUS': + return 'eventLogs_OperatorAVSRegistrationStatusUpdated' default: throw new Error(`Unknown event type: ${eventType}`) } @@ -888,21 +969,21 @@ function mapEventArgs(event: any, eventType: string): EventArgs { switch (eventType) { case 'DEPOSIT': return { - staker: event.staker, - token: event.token, - strategy: event.strategy, + staker: event.staker?.toLowerCase(), + token: event.token?.toLowerCase(), + strategy: event.strategy?.toLowerCase(), shares: event.shares } case 'WITHDRAWAL_QUEUED': return { - staker: event.staker, + staker: event.staker?.toLowerCase(), withdrawalRoot: event.withdrawalRoot, - delegatedTo: event.delegatedTo, - withdrawer: event.withdrawer, + delegatedTo: event.delegatedTo?.toLowerCase(), + withdrawer: event.withdrawer?.toLowerCase(), nonce: event.nonce, startBlock: event.startBlock, strategies: event.strategies?.map((strategy: string, index: number) => ({ - strategy, + strategy: strategy?.toLowerCase(), shares: event.shares?.[index] })) } @@ -910,25 +991,31 @@ function mapEventArgs(event: any, eventType: string): EventArgs { return { withdrawalRoot: event.withdrawalRoot } case 'REWARDS': return { - avs: event.avs, + avs: event.avs?.toLowerCase(), submissionNonce: event.submissionNonce, rewardsSubmissionHash: event.rewardsSubmissionHash, - rewardsSubmissionToken: event.rewardsSubmission_token.toLowerCase(), + rewardsSubmissionToken: event.rewardsSubmission_token?.toLowerCase(), rewardsSubmissionAmount: event.rewardsSubmission_amount, rewardsSubmissionStartTimeStamp: event.rewardsSubmission_startTimestamp, rewardsSubmissionDuration: event.rewardsSubmission_duration, strategies: event.strategiesAndMultipliers_strategies.map( (strategy: string, index: number) => ({ - strategy: strategy.toLowerCase(), + strategy: strategy?.toLowerCase(), multiplier: event.strategiesAndMultipliers_multipliers[index] }) ) } + case 'REGISTRATION_STATUS': + return { + operator: event.operator?.toLowerCase(), + avs: event.avs?.toLowerCase(), + status: event.status === 1 ? 'REGISTERED' : 'DEREGISTERED' + } default: return { - operator: event.operator, - staker: event.staker, - strategy: event.strategy, + operator: event.operator?.toLowerCase(), + staker: event.staker?.toLowerCase(), + strategy: event.strategy?.toLowerCase(), shares: event.shares } } From ae8a2e126e0a419580682ccd8e369df78a3947c9 Mon Sep 17 00:00:00 2001 From: Gowtham Sundaresan <131300352+gowthamsundaresan@users.noreply.github.com> Date: Tue, 17 Dec 2024 13:20:21 +0530 Subject: [PATCH 15/17] chore: update dev-portal url in response message (#303) --- packages/api/src/utils/authMiddleware.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/src/utils/authMiddleware.ts b/packages/api/src/utils/authMiddleware.ts index 83686c5c..cd4a7b4a 100644 --- a/packages/api/src/utils/authMiddleware.ts +++ b/packages/api/src/utils/authMiddleware.ts @@ -102,7 +102,7 @@ export const authenticator = async (req: Request, res: Response, next: NextFunct /* if (accessLevel === 0) { return res.status(401).json({ - error: `Missing or invalid API token. Please generate a valid token on https://dev.eigenexplorer.com and attach it with header 'X-API-Token'.` + error: `Missing or invalid API token. Please generate a valid token on https://developer.eigenexplorer.com and attach it with header 'X-API-Token'.` }) } @@ -142,7 +142,7 @@ for (const [level, plan] of Object.entries(PLANS)) { accessLevel === 0 ? req.ip ?? 'unknown' : req.header('X-API-Token') || '', message: `You've reached the limit of ${plan.requestsPerMin} requests per minute. ${ accessLevel === 0 - ? 'Sign up for a plan on https://dev.eigenexplorer.com for increased limits.' + ? 'Sign up for a plan on https://developer.eigenexplorer.com for increased limits.' : 'Upgrade your plan for increased limits.' }` }) From 486b088d13ce7c7c32ccb8612fc504d3ce807fca Mon Sep 17 00:00:00 2001 From: Surbhit Agrawal <82264758+surbhit14@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:29:44 +0530 Subject: [PATCH 16/17] Adding Events API Routes to Documentation (#299) * Add the new routes * Add event routes to base * Add the response file * Add avs-event route file * Add the global-event files * Add the operator-event file * Add the staker-event files * Add the applyAllRefinements function * Modify the event schemas * Update the openapi file * Modify response descriptions and examples * Change id to getAvsRewardsEvents * Add all to summary * Add registration schemas and other fixes * Add the registration routes * Add WithTokenDataQuerySchema and WithEthValueQuerySchema * Add Registration Event schemas and default dates * Change first letter to lower case * Add latest openapi.json * Remove the hardcoded default date/time value --- .../src/schema/zod/schemas/eventSchemas.ts | 106 +- packages/openapi/openapi.json | 6694 ++++++++++++----- .../apiResponseSchema/events/eventsRespone.ts | 267 + .../src/apiResponseSchema/events/util.ts | 9 + .../withdrawals/withdrawalsResponseSchema.ts | 15 +- packages/openapi/src/documentBase.ts | 4 +- .../routes/avs/getAvsRegistrationEvents.ts | 45 + .../src/routes/avs/getAvsRewardsEvents.ts | 45 + packages/openapi/src/routes/avs/index.ts | 8 +- .../src/routes/events/getDelegationEvents.ts | 49 + .../src/routes/events/getDepositEvents.ts | 41 + .../routes/events/getRegistrationEvents.ts | 37 + .../src/routes/events/getRewardsEvents.ts | 37 + .../src/routes/events/getWithdrawalEvents.ts | 43 + packages/openapi/src/routes/events/index.ts | 14 + .../operators/getOperatorDelegationEvents.ts | 51 + .../getOperatorRegistrationEvents.ts | 46 + .../openapi/src/routes/operators/index.ts | 6 +- .../stakers/getStakerDelegationEvents.ts | 51 + .../routes/stakers/getStakerDepositEvents.ts | 49 + .../stakers/getStakerWithdrawalEvents.ts | 51 + packages/openapi/src/routes/stakers/index.ts | 12 + 22 files changed, 5551 insertions(+), 2129 deletions(-) create mode 100644 packages/openapi/src/apiResponseSchema/events/eventsRespone.ts create mode 100644 packages/openapi/src/apiResponseSchema/events/util.ts create mode 100644 packages/openapi/src/routes/avs/getAvsRegistrationEvents.ts create mode 100644 packages/openapi/src/routes/avs/getAvsRewardsEvents.ts create mode 100644 packages/openapi/src/routes/events/getDelegationEvents.ts create mode 100644 packages/openapi/src/routes/events/getDepositEvents.ts create mode 100644 packages/openapi/src/routes/events/getRegistrationEvents.ts create mode 100644 packages/openapi/src/routes/events/getRewardsEvents.ts create mode 100644 packages/openapi/src/routes/events/getWithdrawalEvents.ts create mode 100644 packages/openapi/src/routes/events/index.ts create mode 100644 packages/openapi/src/routes/operators/getOperatorDelegationEvents.ts create mode 100644 packages/openapi/src/routes/operators/getOperatorRegistrationEvents.ts create mode 100644 packages/openapi/src/routes/stakers/getStakerDelegationEvents.ts create mode 100644 packages/openapi/src/routes/stakers/getStakerDepositEvents.ts create mode 100644 packages/openapi/src/routes/stakers/getStakerWithdrawalEvents.ts diff --git a/packages/api/src/schema/zod/schemas/eventSchemas.ts b/packages/api/src/schema/zod/schemas/eventSchemas.ts index 0b9984a4..c32c454e 100644 --- a/packages/api/src/schema/zod/schemas/eventSchemas.ts +++ b/packages/api/src/schema/zod/schemas/eventSchemas.ts @@ -10,7 +10,7 @@ const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ const yyyymmddRegex = /^\d{4}-\d{2}-\d{2}$/ // Reusable refinement functions -const refineWithEthValueRequiresTokenData = (schema: z.ZodTypeAny) => +export const refineWithEthValueRequiresTokenData = (schema: z.ZodTypeAny) => schema.refine( (data) => { if (data.withEthValue && !data.withTokenData) { @@ -24,7 +24,7 @@ const refineWithEthValueRequiresTokenData = (schema: z.ZodTypeAny) => } ) -const refineStartEndDates = (schema: z.ZodTypeAny) => +export const refineStartEndDates = (schema: z.ZodTypeAny) => schema .refine( (data) => { @@ -55,7 +55,7 @@ const refineStartEndDates = (schema: z.ZodTypeAny) => } ) -const refineDelegationTypeRestrictions = (schema: z.ZodTypeAny) => +export const refineDelegationTypeRestrictions = (schema: z.ZodTypeAny) => schema.refine( (data) => { if (data.type === 'DELEGATION' || data.type === 'UNDELEGATION') { @@ -72,7 +72,7 @@ const refineDelegationTypeRestrictions = (schema: z.ZodTypeAny) => } ) -const refineWithdrawalTypeRestrictions = (schema: z.ZodTypeAny) => +export const refineWithdrawalTypeRestrictions = (schema: z.ZodTypeAny) => schema.refine( (data) => { if (data.type === 'WITHDRAWAL_COMPLETED') { @@ -90,7 +90,7 @@ const refineWithdrawalTypeRestrictions = (schema: z.ZodTypeAny) => ) // Base schema for shared fields -const BaseEventQuerySchema = z.object({ +export const BaseEventQuerySchema = z.object({ txHash: z .string() .regex(/^0x([A-Fa-f0-9]{64})$/, 'Invalid transaction hash') @@ -107,7 +107,6 @@ const BaseEventQuerySchema = z.object({ message: 'Invalid date format for startAt. Use YYYY-MM-DD or ISO 8601 format.' } ) - .default('') .describe('Start date in ISO string format'), endAt: z .string() @@ -120,11 +119,10 @@ const BaseEventQuerySchema = z.object({ message: 'Invalid date format for endAt. Use YYYY-MM-DD or ISO 8601 format.' } ) - .default('') .describe('End date in ISO string format') }) -const WithdrawalEventQuerySchemaBase = BaseEventQuerySchema.extend({ +export const WithdrawalEventQuerySchemaBase = BaseEventQuerySchema.extend({ type: z .enum(['WITHDRAWAL_QUEUED', 'WITHDRAWAL_COMPLETED']) .optional() @@ -152,7 +150,7 @@ export const WithdrawalEventQuerySchema = refineWithdrawalTypeRestrictions( refineWithEthValueRequiresTokenData(refineStartEndDates(WithdrawalEventQuerySchemaBase)) ) -const DepositEventQuerySchemaBase = BaseEventQuerySchema.extend({ +export const DepositEventQuerySchemaBase = BaseEventQuerySchema.extend({ tokenAddress: z .string() .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid token address') @@ -171,9 +169,9 @@ export const DepositEventQuerySchema = refineWithEthValueRequiresTokenData( refineStartEndDates(DepositEventQuerySchemaBase) ) -const DelegationEventQuerySchemaBase = BaseEventQuerySchema.extend({ +export const DelegationEventQuerySchemaBase = BaseEventQuerySchema.extend({ type: z - .enum(['SHARES_INCREASED', 'SHARES_DECREASED', 'DELEGATION', 'UNDELEGATION']) + .enum(['DELEGATION', 'UNDELEGATION', 'SHARES_INCREASED', 'SHARES_DECREASED']) .optional() .describe('The type of the delegation event'), strategyAddress: z @@ -182,42 +180,44 @@ const DelegationEventQuerySchemaBase = BaseEventQuerySchema.extend({ .optional() .describe('The contract address of the restaking strategy') }) - .merge(WithTokenDataQuerySchema) - .merge(WithEthValueQuerySchema) export const DelegationEventQuerySchema = refineDelegationTypeRestrictions( - refineWithEthValueRequiresTokenData(refineStartEndDates(DelegationEventQuerySchemaBase)) -) - -export const OperatorDelegationEventQuerySchema = refineDelegationTypeRestrictions( refineWithEthValueRequiresTokenData( refineStartEndDates( - DelegationEventQuerySchemaBase.extend({ - stakerAddress: z - .string() - .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address') - .optional() - .describe('The address of the staker') - }) + DelegationEventQuerySchemaBase.merge(WithTokenDataQuerySchema).merge(WithEthValueQuerySchema) ) ) ) +export const OperatorDelegationEventQuerySchemaBase = DelegationEventQuerySchemaBase.extend({ + stakerAddress: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address') + .optional() + .describe('The address of the staker') +}) + .merge(WithTokenDataQuerySchema) + .merge(WithEthValueQuerySchema) + +export const OperatorDelegationEventQuerySchema = refineDelegationTypeRestrictions( + refineWithEthValueRequiresTokenData(refineStartEndDates(OperatorDelegationEventQuerySchemaBase)) +) + +export const StakerDelegationEventQuerySchemaBase = DelegationEventQuerySchemaBase.extend({ + operatorAddress: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address') + .optional() + .describe('The address of the operator') +}) + .merge(WithTokenDataQuerySchema) + .merge(WithEthValueQuerySchema) + export const StakerDelegationEventQuerySchema = refineDelegationTypeRestrictions( - refineWithEthValueRequiresTokenData( - refineStartEndDates( - DelegationEventQuerySchemaBase.extend({ - operatorAddress: z - .string() - .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address') - .optional() - .describe('The address of the operator') - }) - ) - ) + refineWithEthValueRequiresTokenData(refineStartEndDates(StakerDelegationEventQuerySchemaBase)) ) -const RewardsEventQuerySchemaBase = BaseEventQuerySchema.extend({ +export const RewardsEventQuerySchemaBase = BaseEventQuerySchema.extend({ rewardsSubmissionHash: z .string() .regex(/^0x([A-Fa-f0-9]{64})$/, 'Invalid reward submission hash') @@ -234,28 +234,32 @@ const RewardsEventQuerySchemaBase = BaseEventQuerySchema.extend({ export const RewardsEventQuerySchema = refineStartEndDates(RewardsEventQuerySchemaBase) -const RegistrationEventQuerySchemaBase = BaseEventQuerySchema.extend({ +export const RegistrationEventQuerySchemaBase = BaseEventQuerySchema.extend({ status: z.enum(['REGISTERED', 'DEREGISTERED']).optional().describe('The status of Registration') }) export const RegistrationEventQuerySchema = refineStartEndDates(RegistrationEventQuerySchemaBase) +export const OperatorRegistrationEventQuerySchemaBase = RegistrationEventQuerySchemaBase.extend({ + avsAddress: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address') + .optional() + .describe('The address of the avs') +}) + export const OperatorRegistrationEventQuerySchema = refineStartEndDates( - RegistrationEventQuerySchemaBase.extend({ - avsAddress: z - .string() - .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address') - .optional() - .describe('The address of the avs') - }) + OperatorRegistrationEventQuerySchemaBase ) +export const AvsRegistrationEventQuerySchemaBase = RegistrationEventQuerySchemaBase.extend({ + operatorAddress: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address') + .optional() + .describe('The address of the operator') +}) + export const AvsRegistrationEventQuerySchema = refineStartEndDates( - RegistrationEventQuerySchemaBase.extend({ - operatorAddress: z - .string() - .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address') - .optional() - .describe('The address of the operator') - }) + AvsRegistrationEventQuerySchemaBase ) diff --git a/packages/openapi/openapi.json b/packages/openapi/openapi.json index ea1b2eef..52a683d1 100644 --- a/packages/openapi/openapi.json +++ b/packages/openapi/openapi.json @@ -4669,77 +4669,95 @@ } } }, - "/operators": { + "/avs/{address}/events/rewards": { "get": { - "operationId": "getAllOperators", - "summary": "Retrieve all operators", - "description": "Returns all operator records. This endpoint supports pagination.", - "tags": ["Operators"], + "operationId": "getAvsRewardsEvents", + "summary": "Retrieve all reward events for a given AVS address", + "description": "Returns a list of all reward events for a given AVS address.", + "tags": ["AVS"], "parameters": [ + { + "in": "path", + "name": "address", + "description": "AVS service manager contract address", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "AVS service manager contract address", + "example": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0" + }, + "required": true + }, { "in": "query", - "name": "withTvl", - "description": "Toggle whether the route should calculate the TVL from shares", + "name": "txHash", + "description": "The transaction hash associated with the event", "schema": { "type": "string", - "enum": ["true", "false"], - "default": "false", - "description": "Toggle whether the route should calculate the TVL from shares", - "example": "false" + "pattern": "^0x([A-Fa-f0-9]{64})$", + "description": "The transaction hash associated with the event" } }, { "in": "query", - "name": "searchByText", - "description": "Case-insensitive search query", + "name": "startAt", + "description": "Start date in ISO string format", "schema": { "type": "string", - "description": "Case-insensitive search query", - "example": "eigen" + "description": "Start date in ISO string format" } }, { "in": "query", - "name": "sortByApy", - "description": "Sort results in asc or desc order of APY", + "name": "endAt", + "description": "End date in ISO string format", "schema": { "type": "string", - "enum": ["asc", "desc"], - "description": "Sort results in asc or desc order of APY", - "example": "desc" + "description": "End date in ISO string format" } }, { "in": "query", - "name": "sortByTvl", - "description": "Sort results in asc or desc order of TVL value", + "name": "rewardsSubmissionHash", + "description": "The reward submission hash associated with the event", "schema": { "type": "string", - "enum": ["asc", "desc"], - "description": "Sort results in asc or desc order of TVL value", - "example": "desc" + "pattern": "^0x([A-Fa-f0-9]{64})$", + "description": "The reward submission hash associated with the event" } }, { "in": "query", - "name": "sortByTotalAvs", - "description": "Sort results in asc or desc order of total AVS (only valid for Operator queries)", + "name": "rewardsSubmissionToken", + "description": "The token address used for the rewards submission", "schema": { "type": "string", - "enum": ["asc", "desc"], - "description": "Sort results in asc or desc order of total AVS (only valid for Operator queries)", - "example": "desc" + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The token address used for the rewards submission" } }, { "in": "query", - "name": "sortByTotalStakers", - "description": "Sort results in asc or desc order of total stakers", + "name": "withIndividualAmount", + "description": "Toggle whether the route should return individual share amount for each strategy", "schema": { "type": "string", - "enum": ["asc", "desc"], - "description": "Sort results in asc or desc order of total stakers", - "example": "desc" + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should return individual share amount for each strategy", + "example": "false" + } + }, + { + "in": "query", + "name": "withEthValue", + "description": "Toggle whether the route should return value denominated in ETH", + "schema": { + "type": "string", + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should return value denominated in ETH", + "example": "false" } }, { @@ -4767,284 +4785,114 @@ ], "responses": { "200": { - "description": "The list of operator records.", + "description": "The reward events found for the AVS.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "properties": { - "address": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the AVS operator", - "example": "0x09e6eb09213bdd3698bd8afb43ec3cb0ecff683a" - }, - "metadataName": { - "type": "string", - "description": "The name of the AVS operator", - "example": "Example AVS Operator" - }, - "metadataDescription": { - "type": "string", - "nullable": true, - "description": "The description of the AVS operator", - "example": "This is an example AVS operator" - }, - "metadataDiscord": { - "type": "string", - "nullable": true, - "format": "uri", - "description": "The URL of the AVS operator's Discord server", - "example": "https://discord.com/invite/abcdefghij" - }, - "metadataLogo": { - "type": "string", - "nullable": true, - "format": "uri", - "description": "The URL of the AVS operator's logo" - }, - "metadataTelegram": { - "type": "string", - "nullable": true, - "format": "uri", - "description": "The URL of the AVS operator's Telegram channel", - "example": "https://t.me/acme" - }, - "metadataWebsite": { - "type": "string", - "nullable": true, - "format": "uri", - "description": "The URL of the AVS operator's website", - "example": "https://acme.com" - }, - "metadataX": { - "type": "string", - "nullable": true, - "format": "uri", - "description": "The URL of the AVS operator's X", - "example": "https://twitter.com/acme" - }, - "totalStakers": { - "type": "number", - "description": "The total number of stakers who have delegated to this AVS operator", - "example": 10 - }, - "totalAvs": { - "type": "number", - "description": "The total number of AVS opted by the AVS operator", - "example": 10 - }, - "apy": { - "type": "string", - "description": "The latest APY recorded for the operator", - "example": "1.0" - }, - "createdAtBlock": { - "type": "string", - "description": "The block number at which the AVS Operator was registered", - "example": "19631203" - }, - "updatedAtBlock": { - "type": "string", - "description": "The block number at which the AVS Operator registration was last updated", - "example": "19631203" - }, - "createdAt": { - "type": "string", - "description": "The time stamp at which the AVS Operator was registered", - "example": "2024-04-11T08:31:11.000Z" - }, - "updatedAt": { - "type": "string", - "description": "The time stamp at which the AVS Operator registration was last updated", - "example": "2024-04-11T08:31:11.000Z" - }, - "shares": { - "type": "array", - "items": { - "type": "object", - "properties": { - "strategyAddress": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the restaking strategy", - "example": "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" - }, - "shares": { - "type": "string", - "description": "The amount of shares held in the strategy", - "example": "1277920000000000000000000" - } - }, - "required": ["strategyAddress", "shares"] - }, - "description": "The strategy shares held in the AVS operator", - "example": [ - { - "strategyAddress": "0x93c4b944d05dfe6df7645a86cd2206016c51564d", - "shares": "135064894598947935263152" + "tx": { + "type": "string", + "description": "The transaction hash", + "example": "0xae41958d0342a4485536f701c72723625131680f182eb21f95abdac6d74d0ff0" + }, + "blockNumber": { + "type": "number", + "description": "The block number of the transaction", + "example": 21045085 + }, + "blockTime": { + "type": "string", + "description": "The block time of the transaction as an ISO 8601 string", + "example": "2024-10-25T20:41:11.000Z" + }, + "type": { + "type": "string", + "enum": ["REWARDS"], + "description": "The type of the event", + "example": "REWARDS" + }, + "args": { + "type": "object", + "properties": { + "submissionNonce": { + "type": "number", + "description": "The nonce of the rewards submission", + "example": 2 + }, + "rewardsSubmissionHash": { + "type": "string", + "description": "The hash of the rewards submission", + "example": "0x1e391c015c923972811a27e1c6c3a874511e47033f1022021f29967a60ab2c87" + }, + "rewardsSubmissionToken": { + "type": "string", + "description": "The contract address of the token used for rewards distribution", + "example": "0xba50933c268f567bdc86e1ac131be072c6b0b71a" + }, + "rewardsSubmissionAmount": { + "type": "string", + "description": "The total amount of rewards allocated in this submission", + "example": "49000000000000000000000" + }, + "rewardsSubmissionStartTimeStamp": { + "type": "number", + "description": "The timestamp marking the start of this rewards distribution period", + "example": 1728518400 + }, + "rewardsSubmissionDuration": { + "type": "number", + "description": "The duration (in seconds) over which the rewards are distributed", + "example": 6048000 + }, + "strategies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "strategy": { + "type": "string", + "description": "The contract address of the restaking strategy", + "example": "0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6" }, - { - "strategyAddress": "0x54945180db7943c0ed0fee7edab2bd24620256bc", - "shares": "9323641881708650182301" - } - ] - }, - "avsRegistrations": { - "type": "array", - "items": { - "type": "object", - "properties": { - "avsAddress": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "AVS service manager contract address", - "example": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0" - }, - "isActive": { - "type": "boolean", - "description": "True indicates operator is an active participant while False indicates it used to be one but not anymore", - "example": false - } + "multiplier": { + "type": "string", + "description": "The multiplier associated with this strategy", + "example": "1068966896363604679" }, - "required": ["avsAddress", "isActive"] - }, - "description": "Operator AVS registrations and their participation status", - "example": [ - { - "avsAddress": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0", - "isActive": true + "amount": { + "type": "string", + "description": "The amount of rewards allocated to this strategy from the total rewards in this submissionn", + "example": "3.7932452554246293e+21" }, - { - "avsAddress": "0xe8e59c6c8b56f2c178f63bcfc4ce5e5e2359c8fc", - "isActive": false + "amountEthValue": { + "type": "number", + "description": "The value of the rewards amount allocated to this strategy in ETH", + "example": 0.0638779707245759 } - ] + }, + "required": ["strategy", "multiplier"] }, - "tvl": { - "type": "object", - "properties": { - "tvl": { - "type": "number", - "description": "The combined TVL of all restaking strategies in ETH", - "example": 1000000 - }, - "tvlBeaconChain": { - "type": "number", - "description": "The TVL of Beacon Chain restaking strategy in ETH", - "example": 1000000 - }, - "tvlRestaking": { - "type": "number", - "description": "The combined TVL of all liquid restaking strategies in ETH", - "example": 1000000 - }, - "tvlWETH": { - "type": "number", - "description": "The TVL of WETH restaking strategy in ETH", - "example": 1000000 - }, - "tvlStrategies": { - "type": "object", - "additionalProperties": { - "type": "number", - "description": "The total value locked (TVL) in the strategy, denominated in the strategy's native token", - "example": 1000000 - }, - "description": "The TVL of each individual restaking strategy in its native token", - "example": { - "Eigen": 1000000, - "cbETH": 2000000 - } - }, - "tvlStrategiesEth": { - "type": "object", - "additionalProperties": { - "type": "number", - "description": "The total value locked (TVL) in the strategy, denominated in ETH", - "example": 1000000 - }, - "description": "The TVL of each individual restaking strategy in ETH", - "example": { - "Eigen": 1000000, - "cbETH": 2000000 - } - } - }, - "required": [ - "tvl", - "tvlBeaconChain", - "tvlRestaking", - "tvlWETH", - "tvlStrategies", - "tvlStrategiesEth" - ], - "description": "The total value locked (TVL) in the AVS operator", - "example": { - "tvl": 1000000, - "tvlBeaconChain": 1000000, - "tvlWETH": 1000000, - "tvlRestaking": 1000000, - "tvlStrategies": { - "Eigen": 1000000, - "cbETH": 2000000 - }, - "tvlStrategiesEth": { - "stETH": 1000000, - "cbETH": 2000000 - } - } - } - }, - "required": [ - "address", - "metadataName", - "metadataDescription", - "metadataDiscord", - "metadataLogo", - "metadataTelegram", - "metadataWebsite", - "metadataX", - "totalStakers", - "totalAvs", - "apy", - "createdAtBlock", - "updatedAtBlock", - "createdAt", - "updatedAt", - "shares", - "avsRegistrations" - ] - } - }, - "meta": { - "type": "object", - "properties": { - "total": { - "type": "number", - "description": "Total number of records in the database", - "example": 30 - }, - "skip": { - "type": "number", - "description": "The number of skiped records for this query", - "example": 0 - }, - "take": { - "type": "number", - "description": "The number of records returned for this query", - "example": 12 + "description": "List of strategies involved in the rewards submission" } }, - "required": ["total", "skip", "take"] + "required": [ + "submissionNonce", + "rewardsSubmissionHash", + "rewardsSubmissionToken", + "rewardsSubmissionAmount", + "rewardsSubmissionStartTimeStamp", + "rewardsSubmissionDuration", + "strategies" + ] + }, + "ethValue": { + "type": "number", + "description": "The value of the shares in ETH", + "example": 1 } }, - "required": ["data", "meta"] + "required": ["tx", "blockNumber", "blockTime", "type", "args"] } } } @@ -5073,33 +4921,71 @@ } } }, - "/operators/addresses": { + "/avs/{address}/events/registration-status": { "get": { - "operationId": "getAllOperatorAddresses", - "summary": "Retrieve all operator addresses", - "description": "Returns a list of all operator addresses. This page supports pagination.", - "tags": ["Operators"], + "operationId": "getAvsRegistrationEvents", + "summary": "Retrieve all registration events for a given AVS address", + "description": "Returns a list of all registration events for a given AVS address.", + "tags": ["AVS"], "parameters": [ + { + "in": "path", + "name": "address", + "description": "AVS service manager contract address", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "AVS service manager contract address", + "example": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0" + }, + "required": true + }, { "in": "query", - "name": "searchMode", - "description": "Search mode", + "name": "txHash", + "description": "The transaction hash associated with the event", "schema": { "type": "string", - "enum": ["contains", "startsWith"], - "default": "contains", - "description": "Search mode", - "example": "contains" + "pattern": "^0x([A-Fa-f0-9]{64})$", + "description": "The transaction hash associated with the event" } }, { "in": "query", - "name": "searchByText", - "description": "Case-insensitive search query", + "name": "startAt", + "description": "Start date in ISO string format", "schema": { "type": "string", - "description": "Case-insensitive search query", - "example": "eigen" + "description": "Start date in ISO string format" + } + }, + { + "in": "query", + "name": "endAt", + "description": "End date in ISO string format", + "schema": { + "type": "string", + "description": "End date in ISO string format" + } + }, + { + "in": "query", + "name": "status", + "description": "The status of Registration", + "schema": { + "type": "string", + "enum": ["REGISTERED", "DEREGISTERED"], + "description": "The status of Registration" + } + }, + { + "in": "query", + "name": "operatorAddress", + "description": "The address of the operator", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the operator" } }, { @@ -5127,60 +5013,52 @@ ], "responses": { "200": { - "description": "The list of operator addresses.", + "description": "The registration events found for the AVS.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "properties": { - "address": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the AVS operator", - "example": "0x0000039b2f2ac9e3492a0f805ec7aea9eaee0c25" - }, - "name": { - "type": "string", - "description": "The Operator's name", - "example": "Example Operator" - }, - "logo": { - "type": "string", - "description": "The Operator's logo URL", - "example": "https://example.operator/logo.png" - } - }, - "required": ["address", "name", "logo"] - } + "tx": { + "type": "string", + "description": "The transaction hash", + "example": "0xae41958d0342a4485536f701c72723625131680f182eb21f95abdac6d74d0ff0" }, - "meta": { + "blockNumber": { + "type": "number", + "description": "The block number of the transaction", + "example": 21045085 + }, + "blockTime": { + "type": "string", + "description": "The block time of the transaction as an ISO 8601 string", + "example": "2024-10-25T20:41:11.000Z" + }, + "type": { + "type": "string", + "enum": ["REGISTRATION_STATUS"], + "description": "The type of the event", + "example": "REGISTRATION_STATUS" + }, + "args": { "type": "object", "properties": { - "total": { - "type": "number", - "description": "Total number of records in the database", - "example": 30 - }, - "skip": { - "type": "number", - "description": "The number of skiped records for this query", - "example": 0 + "operator": { + "type": "string", + "description": "The contract address of the AVS operator", + "example": "0x9abce41e1486210ad83deb831afcdd214af5b49d" }, - "take": { - "type": "number", - "description": "The number of records returned for this query", - "example": 12 + "status": { + "type": "string", + "enum": ["REGISTERED", "DEREGISTERED"], + "description": "The status of the registration", + "example": "REGISTERED" } }, - "required": ["total", "skip", "take"] + "required": ["operator", "status"] } }, - "required": ["data", "meta"] + "required": ["tx", "blockNumber", "blockTime", "type", "args"] } } } @@ -5209,25 +5087,13 @@ } } }, - "/operators/{address}": { + "/operators": { "get": { - "operationId": "getOperatorByAddress", - "summary": "Retrieve an operator by address", - "description": "Returns an operator record by address.", + "operationId": "getAllOperators", + "summary": "Retrieve all operators", + "description": "Returns all operator records. This endpoint supports pagination.", "tags": ["Operators"], "parameters": [ - { - "in": "path", - "name": "address", - "description": "The address of the operator", - "schema": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The address of the operator", - "example": "0x00107cfdeaddc0a3160ed2f6fedd627f313e7b1a" - }, - "required": true - }, { "in": "query", "name": "withTvl", @@ -5242,453 +5108,212 @@ }, { "in": "query", - "name": "withAvsData", - "description": "Toggle whether to return additional data for each AVS registration for a given Operator", + "name": "searchByText", + "description": "Case-insensitive search query", "schema": { "type": "string", - "enum": ["true", "false"], - "default": "false", - "description": "Toggle whether to return additional data for each AVS registration for a given Operator", - "example": "false" + "description": "Case-insensitive search query", + "example": "eigen" } }, { "in": "query", - "name": "withRewards", - "description": "Toggle whether the route should return Avs/Operator rewards APY data", + "name": "sortByApy", + "description": "Sort results in asc or desc order of APY", "schema": { "type": "string", - "enum": ["true", "false"], - "default": "false", - "description": "Toggle whether the route should return Avs/Operator rewards APY data", - "example": "false" + "enum": ["asc", "desc"], + "description": "Sort results in asc or desc order of APY", + "example": "desc" } - } + }, + { + "in": "query", + "name": "sortByTvl", + "description": "Sort results in asc or desc order of TVL value", + "schema": { + "type": "string", + "enum": ["asc", "desc"], + "description": "Sort results in asc or desc order of TVL value", + "example": "desc" + } + }, + { + "in": "query", + "name": "sortByTotalAvs", + "description": "Sort results in asc or desc order of total AVS (only valid for Operator queries)", + "schema": { + "type": "string", + "enum": ["asc", "desc"], + "description": "Sort results in asc or desc order of total AVS (only valid for Operator queries)", + "example": "desc" + } + }, + { + "in": "query", + "name": "sortByTotalStakers", + "description": "Sort results in asc or desc order of total stakers", + "schema": { + "type": "string", + "enum": ["asc", "desc"], + "description": "Sort results in asc or desc order of total stakers", + "example": "desc" + } + }, + { + "in": "query", + "name": "skip", + "description": "The number of records to skip for pagination", + "schema": { + "type": "string", + "default": "0", + "description": "The number of records to skip for pagination", + "example": 0 + } + }, + { + "in": "query", + "name": "take", + "description": "The number of records to return for pagination", + "schema": { + "type": "string", + "default": "12", + "description": "The number of records to return for pagination", + "example": 12 + } + } ], "responses": { "200": { - "description": "The record of the requested operator.", + "description": "The list of operator records.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "address": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the AVS operator", - "example": "0x09e6eb09213bdd3698bd8afb43ec3cb0ecff683a" - }, - "metadataName": { - "type": "string", - "description": "The name of the AVS operator", - "example": "Example AVS Operator" - }, - "metadataDescription": { - "type": "string", - "nullable": true, - "description": "The description of the AVS operator", - "example": "This is an example AVS operator" - }, - "metadataDiscord": { - "type": "string", - "nullable": true, - "format": "uri", - "description": "The URL of the AVS operator's Discord server", - "example": "https://discord.com/invite/abcdefghij" - }, - "metadataLogo": { - "type": "string", - "nullable": true, - "format": "uri", - "description": "The URL of the AVS operator's logo" - }, - "metadataTelegram": { - "type": "string", - "nullable": true, - "format": "uri", - "description": "The URL of the AVS operator's Telegram channel", - "example": "https://t.me/acme" - }, - "metadataWebsite": { - "type": "string", - "nullable": true, - "format": "uri", - "description": "The URL of the AVS operator's website", - "example": "https://acme.com" - }, - "metadataX": { - "type": "string", - "nullable": true, - "format": "uri", - "description": "The URL of the AVS operator's X", - "example": "https://twitter.com/acme" - }, - "totalStakers": { - "type": "number", - "description": "The total number of stakers who have delegated to this AVS operator", - "example": 10 - }, - "totalAvs": { - "type": "number", - "description": "The total number of AVS opted by the AVS operator", - "example": 10 - }, - "apy": { - "type": "string", - "description": "The latest APY recorded for the operator", - "example": "1.0" - }, - "createdAtBlock": { - "type": "string", - "description": "The block number at which the AVS Operator was registered", - "example": "19631203" - }, - "updatedAtBlock": { - "type": "string", - "description": "The block number at which the AVS Operator registration was last updated", - "example": "19631203" - }, - "createdAt": { - "type": "string", - "description": "The time stamp at which the AVS Operator was registered", - "example": "2024-04-11T08:31:11.000Z" - }, - "updatedAt": { - "type": "string", - "description": "The time stamp at which the AVS Operator registration was last updated", - "example": "2024-04-11T08:31:11.000Z" - }, - "shares": { - "type": "array", - "items": { - "type": "object", - "properties": { - "strategyAddress": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the restaking strategy", - "example": "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" - }, - "shares": { - "type": "string", - "description": "The amount of shares held in the strategy", - "example": "1277920000000000000000000" - } - }, - "required": ["strategyAddress", "shares"] - }, - "description": "The strategy shares held in the AVS operator", - "example": [ - { - "strategyAddress": "0x93c4b944d05dfe6df7645a86cd2206016c51564d", - "shares": "135064894598947935263152" - }, - { - "strategyAddress": "0x54945180db7943c0ed0fee7edab2bd24620256bc", - "shares": "9323641881708650182301" - } - ] - }, - "avsRegistrations": { + "data": { "type": "array", "items": { "type": "object", "properties": { - "avsAddress": { + "address": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The address of the AVS contract", - "example": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0" - }, - "isActive": { - "type": "boolean", - "description": "True indicates operator is an active participant while False indicates it used to be one but not anymore", - "example": false + "description": "The contract address of the AVS operator", + "example": "0x09e6eb09213bdd3698bd8afb43ec3cb0ecff683a" }, "metadataName": { "type": "string", - "description": "The name of the AVS", - "example": "Example AVS" + "description": "The name of the AVS operator", + "example": "Example AVS Operator" }, "metadataDescription": { "type": "string", "nullable": true, - "description": "The description of the AVS", - "example": "This is an example AVS" + "description": "The description of the AVS operator", + "example": "This is an example AVS operator" }, "metadataDiscord": { "type": "string", "nullable": true, "format": "uri", - "description": "The URL of the AVS's Discord server", + "description": "The URL of the AVS operator's Discord server", "example": "https://discord.com/invite/abcdefghij" }, "metadataLogo": { "type": "string", "nullable": true, "format": "uri", - "description": "The URL of the AVS's logo" + "description": "The URL of the AVS operator's logo" }, "metadataTelegram": { "type": "string", "nullable": true, "format": "uri", - "description": "The URL of the AVS's Telegram channel", + "description": "The URL of the AVS operator's Telegram channel", "example": "https://t.me/acme" }, "metadataWebsite": { "type": "string", "nullable": true, "format": "uri", - "description": "The URL of the AVS's website", + "description": "The URL of the AVS operator's website", "example": "https://acme.com" }, "metadataX": { "type": "string", "nullable": true, "format": "uri", - "description": "The URL of the AVS's X", + "description": "The URL of the AVS operator's X", "example": "https://twitter.com/acme" }, - "metadataUrl": { + "totalStakers": { + "type": "number", + "description": "The total number of stakers who have delegated to this AVS operator", + "example": 10 + }, + "totalAvs": { + "type": "number", + "description": "The total number of AVS opted by the AVS operator", + "example": 10 + }, + "apy": { "type": "string", - "description": "URL for AVS metadata", - "example": "https://example.json" - }, - "curatedMetadata": { - "type": "object", - "properties": { - "metadataName": { - "type": "string", - "description": "The name of the AVS", - "example": "Example AVS" - }, - "metadataDescription": { - "type": "string", - "nullable": true, - "description": "The description of the AVS", - "example": "This is an example AVS" - }, - "metadataDiscord": { - "type": "string", - "nullable": true, - "format": "uri", - "description": "The URL of the AVS's Discord server", - "example": "https://discord.com/invite/abcdefghij" - }, - "metadataLogo": { - "type": "string", - "nullable": true, - "format": "uri", - "description": "The URL of the AVS's logo" - }, - "metadataTelegram": { - "type": "string", - "nullable": true, - "format": "uri", - "description": "The URL of the AVS's Telegram channel", - "example": "https://t.me/acme" - }, - "metadataWebsite": { - "type": "string", - "nullable": true, - "format": "uri", - "description": "The URL of the AVS's website", - "example": "https://acme.com" - }, - "metadataX": { - "type": "string", - "nullable": true, - "format": "uri", - "description": "The URL of the AVS's X", - "example": "https://twitter.com/acme" - }, - "metadataGithub": { - "type": "string", - "nullable": true, - "format": "uri", - "description": "The URL of the AVS's Github", - "example": "https://github.com/acme" - }, - "metadataTokenAddress": { - "type": "string", - "nullable": true, - "format": "uri", - "description": "The Token Address of the AVS", - "example": "0x2344c0fe02ccd2b32155ca0ffcb1978a6d96a552" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Tags to describe the AVS", - "example": ["Example tag 1", "Example tag 2"] - }, - "isVisible": { - "type": "boolean", - "description": "Indicates if AVS visibility is allowed", - "example": false - }, - "isVerified": { - "type": "boolean", - "description": "Indicates if the AVS has been verified by the EigenExplorer team", - "example": false - } - }, - "required": [ - "metadataName", - "metadataDescription", - "metadataDiscord", - "metadataLogo", - "metadataTelegram", - "metadataWebsite", - "metadataX", - "metadataGithub", - "metadataTokenAddress", - "tags", - "isVisible", - "isVerified" - ], - "description": "Curated metadata information for AVS" - }, - "restakeableStrategies": { - "type": "array", - "items": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$" - }, - "description": "The list of restakeable strategies for AVS" - }, - "totalStakers": { - "type": "number", - "description": "Total number of stakers", - "example": 80000 - }, - "totalOperators": { - "type": "number", - "description": "Total number of operators", - "example": 200 - }, - "tvlEth": { - "type": "string", - "description": "Total TVL in ETH", - "example": "3000000" + "description": "The latest APY recorded for the operator", + "example": "1.0" }, "createdAtBlock": { - "type": "number", - "description": "The block number at which the AVS was created", - "example": 19592323 + "type": "string", + "description": "The block number at which the AVS Operator was registered", + "example": "19631203" }, "updatedAtBlock": { - "type": "number", - "description": "The block number at which the AVS was last updated", - "example": 19592323 + "type": "string", + "description": "The block number at which the AVS Operator registration was last updated", + "example": "19631203" }, "createdAt": { "type": "string", - "description": "The time stamp at which the AVS was created", - "example": "2024-04-05T21:49:59.000Z" + "description": "The time stamp at which the AVS Operator was registered", + "example": "2024-04-11T08:31:11.000Z" }, "updatedAt": { "type": "string", - "description": "The time stamp at which the AVS was last updated", - "example": "2024-04-05T21:49:59.000Z" - }, - "address": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "AVS service manager contract address", - "example": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0" + "description": "The time stamp at which the AVS Operator registration was last updated", + "example": "2024-04-11T08:31:11.000Z" }, - "rewardsSubmissions": { + "shares": { "type": "array", "items": { "type": "object", "properties": { - "id": { - "type": "number", - "description": "Id for the rewards submission", - "example": 1 - }, - "submissionNonce": { - "type": "number", - "description": "The nonce of the rewards submission", - "example": 0 - }, - "rewardsSubmissionHash": { - "type": "string", - "description": "The hash of the rewards submission", - "example": "0x2bc2f7cef0974f7064dbdae054f9a0e5ea1c2293d180a749c70100506382d85" - }, - "avsAddress": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "AVS address for the rewards submission", - "example": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0" - }, "strategyAddress": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "Strategy address for the rewards submission", - "example": "0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6" - }, - "multiplier": { - "type": "string", - "description": "The multiplier associated with this strategy", - "example": "1000000000000000000" - }, - "token": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the token used for rewards distribution", - "example": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - }, - "amount": { - "type": "string", - "description": "The amount of rewards allocated to this strategy from the total rewards", - "example": "300000000000000000" - }, - "startTimestamp": { - "type": "number", - "description": "The timestamp marking the start of this rewards distribution period", - "example": 1720000000 - }, - "duration": { - "type": "number", - "description": "The duration (in seconds) over which the rewards are distributed", - "example": 2500000 - }, - "createdAtBlock": { - "type": "number", - "description": "The block number at which the reward submission was recorded", - "example": 20495824 + "description": "The contract address of the restaking strategy", + "example": "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" }, - "createdAt": { + "shares": { "type": "string", - "description": "The timestamp at which the reward submission was recorded", - "example": "2024-08-10T04:28:47.000Z" + "description": "The amount of shares held in the strategy", + "example": "1277920000000000000000000" } }, - "required": [ - "id", - "submissionNonce", - "rewardsSubmissionHash", - "avsAddress", - "strategyAddress", - "multiplier", - "token", - "amount", - "startTimestamp", - "duration", - "createdAtBlock", - "createdAt" - ] + "required": ["strategyAddress", "shares"] }, - "description": "List of rewards submissions associated with AVS" + "description": "The strategy shares held in the AVS operator", + "example": [ + { + "strategyAddress": "0x93c4b944d05dfe6df7645a86cd2206016c51564d", + "shares": "135064894598947935263152" + }, + { + "strategyAddress": "0x54945180db7943c0ed0fee7edab2bd24620256bc", + "shares": "9323641881708650182301" + } + ] }, - "operators": { + "avsRegistrations": { "type": "array", "items": { "type": "object", @@ -5699,60 +5324,853 @@ "description": "AVS service manager contract address", "example": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0" }, - "operatorAddress": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the AVS operator", - "example": "0x4cd2086e1d708e65db5d4f5712a9ca46ed4bbd0a" - }, "isActive": { "type": "boolean", "description": "True indicates operator is an active participant while False indicates it used to be one but not anymore", - "example": true - }, - "restakedStrategies": { - "type": "array", - "items": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$" - }, - "description": "List of strategies restaked by the operator", - "example": [ - "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0", - "0x93c4b944d05dfe6df7645a86cd2206016c51564d" - ] - }, - "createdAtBlock": { - "type": "number", - "description": "The block number at which the AVS Operator was registered", - "example": 19614553 - }, - "updatedAtBlock": { - "type": "number", - "description": "The block number at which the AVS Operator registration was last updated", - "example": 19614553 - }, - "createdAt": { - "type": "string", - "description": "The time stamp at which the AVS Operator was registered", - "example": "2024-04-09T00:35:35.000Z" - }, - "updatedAt": { - "type": "string", - "description": "The time stamp at which the AVS Operator registration was last updated", - "example": "2024-04-09T00:35:35.000Z" - }, - "operator": { - "type": "object", - "properties": { - "address": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the AVS operator", - "example": "0x09e6eb09213bdd3698bd8afb43ec3cb0ecff683a" - }, - "metadataUrl": { - "type": "string", + "example": false + } + }, + "required": ["avsAddress", "isActive"] + }, + "description": "Operator AVS registrations and their participation status", + "example": [ + { + "avsAddress": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0", + "isActive": true + }, + { + "avsAddress": "0xe8e59c6c8b56f2c178f63bcfc4ce5e5e2359c8fc", + "isActive": false + } + ] + }, + "tvl": { + "type": "object", + "properties": { + "tvl": { + "type": "number", + "description": "The combined TVL of all restaking strategies in ETH", + "example": 1000000 + }, + "tvlBeaconChain": { + "type": "number", + "description": "The TVL of Beacon Chain restaking strategy in ETH", + "example": 1000000 + }, + "tvlRestaking": { + "type": "number", + "description": "The combined TVL of all liquid restaking strategies in ETH", + "example": 1000000 + }, + "tvlWETH": { + "type": "number", + "description": "The TVL of WETH restaking strategy in ETH", + "example": 1000000 + }, + "tvlStrategies": { + "type": "object", + "additionalProperties": { + "type": "number", + "description": "The total value locked (TVL) in the strategy, denominated in the strategy's native token", + "example": 1000000 + }, + "description": "The TVL of each individual restaking strategy in its native token", + "example": { + "Eigen": 1000000, + "cbETH": 2000000 + } + }, + "tvlStrategiesEth": { + "type": "object", + "additionalProperties": { + "type": "number", + "description": "The total value locked (TVL) in the strategy, denominated in ETH", + "example": 1000000 + }, + "description": "The TVL of each individual restaking strategy in ETH", + "example": { + "Eigen": 1000000, + "cbETH": 2000000 + } + } + }, + "required": [ + "tvl", + "tvlBeaconChain", + "tvlRestaking", + "tvlWETH", + "tvlStrategies", + "tvlStrategiesEth" + ], + "description": "The total value locked (TVL) in the AVS operator", + "example": { + "tvl": 1000000, + "tvlBeaconChain": 1000000, + "tvlWETH": 1000000, + "tvlRestaking": 1000000, + "tvlStrategies": { + "Eigen": 1000000, + "cbETH": 2000000 + }, + "tvlStrategiesEth": { + "stETH": 1000000, + "cbETH": 2000000 + } + } + } + }, + "required": [ + "address", + "metadataName", + "metadataDescription", + "metadataDiscord", + "metadataLogo", + "metadataTelegram", + "metadataWebsite", + "metadataX", + "totalStakers", + "totalAvs", + "apy", + "createdAtBlock", + "updatedAtBlock", + "createdAt", + "updatedAt", + "shares", + "avsRegistrations" + ] + } + }, + "meta": { + "type": "object", + "properties": { + "total": { + "type": "number", + "description": "Total number of records in the database", + "example": 30 + }, + "skip": { + "type": "number", + "description": "The number of skiped records for this query", + "example": 0 + }, + "take": { + "type": "number", + "description": "The number of records returned for this query", + "example": 12 + } + }, + "required": ["total", "skip", "take"] + } + }, + "required": ["data", "meta"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, + "/operators/addresses": { + "get": { + "operationId": "getAllOperatorAddresses", + "summary": "Retrieve all operator addresses", + "description": "Returns a list of all operator addresses. This page supports pagination.", + "tags": ["Operators"], + "parameters": [ + { + "in": "query", + "name": "searchMode", + "description": "Search mode", + "schema": { + "type": "string", + "enum": ["contains", "startsWith"], + "default": "contains", + "description": "Search mode", + "example": "contains" + } + }, + { + "in": "query", + "name": "searchByText", + "description": "Case-insensitive search query", + "schema": { + "type": "string", + "description": "Case-insensitive search query", + "example": "eigen" + } + }, + { + "in": "query", + "name": "skip", + "description": "The number of records to skip for pagination", + "schema": { + "type": "string", + "default": "0", + "description": "The number of records to skip for pagination", + "example": 0 + } + }, + { + "in": "query", + "name": "take", + "description": "The number of records to return for pagination", + "schema": { + "type": "string", + "default": "12", + "description": "The number of records to return for pagination", + "example": 12 + } + } + ], + "responses": { + "200": { + "description": "The list of operator addresses.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the AVS operator", + "example": "0x0000039b2f2ac9e3492a0f805ec7aea9eaee0c25" + }, + "name": { + "type": "string", + "description": "The Operator's name", + "example": "Example Operator" + }, + "logo": { + "type": "string", + "description": "The Operator's logo URL", + "example": "https://example.operator/logo.png" + } + }, + "required": ["address", "name", "logo"] + } + }, + "meta": { + "type": "object", + "properties": { + "total": { + "type": "number", + "description": "Total number of records in the database", + "example": 30 + }, + "skip": { + "type": "number", + "description": "The number of skiped records for this query", + "example": 0 + }, + "take": { + "type": "number", + "description": "The number of records returned for this query", + "example": 12 + } + }, + "required": ["total", "skip", "take"] + } + }, + "required": ["data", "meta"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, + "/operators/{address}": { + "get": { + "operationId": "getOperatorByAddress", + "summary": "Retrieve an operator by address", + "description": "Returns an operator record by address.", + "tags": ["Operators"], + "parameters": [ + { + "in": "path", + "name": "address", + "description": "The address of the operator", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the operator", + "example": "0x00107cfdeaddc0a3160ed2f6fedd627f313e7b1a" + }, + "required": true + }, + { + "in": "query", + "name": "withTvl", + "description": "Toggle whether the route should calculate the TVL from shares", + "schema": { + "type": "string", + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should calculate the TVL from shares", + "example": "false" + } + }, + { + "in": "query", + "name": "withAvsData", + "description": "Toggle whether to return additional data for each AVS registration for a given Operator", + "schema": { + "type": "string", + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether to return additional data for each AVS registration for a given Operator", + "example": "false" + } + }, + { + "in": "query", + "name": "withRewards", + "description": "Toggle whether the route should return Avs/Operator rewards APY data", + "schema": { + "type": "string", + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should return Avs/Operator rewards APY data", + "example": "false" + } + } + ], + "responses": { + "200": { + "description": "The record of the requested operator.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the AVS operator", + "example": "0x09e6eb09213bdd3698bd8afb43ec3cb0ecff683a" + }, + "metadataName": { + "type": "string", + "description": "The name of the AVS operator", + "example": "Example AVS Operator" + }, + "metadataDescription": { + "type": "string", + "nullable": true, + "description": "The description of the AVS operator", + "example": "This is an example AVS operator" + }, + "metadataDiscord": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS operator's Discord server", + "example": "https://discord.com/invite/abcdefghij" + }, + "metadataLogo": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS operator's logo" + }, + "metadataTelegram": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS operator's Telegram channel", + "example": "https://t.me/acme" + }, + "metadataWebsite": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS operator's website", + "example": "https://acme.com" + }, + "metadataX": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS operator's X", + "example": "https://twitter.com/acme" + }, + "totalStakers": { + "type": "number", + "description": "The total number of stakers who have delegated to this AVS operator", + "example": 10 + }, + "totalAvs": { + "type": "number", + "description": "The total number of AVS opted by the AVS operator", + "example": 10 + }, + "apy": { + "type": "string", + "description": "The latest APY recorded for the operator", + "example": "1.0" + }, + "createdAtBlock": { + "type": "string", + "description": "The block number at which the AVS Operator was registered", + "example": "19631203" + }, + "updatedAtBlock": { + "type": "string", + "description": "The block number at which the AVS Operator registration was last updated", + "example": "19631203" + }, + "createdAt": { + "type": "string", + "description": "The time stamp at which the AVS Operator was registered", + "example": "2024-04-11T08:31:11.000Z" + }, + "updatedAt": { + "type": "string", + "description": "The time stamp at which the AVS Operator registration was last updated", + "example": "2024-04-11T08:31:11.000Z" + }, + "shares": { + "type": "array", + "items": { + "type": "object", + "properties": { + "strategyAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the restaking strategy", + "example": "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" + }, + "shares": { + "type": "string", + "description": "The amount of shares held in the strategy", + "example": "1277920000000000000000000" + } + }, + "required": ["strategyAddress", "shares"] + }, + "description": "The strategy shares held in the AVS operator", + "example": [ + { + "strategyAddress": "0x93c4b944d05dfe6df7645a86cd2206016c51564d", + "shares": "135064894598947935263152" + }, + { + "strategyAddress": "0x54945180db7943c0ed0fee7edab2bd24620256bc", + "shares": "9323641881708650182301" + } + ] + }, + "avsRegistrations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "avsAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the AVS contract", + "example": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0" + }, + "isActive": { + "type": "boolean", + "description": "True indicates operator is an active participant while False indicates it used to be one but not anymore", + "example": false + }, + "metadataName": { + "type": "string", + "description": "The name of the AVS", + "example": "Example AVS" + }, + "metadataDescription": { + "type": "string", + "nullable": true, + "description": "The description of the AVS", + "example": "This is an example AVS" + }, + "metadataDiscord": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's Discord server", + "example": "https://discord.com/invite/abcdefghij" + }, + "metadataLogo": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's logo" + }, + "metadataTelegram": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's Telegram channel", + "example": "https://t.me/acme" + }, + "metadataWebsite": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's website", + "example": "https://acme.com" + }, + "metadataX": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's X", + "example": "https://twitter.com/acme" + }, + "metadataUrl": { + "type": "string", + "description": "URL for AVS metadata", + "example": "https://example.json" + }, + "curatedMetadata": { + "type": "object", + "properties": { + "metadataName": { + "type": "string", + "description": "The name of the AVS", + "example": "Example AVS" + }, + "metadataDescription": { + "type": "string", + "nullable": true, + "description": "The description of the AVS", + "example": "This is an example AVS" + }, + "metadataDiscord": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's Discord server", + "example": "https://discord.com/invite/abcdefghij" + }, + "metadataLogo": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's logo" + }, + "metadataTelegram": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's Telegram channel", + "example": "https://t.me/acme" + }, + "metadataWebsite": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's website", + "example": "https://acme.com" + }, + "metadataX": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's X", + "example": "https://twitter.com/acme" + }, + "metadataGithub": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The URL of the AVS's Github", + "example": "https://github.com/acme" + }, + "metadataTokenAddress": { + "type": "string", + "nullable": true, + "format": "uri", + "description": "The Token Address of the AVS", + "example": "0x2344c0fe02ccd2b32155ca0ffcb1978a6d96a552" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags to describe the AVS", + "example": ["Example tag 1", "Example tag 2"] + }, + "isVisible": { + "type": "boolean", + "description": "Indicates if AVS visibility is allowed", + "example": false + }, + "isVerified": { + "type": "boolean", + "description": "Indicates if the AVS has been verified by the EigenExplorer team", + "example": false + } + }, + "required": [ + "metadataName", + "metadataDescription", + "metadataDiscord", + "metadataLogo", + "metadataTelegram", + "metadataWebsite", + "metadataX", + "metadataGithub", + "metadataTokenAddress", + "tags", + "isVisible", + "isVerified" + ], + "description": "Curated metadata information for AVS" + }, + "restakeableStrategies": { + "type": "array", + "items": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$" + }, + "description": "The list of restakeable strategies for AVS" + }, + "totalStakers": { + "type": "number", + "description": "Total number of stakers", + "example": 80000 + }, + "totalOperators": { + "type": "number", + "description": "Total number of operators", + "example": 200 + }, + "tvlEth": { + "type": "string", + "description": "Total TVL in ETH", + "example": "3000000" + }, + "createdAtBlock": { + "type": "number", + "description": "The block number at which the AVS was created", + "example": 19592323 + }, + "updatedAtBlock": { + "type": "number", + "description": "The block number at which the AVS was last updated", + "example": 19592323 + }, + "createdAt": { + "type": "string", + "description": "The time stamp at which the AVS was created", + "example": "2024-04-05T21:49:59.000Z" + }, + "updatedAt": { + "type": "string", + "description": "The time stamp at which the AVS was last updated", + "example": "2024-04-05T21:49:59.000Z" + }, + "address": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "AVS service manager contract address", + "example": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0" + }, + "rewardsSubmissions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "Id for the rewards submission", + "example": 1 + }, + "submissionNonce": { + "type": "number", + "description": "The nonce of the rewards submission", + "example": 0 + }, + "rewardsSubmissionHash": { + "type": "string", + "description": "The hash of the rewards submission", + "example": "0x2bc2f7cef0974f7064dbdae054f9a0e5ea1c2293d180a749c70100506382d85" + }, + "avsAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "AVS address for the rewards submission", + "example": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0" + }, + "strategyAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "Strategy address for the rewards submission", + "example": "0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6" + }, + "multiplier": { + "type": "string", + "description": "The multiplier associated with this strategy", + "example": "1000000000000000000" + }, + "token": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the token used for rewards distribution", + "example": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + }, + "amount": { + "type": "string", + "description": "The amount of rewards allocated to this strategy from the total rewards", + "example": "300000000000000000" + }, + "startTimestamp": { + "type": "number", + "description": "The timestamp marking the start of this rewards distribution period", + "example": 1720000000 + }, + "duration": { + "type": "number", + "description": "The duration (in seconds) over which the rewards are distributed", + "example": 2500000 + }, + "createdAtBlock": { + "type": "number", + "description": "The block number at which the reward submission was recorded", + "example": 20495824 + }, + "createdAt": { + "type": "string", + "description": "The timestamp at which the reward submission was recorded", + "example": "2024-08-10T04:28:47.000Z" + } + }, + "required": [ + "id", + "submissionNonce", + "rewardsSubmissionHash", + "avsAddress", + "strategyAddress", + "multiplier", + "token", + "amount", + "startTimestamp", + "duration", + "createdAtBlock", + "createdAt" + ] + }, + "description": "List of rewards submissions associated with AVS" + }, + "operators": { + "type": "array", + "items": { + "type": "object", + "properties": { + "avsAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "AVS service manager contract address", + "example": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0" + }, + "operatorAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the AVS operator", + "example": "0x4cd2086e1d708e65db5d4f5712a9ca46ed4bbd0a" + }, + "isActive": { + "type": "boolean", + "description": "True indicates operator is an active participant while False indicates it used to be one but not anymore", + "example": true + }, + "restakedStrategies": { + "type": "array", + "items": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$" + }, + "description": "List of strategies restaked by the operator", + "example": [ + "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0", + "0x93c4b944d05dfe6df7645a86cd2206016c51564d" + ] + }, + "createdAtBlock": { + "type": "number", + "description": "The block number at which the AVS Operator was registered", + "example": 19614553 + }, + "updatedAtBlock": { + "type": "number", + "description": "The block number at which the AVS Operator registration was last updated", + "example": 19614553 + }, + "createdAt": { + "type": "string", + "description": "The time stamp at which the AVS Operator was registered", + "example": "2024-04-09T00:35:35.000Z" + }, + "updatedAt": { + "type": "string", + "description": "The time stamp at which the AVS Operator registration was last updated", + "example": "2024-04-09T00:35:35.000Z" + }, + "operator": { + "type": "object", + "properties": { + "address": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the AVS operator", + "example": "0x09e6eb09213bdd3698bd8afb43ec3cb0ecff683a" + }, + "metadataUrl": { + "type": "string", "description": "URL for operator metadata", "example": "https://raw.githubusercontent.com/github-infstones/eigenlayer/main/metadata.json" }, @@ -5899,199 +6317,1892 @@ ] } }, - "required": [ - "avsAddress", - "operatorAddress", - "isActive", - "restakedStrategies", - "createdAtBlock", - "updatedAtBlock", - "createdAt", - "updatedAt", - "operator" - ] + "required": [ + "avsAddress", + "operatorAddress", + "isActive", + "restakedStrategies", + "createdAtBlock", + "updatedAtBlock", + "createdAt", + "updatedAt", + "operator" + ] + }, + "description": "List of operators associated with the AVS registration" + } + }, + "required": [ + "avsAddress", + "isActive", + "metadataName", + "metadataDescription", + "metadataDiscord", + "metadataLogo", + "metadataTelegram", + "metadataWebsite", + "metadataX", + "metadataUrl", + "restakeableStrategies", + "totalStakers", + "totalOperators", + "tvlEth", + "createdAtBlock", + "updatedAtBlock", + "createdAt", + "updatedAt", + "address", + "rewardsSubmissions", + "operators" + ] + }, + "description": "Detailed AVS registrations information for the operator" + }, + "tvl": { + "type": "object", + "properties": { + "tvl": { + "type": "number", + "description": "The combined TVL of all restaking strategies in ETH", + "example": 1000000 + }, + "tvlBeaconChain": { + "type": "number", + "description": "The TVL of Beacon Chain restaking strategy in ETH", + "example": 1000000 + }, + "tvlRestaking": { + "type": "number", + "description": "The combined TVL of all liquid restaking strategies in ETH", + "example": 1000000 + }, + "tvlWETH": { + "type": "number", + "description": "The TVL of WETH restaking strategy in ETH", + "example": 1000000 + }, + "tvlStrategies": { + "type": "object", + "additionalProperties": { + "type": "number", + "description": "The total value locked (TVL) in the strategy, denominated in the strategy's native token", + "example": 1000000 + }, + "description": "The TVL of each individual restaking strategy in its native token", + "example": { + "Eigen": 1000000, + "cbETH": 2000000 + } + }, + "tvlStrategiesEth": { + "type": "object", + "additionalProperties": { + "type": "number", + "description": "The total value locked (TVL) in the strategy, denominated in ETH", + "example": 1000000 + }, + "description": "The TVL of each individual restaking strategy in ETH", + "example": { + "Eigen": 1000000, + "cbETH": 2000000 + } + } + }, + "required": [ + "tvl", + "tvlBeaconChain", + "tvlRestaking", + "tvlWETH", + "tvlStrategies", + "tvlStrategiesEth" + ], + "description": "The total value locked (TVL) in the AVS operator", + "example": { + "tvl": 1000000, + "tvlBeaconChain": 1000000, + "tvlWETH": 1000000, + "tvlRestaking": 1000000, + "tvlStrategies": { + "Eigen": 1000000, + "cbETH": 2000000 + }, + "tvlStrategiesEth": { + "stETH": 1000000, + "cbETH": 2000000 + } + } + }, + "rewards": { + "type": "object", + "properties": { + "avs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "avsAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "AVS service manager contract address", + "example": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0" + }, + "apy": { + "type": "number", + "description": "Latest APY recorded for the AVS", + "example": 0.15973119826488588 + } + }, + "required": ["avsAddress", "apy"] + } + }, + "strategies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "strategyAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the restaking strategy", + "example": "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" + }, + "apy": { + "type": "number", + "description": "APY of the restaking strategy", + "example": 0.1 + } + }, + "required": ["strategyAddress", "apy"] + } + }, + "aggregateApy": { + "type": "number", + "description": "The aggregate APY across all strategies", + "example": 1 + }, + "operatorEarningsEth": { + "type": "string", + "description": "Total earnings for the operator in ETH", + "example": "1000000000000000000" + } + }, + "required": ["avs", "strategies", "aggregateApy", "operatorEarningsEth"], + "description": "The rewards information for the operator" + } + }, + "required": [ + "address", + "metadataName", + "metadataDescription", + "metadataDiscord", + "metadataLogo", + "metadataTelegram", + "metadataWebsite", + "metadataX", + "totalStakers", + "totalAvs", + "apy", + "createdAtBlock", + "updatedAtBlock", + "createdAt", + "updatedAt", + "shares", + "avsRegistrations", + "rewards" + ] + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, + "/operators/{address}/rewards": { + "get": { + "operationId": "getOperatorRewards", + "summary": "Retrieve rewards info for an operator", + "description": "Returns a list of strategies that the Operator is rewarded for, and the tokens they are rewarded in.", + "tags": ["Operators"], + "parameters": [ + { + "in": "path", + "name": "address", + "description": "The address of the operator", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the operator", + "example": "0x00107cfdeaddc0a3160ed2f6fedd627f313e7b1a" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "The reward strategies and tokens found for the Operator.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the AVS operator", + "example": "0xdbed88d83176316fc46797b43adee927dc2ff2f5" + }, + "rewardTokens": { + "type": "array", + "items": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$" + }, + "description": "List of tokens in which the operator receives rewards", + "example": [ + "0xba50933c268f567bdc86e1ac131be072c6b0b71a", + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + ] + }, + "rewardStrategies": { + "type": "array", + "items": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$" + }, + "description": "List of strategies for which the operator receives rewards", + "example": [ + "0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6", + "0x13760f50a9d7377e4f20cb8cf9e4c26586c658ff" + ] + } + }, + "required": ["address", "rewardTokens", "rewardStrategies"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, + "/operators/{address}/events/delegation": { + "get": { + "operationId": "getOperatorDelegationEvents", + "summary": "Retrieve all delegation events for a given operator address", + "description": "Returns a list of all delegation events for a given operator address.", + "tags": ["Operators"], + "parameters": [ + { + "in": "path", + "name": "address", + "description": "The address of the operator", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the operator", + "example": "0x00107cfdeaddc0a3160ed2f6fedd627f313e7b1a" + }, + "required": true + }, + { + "in": "query", + "name": "txHash", + "description": "The transaction hash associated with the event", + "schema": { + "type": "string", + "pattern": "^0x([A-Fa-f0-9]{64})$", + "description": "The transaction hash associated with the event" + } + }, + { + "in": "query", + "name": "startAt", + "description": "Start date in ISO string format", + "schema": { + "type": "string", + "description": "Start date in ISO string format" + } + }, + { + "in": "query", + "name": "endAt", + "description": "End date in ISO string format", + "schema": { + "type": "string", + "description": "End date in ISO string format" + } + }, + { + "in": "query", + "name": "type", + "description": "The type of the delegation event", + "schema": { + "type": "string", + "enum": ["DELEGATION", "UNDELEGATION", "SHARES_INCREASED", "SHARES_DECREASED"], + "description": "The type of the delegation event" + } + }, + { + "in": "query", + "name": "strategyAddress", + "description": "The contract address of the restaking strategy", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the restaking strategy" + } + }, + { + "in": "query", + "name": "stakerAddress", + "description": "The address of the staker", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the staker" + } + }, + { + "in": "query", + "name": "withTokenData", + "description": "Toggle whether the route should return underlying token address and underlying value", + "schema": { + "type": "string", + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should return underlying token address and underlying value", + "example": "false" + } + }, + { + "in": "query", + "name": "withEthValue", + "description": "Toggle whether the route should return value denominated in ETH", + "schema": { + "type": "string", + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should return value denominated in ETH", + "example": "false" + } + }, + { + "in": "query", + "name": "skip", + "description": "The number of records to skip for pagination", + "schema": { + "type": "string", + "default": "0", + "description": "The number of records to skip for pagination", + "example": 0 + } + }, + { + "in": "query", + "name": "take", + "description": "The number of records to return for pagination", + "schema": { + "type": "string", + "default": "12", + "description": "The number of records to return for pagination", + "example": 12 + } + } + ], + "responses": { + "200": { + "description": "The delegation events found for the operator.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tx": { + "type": "string", + "description": "The transaction hash", + "example": "0xae41958d0342a4485536f701c72723625131680f182eb21f95abdac6d74d0ff0" + }, + "blockNumber": { + "type": "number", + "description": "The block number of the transaction", + "example": 21045085 + }, + "blockTime": { + "type": "string", + "description": "The block time of the transaction as an ISO 8601 string", + "example": "2024-10-25T20:41:11.000Z" + }, + "type": { + "type": "string", + "enum": [ + "DELEGATION", + "UNDELEGATION", + "SHARES_INCREASED", + "SHARES_DECREASED" + ], + "description": "The type of the event", + "example": "DELEGATION" + }, + "args": { + "type": "object", + "properties": { + "staker": { + "type": "string", + "description": "The contract address of the staker", + "example": "0x42318adf0773b8af4aa8ed1670ea0af7761d07c7" + }, + "strategy": { + "type": "string", + "description": "The contract address of the restaking strategy", + "example": "0x93c4b944d05dfe6df7645a86cd2206016c51564d" + }, + "shares": { + "type": "number", + "description": "The change in the operator's delegated shares, added or subtracted from the total.", + "example": 62816824424188010 + } + }, + "required": ["staker"] + }, + "underlyingToken": { + "type": "string", + "description": "The contract address of the token associated with this strategy", + "example": "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" + }, + "underlyingValue": { + "type": "number", + "description": "The value of the shares in terms of the underlying token", + "example": 5 + }, + "ethValue": { + "type": "number", + "description": "The value of the shares in ETH", + "example": 1 + } + }, + "required": ["tx", "blockNumber", "blockTime", "type", "args"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, + "/operators/{address}/events/registration-status": { + "get": { + "operationId": "getOperatorRegistrationEvents", + "summary": "Retrieve all registration events for a given operator address", + "description": "Returns a list of all registration events for a given operator address.", + "tags": ["Operators"], + "parameters": [ + { + "in": "path", + "name": "address", + "description": "The address of the operator", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the operator", + "example": "0x00107cfdeaddc0a3160ed2f6fedd627f313e7b1a" + }, + "required": true + }, + { + "in": "query", + "name": "txHash", + "description": "The transaction hash associated with the event", + "schema": { + "type": "string", + "pattern": "^0x([A-Fa-f0-9]{64})$", + "description": "The transaction hash associated with the event" + } + }, + { + "in": "query", + "name": "startAt", + "description": "Start date in ISO string format", + "schema": { + "type": "string", + "description": "Start date in ISO string format" + } + }, + { + "in": "query", + "name": "endAt", + "description": "End date in ISO string format", + "schema": { + "type": "string", + "description": "End date in ISO string format" + } + }, + { + "in": "query", + "name": "status", + "description": "The status of Registration", + "schema": { + "type": "string", + "enum": ["REGISTERED", "DEREGISTERED"], + "description": "The status of Registration" + } + }, + { + "in": "query", + "name": "avsAddress", + "description": "The address of the avs", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the avs" + } + }, + { + "in": "query", + "name": "skip", + "description": "The number of records to skip for pagination", + "schema": { + "type": "string", + "default": "0", + "description": "The number of records to skip for pagination", + "example": 0 + } + }, + { + "in": "query", + "name": "take", + "description": "The number of records to return for pagination", + "schema": { + "type": "string", + "default": "12", + "description": "The number of records to return for pagination", + "example": 12 + } + } + ], + "responses": { + "200": { + "description": "The registration events found for the operator.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tx": { + "type": "string", + "description": "The transaction hash", + "example": "0xae41958d0342a4485536f701c72723625131680f182eb21f95abdac6d74d0ff0" + }, + "blockNumber": { + "type": "number", + "description": "The block number of the transaction", + "example": 21045085 + }, + "blockTime": { + "type": "string", + "description": "The block time of the transaction as an ISO 8601 string", + "example": "2024-10-25T20:41:11.000Z" + }, + "type": { + "type": "string", + "enum": ["REGISTRATION_STATUS"], + "description": "The type of the event", + "example": "REGISTRATION_STATUS" + }, + "args": { + "type": "object", + "properties": { + "avs": { + "type": "string", + "description": "AVS service manager contract address", + "example": "0xb73a87e8f7f9129816d40940ca19dfa396944c71" + }, + "status": { + "type": "string", + "enum": ["REGISTERED", "DEREGISTERED"], + "description": "The status of the registration", + "example": "REGISTERED" + } + }, + "required": ["avs", "status"] + } + }, + "required": ["tx", "blockNumber", "blockTime", "type", "args"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, + "/withdrawals": { + "get": { + "operationId": "getAllWithdrawals", + "summary": "Retrieve all withdrawals", + "description": "Returns all withdrawal data, including the withdrawal root, nonce, withdrawal status, and other relevant information.", + "tags": ["Withdrawals"], + "parameters": [ + { + "in": "query", + "name": "stakerAddress", + "description": "The address of the staker", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the staker", + "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" + } + }, + { + "in": "query", + "name": "delegatedTo", + "description": "The address of the operator to which the stake is delegated", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the operator to which the stake is delegated", + "example": "0x5accc90436492f24e6af278569691e2c942a676d" + } + }, + { + "in": "query", + "name": "strategyAddress", + "description": "The contract address of the restaking strategy", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the restaking strategy", + "example": "0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6" + } + }, + { + "in": "query", + "name": "status", + "description": "The status of the withdrawal", + "schema": { + "type": "string", + "enum": ["queued", "queued_withdrawable", "completed"], + "description": "The status of the withdrawal", + "example": "queued" + } + }, + { + "in": "query", + "name": "skip", + "description": "The number of records to skip for pagination", + "schema": { + "type": "string", + "default": "0", + "description": "The number of records to skip for pagination", + "example": 0 + } + }, + { + "in": "query", + "name": "take", + "description": "The number of records to return for pagination", + "schema": { + "type": "string", + "default": "12", + "description": "The number of records to return for pagination", + "example": 12 + } + } + ], + "responses": { + "200": { + "description": "The list of withdrawals.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "withdrawalRoot": { + "type": "string", + "description": "The root hash of the withdrawal", + "example": "0x9e6728ef0a8ad6009107a886047aae35bc5ed7deaa68580b0d1f8f67e3e5ed31" + }, + "nonce": { + "type": "number", + "description": "The nonce of the withdrawal, ensuring unique hashes for otherwise identical withdrawals", + "example": 0 + }, + "stakerAddress": { + "type": "string", + "description": "The contract address of the staker who initiated the withdrawal", + "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" + }, + "delegatedTo": { + "type": "string", + "description": "The address to which the staker was delegated when the withdrawal was initiated", + "example": "0x0000000000000000000000000000000000000000" + }, + "withdrawerAddress": { + "type": "string", + "description": "The address of the withdrawer, authorized to complete the withdrawal and receive the funds", + "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" + }, + "shares": { + "type": "array", + "items": { + "type": "object", + "properties": { + "strategyAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the restaking strategy", + "example": "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" + }, + "shares": { + "type": "string", + "description": "The amount of shares held in the strategy", + "example": "1277920000000000000000000" + } + }, + "required": ["strategyAddress", "shares"] + }, + "description": "The list of strategy shares", + "example": [ + { + "strategyAddress": "0x93c4b944d05dfe6df7645a86cd2206016c51564d", + "shares": "1000288824523326631" + } + ] + }, + "createdAtBlock": { + "type": "number", + "description": "The block number when the withdrawal was recorded by EigenExplorer", + "example": 19912470 + }, + "createdAt": { + "type": "string", + "description": "The time stamp when the withdrawal was recorded by EigenExplorer", + "example": "2024-07-07T23:53:35.000Z" + }, + "updatedAtBlock": { + "type": "number", + "description": "The block number when the withdrawal was last updated", + "example": 19912470 + }, + "updatedAt": { + "type": "string", + "description": "The time stamp when the withdrawal was last updated", + "example": "2024-07-07T23:53:35.000Z" + }, + "isCompleted": { + "type": "boolean", + "description": "Indicates if the withdrawal is completed", + "example": false + } + }, + "required": [ + "withdrawalRoot", + "nonce", + "stakerAddress", + "delegatedTo", + "withdrawerAddress", + "shares", + "createdAtBlock", + "createdAt", + "updatedAtBlock", + "updatedAt", + "isCompleted" + ] + } + }, + "meta": { + "type": "object", + "properties": { + "total": { + "type": "number", + "description": "Total number of records in the database", + "example": 30 + }, + "skip": { + "type": "number", + "description": "The number of skiped records for this query", + "example": 0 + }, + "take": { + "type": "number", + "description": "The number of records returned for this query", + "example": 12 + } + }, + "required": ["total", "skip", "take"] + } + }, + "required": ["data", "meta"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, + "/withdrawals/{withdrawalRoot}": { + "get": { + "operationId": "getWithdrawalByWithdrawalRoot", + "summary": "Retrieve withdrawal by withdrawal root", + "description": "Returns the withdrawal data by withdrawal root.", + "tags": ["Withdrawals"], + "parameters": [ + { + "in": "path", + "name": "withdrawalRoot", + "description": "The root hash of the withdrawal", + "schema": { + "type": "string", + "description": "The root hash of the withdrawal", + "example": "0x9e6728ef0a8ad6009107a886047aae35bc5ed7deaa68580b0d1f8f67e3e5ed31" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "The requested withdrawal record.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "withdrawalRoot": { + "type": "string", + "description": "The root hash of the withdrawal", + "example": "0x9e6728ef0a8ad6009107a886047aae35bc5ed7deaa68580b0d1f8f67e3e5ed31" + }, + "nonce": { + "type": "number", + "description": "The nonce of the withdrawal, ensuring unique hashes for otherwise identical withdrawals", + "example": 0 + }, + "stakerAddress": { + "type": "string", + "description": "The contract address of the staker who initiated the withdrawal", + "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" + }, + "delegatedTo": { + "type": "string", + "description": "The address to which the staker was delegated when the withdrawal was initiated", + "example": "0x0000000000000000000000000000000000000000" + }, + "withdrawerAddress": { + "type": "string", + "description": "The address of the withdrawer, authorized to complete the withdrawal and receive the funds", + "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" + }, + "shares": { + "type": "array", + "items": { + "type": "object", + "properties": { + "strategyAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the restaking strategy", + "example": "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" + }, + "shares": { + "type": "string", + "description": "The amount of shares held in the strategy", + "example": "1277920000000000000000000" + } + }, + "required": ["strategyAddress", "shares"] + }, + "description": "The list of strategy shares", + "example": [ + { + "strategyAddress": "0x93c4b944d05dfe6df7645a86cd2206016c51564d", + "shares": "1000288824523326631" + } + ] + }, + "createdAtBlock": { + "type": "number", + "description": "The block number when the withdrawal was recorded by EigenExplorer", + "example": 19912470 + }, + "createdAt": { + "type": "string", + "description": "The time stamp when the withdrawal was recorded by EigenExplorer", + "example": "2024-07-07T23:53:35.000Z" + }, + "updatedAtBlock": { + "type": "number", + "description": "The block number when the withdrawal was last updated", + "example": 19912470 + }, + "updatedAt": { + "type": "string", + "description": "The time stamp when the withdrawal was last updated", + "example": "2024-07-07T23:53:35.000Z" + }, + "isCompleted": { + "type": "boolean", + "description": "Indicates if the withdrawal is completed", + "example": false + } + }, + "required": [ + "withdrawalRoot", + "nonce", + "stakerAddress", + "delegatedTo", + "withdrawerAddress", + "shares", + "createdAtBlock", + "createdAt", + "updatedAtBlock", + "updatedAt", + "isCompleted" + ] + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, + "/stakers": { + "get": { + "operationId": "getAllStakers", + "summary": "Retrieve all stakers", + "description": "Returns all staker records. This endpoint supports pagination.", + "tags": ["Stakers"], + "parameters": [ + { + "in": "query", + "name": "withTvl", + "description": "Toggle whether the route should calculate the TVL from shares", + "schema": { + "type": "string", + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should calculate the TVL from shares", + "example": "false" + } + }, + { + "in": "query", + "name": "updatedSince", + "description": "Fetch stakers updated since this timestamp", + "schema": { + "type": "string", + "description": "Fetch stakers updated since this timestamp", + "example": "2024-04-11T08:31:11.000Z" + } + }, + { + "in": "query", + "name": "skip", + "description": "The number of records to skip for pagination", + "schema": { + "type": "string", + "default": "0", + "description": "The number of records to skip for pagination", + "example": 0 + } + }, + { + "in": "query", + "name": "take", + "description": "The number of records to return for pagination", + "schema": { + "type": "string", + "default": "12", + "description": "The number of records to return for pagination", + "example": 12 + } + } + ], + "responses": { + "200": { + "description": "The list of staker records.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the staker", + "example": "0x0000006c21964af0d420af8992851a30fa13a68b" + }, + "operatorAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the operator", + "example": "0x71c6f7ed8c2d4925d0baf16f6a85bb1736d412eb" + }, + "createdAtBlock": { + "type": "string", + "description": "The block number at which the Staker made first delegation", + "example": "19631203" + }, + "updatedAtBlock": { + "type": "string", + "description": "The block number at which the Staker made last delegation", + "example": "19631203" + }, + "createdAt": { + "type": "string", + "description": "The time stamp at which the Staker made first delegation", + "example": "2024-04-11T08:31:11.000Z" + }, + "updatedAt": { + "type": "string", + "description": "The time stamp at which the Staker made last delegation", + "example": "2024-04-11T08:31:11.000Z" + }, + "shares": { + "type": "array", + "items": { + "type": "object", + "properties": { + "strategyAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the restaking strategy", + "example": "0x93c4b944d05dfe6df7645a86cd2206016c51564a" + }, + "shares": { + "type": "string", + "description": "The amount of shares held in the strategy", + "example": "40000000000000000" + } + }, + "required": ["strategyAddress", "shares"] + } + }, + "tvl": { + "type": "object", + "properties": { + "tvl": { + "type": "number", + "description": "The combined TVL of all restaking strategies in ETH", + "example": 1000000 + }, + "tvlBeaconChain": { + "type": "number", + "description": "The TVL of Beacon Chain restaking strategy in ETH", + "example": 1000000 + }, + "tvlRestaking": { + "type": "number", + "description": "The combined TVL of all liquid restaking strategies in ETH", + "example": 1000000 + }, + "tvlWETH": { + "type": "number", + "description": "The TVL of WETH restaking strategy in ETH", + "example": 1000000 + }, + "tvlStrategies": { + "type": "object", + "additionalProperties": { + "type": "number", + "description": "The total value locked (TVL) in the strategy, denominated in the strategy's native token", + "example": 1000000 + }, + "description": "The TVL of each individual restaking strategy in its native token", + "example": { + "Eigen": 1000000, + "cbETH": 2000000 + } + }, + "tvlStrategiesEth": { + "type": "object", + "additionalProperties": { + "type": "number", + "description": "The total value locked (TVL) in the strategy, denominated in ETH", + "example": 1000000 + }, + "description": "The TVL of each individual restaking strategy in ETH", + "example": { + "Eigen": 1000000, + "cbETH": 2000000 + } + } + }, + "required": [ + "tvl", + "tvlBeaconChain", + "tvlRestaking", + "tvlWETH", + "tvlStrategies", + "tvlStrategiesEth" + ], + "description": "The total value locked (TVL) in the AVS staker", + "example": { + "tvl": 1000000, + "tvlBeaconChain": 1000000, + "tvlWETH": 1000000, + "tvlRestaking": 1000000, + "tvlStrategies": { + "Eigen": 1000000, + "cbETH": 2000000 + }, + "tvlStrategiesEth": { + "stETH": 1000000, + "cbETH": 2000000 + } + } + } + }, + "required": [ + "address", + "operatorAddress", + "createdAtBlock", + "updatedAtBlock", + "createdAt", + "updatedAt", + "shares" + ] + } + }, + "meta": { + "type": "object", + "properties": { + "total": { + "type": "number", + "description": "Total number of records in the database", + "example": 30 + }, + "skip": { + "type": "number", + "description": "The number of skiped records for this query", + "example": 0 + }, + "take": { + "type": "number", + "description": "The number of records returned for this query", + "example": 12 + } + }, + "required": ["total", "skip", "take"] + } + }, + "required": ["data", "meta"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, + "/stakers/{address}": { + "get": { + "operationId": "getStakerByAddress", + "summary": "Retrieve a staker by address", + "description": "Returns a staker record by address.", + "tags": ["Stakers"], + "parameters": [ + { + "in": "path", + "name": "address", + "description": "The address of the staker", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the staker", + "example": "0x9791fdb4e9c0495efc5a1f3f9271ef226251eb34" + }, + "required": true + }, + { + "in": "query", + "name": "withTvl", + "description": "Toggle whether the route should calculate the TVL from shares", + "schema": { + "type": "string", + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should calculate the TVL from shares", + "example": "false" + } + } + ], + "responses": { + "200": { + "description": "The record of the requested operator.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the staker", + "example": "0x0000006c21964af0d420af8992851a30fa13a68b" + }, + "operatorAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the operator", + "example": "0x71c6f7ed8c2d4925d0baf16f6a85bb1736d412eb" + }, + "createdAtBlock": { + "type": "string", + "description": "The block number at which the Staker made first delegation", + "example": "19631203" + }, + "updatedAtBlock": { + "type": "string", + "description": "The block number at which the Staker made last delegation", + "example": "19631203" + }, + "createdAt": { + "type": "string", + "description": "The time stamp at which the Staker made first delegation", + "example": "2024-04-11T08:31:11.000Z" + }, + "updatedAt": { + "type": "string", + "description": "The time stamp at which the Staker made last delegation", + "example": "2024-04-11T08:31:11.000Z" + }, + "shares": { + "type": "array", + "items": { + "type": "object", + "properties": { + "strategyAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the restaking strategy", + "example": "0x93c4b944d05dfe6df7645a86cd2206016c51564a" + }, + "shares": { + "type": "string", + "description": "The amount of shares held in the strategy", + "example": "40000000000000000" + } + }, + "required": ["strategyAddress", "shares"] + } + }, + "tvl": { + "type": "object", + "properties": { + "tvl": { + "type": "number", + "description": "The combined TVL of all restaking strategies in ETH", + "example": 1000000 + }, + "tvlBeaconChain": { + "type": "number", + "description": "The TVL of Beacon Chain restaking strategy in ETH", + "example": 1000000 + }, + "tvlRestaking": { + "type": "number", + "description": "The combined TVL of all liquid restaking strategies in ETH", + "example": 1000000 + }, + "tvlWETH": { + "type": "number", + "description": "The TVL of WETH restaking strategy in ETH", + "example": 1000000 + }, + "tvlStrategies": { + "type": "object", + "additionalProperties": { + "type": "number", + "description": "The total value locked (TVL) in the strategy, denominated in the strategy's native token", + "example": 1000000 + }, + "description": "The TVL of each individual restaking strategy in its native token", + "example": { + "Eigen": 1000000, + "cbETH": 2000000 + } + }, + "tvlStrategiesEth": { + "type": "object", + "additionalProperties": { + "type": "number", + "description": "The total value locked (TVL) in the strategy, denominated in ETH", + "example": 1000000 + }, + "description": "The TVL of each individual restaking strategy in ETH", + "example": { + "Eigen": 1000000, + "cbETH": 2000000 + } + } + }, + "required": [ + "tvl", + "tvlBeaconChain", + "tvlRestaking", + "tvlWETH", + "tvlStrategies", + "tvlStrategiesEth" + ], + "description": "The total value locked (TVL) in the AVS staker", + "example": { + "tvl": 1000000, + "tvlBeaconChain": 1000000, + "tvlWETH": 1000000, + "tvlRestaking": 1000000, + "tvlStrategies": { + "Eigen": 1000000, + "cbETH": 2000000 + }, + "tvlStrategiesEth": { + "stETH": 1000000, + "cbETH": 2000000 + } + } + } + }, + "required": [ + "address", + "operatorAddress", + "createdAtBlock", + "updatedAtBlock", + "createdAt", + "updatedAt", + "shares" + ] + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, + "/stakers/{address}/withdrawals": { + "get": { + "operationId": "getStakerWithdrawals", + "summary": "Retrieve all withdrawals by staker address", + "description": "Returns all withdrawal data of the requested staker, including the withdrawal root, nonce, withdrawal status, and other relevant information.", + "tags": ["Stakers"], + "parameters": [ + { + "in": "path", + "name": "address", + "description": "The address of the staker", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the staker", + "example": "0x9791fdb4e9c0495efc5a1f3f9271ef226251eb34" + }, + "required": true + }, + { + "in": "query", + "name": "skip", + "description": "The number of records to skip for pagination", + "schema": { + "type": "string", + "default": "0", + "description": "The number of records to skip for pagination", + "example": 0 + } + }, + { + "in": "query", + "name": "take", + "description": "The number of records to return for pagination", + "schema": { + "type": "string", + "default": "12", + "description": "The number of records to return for pagination", + "example": 12 + } + } + ], + "responses": { + "200": { + "description": "The list of withdrawals.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "withdrawalRoot": { + "type": "string", + "description": "The root hash of the withdrawal", + "example": "0x9e6728ef0a8ad6009107a886047aae35bc5ed7deaa68580b0d1f8f67e3e5ed31" + }, + "nonce": { + "type": "number", + "description": "The nonce of the withdrawal, ensuring unique hashes for otherwise identical withdrawals", + "example": 0 + }, + "stakerAddress": { + "type": "string", + "description": "The contract address of the staker who initiated the withdrawal", + "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" + }, + "delegatedTo": { + "type": "string", + "description": "The address to which the staker was delegated when the withdrawal was initiated", + "example": "0x0000000000000000000000000000000000000000" + }, + "withdrawerAddress": { + "type": "string", + "description": "The address of the withdrawer, authorized to complete the withdrawal and receive the funds", + "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" + }, + "shares": { + "type": "array", + "items": { + "type": "object", + "properties": { + "strategyAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the restaking strategy", + "example": "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" + }, + "shares": { + "type": "string", + "description": "The amount of shares held in the strategy", + "example": "1277920000000000000000000" + } + }, + "required": ["strategyAddress", "shares"] }, - "description": "List of operators associated with the AVS registration" + "description": "The list of strategy shares", + "example": [ + { + "strategyAddress": "0x93c4b944d05dfe6df7645a86cd2206016c51564d", + "shares": "1000288824523326631" + } + ] + }, + "createdAtBlock": { + "type": "number", + "description": "The block number when the withdrawal was recorded by EigenExplorer", + "example": 19912470 + }, + "createdAt": { + "type": "string", + "description": "The time stamp when the withdrawal was recorded by EigenExplorer", + "example": "2024-07-07T23:53:35.000Z" + }, + "updatedAtBlock": { + "type": "number", + "description": "The block number when the withdrawal was last updated", + "example": 19912470 + }, + "updatedAt": { + "type": "string", + "description": "The time stamp when the withdrawal was last updated", + "example": "2024-07-07T23:53:35.000Z" + }, + "isCompleted": { + "type": "boolean", + "description": "Indicates if the withdrawal is completed", + "example": false } }, "required": [ - "avsAddress", - "isActive", - "metadataName", - "metadataDescription", - "metadataDiscord", - "metadataLogo", - "metadataTelegram", - "metadataWebsite", - "metadataX", - "metadataUrl", - "restakeableStrategies", - "totalStakers", - "totalOperators", - "tvlEth", + "withdrawalRoot", + "nonce", + "stakerAddress", + "delegatedTo", + "withdrawerAddress", + "shares", "createdAtBlock", - "updatedAtBlock", "createdAt", + "updatedAtBlock", "updatedAt", - "address", - "rewardsSubmissions", - "operators" + "isCompleted" ] - }, - "description": "Detailed AVS registrations information for the operator" + } }, - "tvl": { + "meta": { "type": "object", "properties": { - "tvl": { - "type": "number", - "description": "The combined TVL of all restaking strategies in ETH", - "example": 1000000 - }, - "tvlBeaconChain": { + "total": { "type": "number", - "description": "The TVL of Beacon Chain restaking strategy in ETH", - "example": 1000000 + "description": "Total number of records in the database", + "example": 30 }, - "tvlRestaking": { + "skip": { "type": "number", - "description": "The combined TVL of all liquid restaking strategies in ETH", - "example": 1000000 + "description": "The number of skiped records for this query", + "example": 0 }, - "tvlWETH": { + "take": { "type": "number", - "description": "The TVL of WETH restaking strategy in ETH", - "example": 1000000 - }, - "tvlStrategies": { - "type": "object", - "additionalProperties": { + "description": "The number of records returned for this query", + "example": 12 + } + }, + "required": ["total", "skip", "take"] + } + }, + "required": ["data", "meta"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, + "/stakers/{address}/withdrawals/queued": { + "get": { + "operationId": "getQueuedStakerWithdrawals", + "summary": "Retrieve queued withdrawals by staker address", + "description": "Returns all queued withdrawal data of the requested staker.", + "tags": ["Stakers"], + "parameters": [ + { + "in": "path", + "name": "address", + "description": "The address of the staker", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the staker", + "example": "0x9791fdb4e9c0495efc5a1f3f9271ef226251eb34" + }, + "required": true + }, + { + "in": "query", + "name": "skip", + "description": "The number of records to skip for pagination", + "schema": { + "type": "string", + "default": "0", + "description": "The number of records to skip for pagination", + "example": 0 + } + }, + { + "in": "query", + "name": "take", + "description": "The number of records to return for pagination", + "schema": { + "type": "string", + "default": "12", + "description": "The number of records to return for pagination", + "example": 12 + } + } + ], + "responses": { + "200": { + "description": "The list of queued withdrawals.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "withdrawalRoot": { + "type": "string", + "description": "The root hash of the withdrawal", + "example": "0x9e6728ef0a8ad6009107a886047aae35bc5ed7deaa68580b0d1f8f67e3e5ed31" + }, + "nonce": { "type": "number", - "description": "The total value locked (TVL) in the strategy, denominated in the strategy's native token", - "example": 1000000 + "description": "The nonce of the withdrawal, ensuring unique hashes for otherwise identical withdrawals", + "example": 0 + }, + "stakerAddress": { + "type": "string", + "description": "The contract address of the staker who initiated the withdrawal", + "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" + }, + "delegatedTo": { + "type": "string", + "description": "The address to which the staker was delegated when the withdrawal was initiated", + "example": "0x0000000000000000000000000000000000000000" + }, + "withdrawerAddress": { + "type": "string", + "description": "The address of the withdrawer, authorized to complete the withdrawal and receive the funds", + "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" + }, + "shares": { + "type": "array", + "items": { + "type": "object", + "properties": { + "strategyAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the restaking strategy", + "example": "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" + }, + "shares": { + "type": "string", + "description": "The amount of shares held in the strategy", + "example": "1277920000000000000000000" + } + }, + "required": ["strategyAddress", "shares"] + }, + "description": "The list of strategy shares", + "example": [ + { + "strategyAddress": "0x93c4b944d05dfe6df7645a86cd2206016c51564d", + "shares": "1000288824523326631" + } + ] }, - "description": "The TVL of each individual restaking strategy in its native token", - "example": { - "Eigen": 1000000, - "cbETH": 2000000 - } - }, - "tvlStrategiesEth": { - "type": "object", - "additionalProperties": { + "createdAtBlock": { "type": "number", - "description": "The total value locked (TVL) in the strategy, denominated in ETH", - "example": 1000000 + "description": "The block number when the withdrawal was recorded by EigenExplorer", + "example": 19912470 }, - "description": "The TVL of each individual restaking strategy in ETH", - "example": { - "Eigen": 1000000, - "cbETH": 2000000 + "createdAt": { + "type": "string", + "description": "The time stamp when the withdrawal was recorded by EigenExplorer", + "example": "2024-07-07T23:53:35.000Z" } - } - }, - "required": [ - "tvl", - "tvlBeaconChain", - "tvlRestaking", - "tvlWETH", - "tvlStrategies", - "tvlStrategiesEth" - ], - "description": "The total value locked (TVL) in the AVS operator", - "example": { - "tvl": 1000000, - "tvlBeaconChain": 1000000, - "tvlWETH": 1000000, - "tvlRestaking": 1000000, - "tvlStrategies": { - "Eigen": 1000000, - "cbETH": 2000000 }, - "tvlStrategiesEth": { - "stETH": 1000000, - "cbETH": 2000000 - } + "required": [ + "withdrawalRoot", + "nonce", + "stakerAddress", + "delegatedTo", + "withdrawerAddress", + "shares", + "createdAtBlock", + "createdAt" + ] } }, - "rewards": { + "meta": { "type": "object", "properties": { - "avs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "avsAddress": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "AVS service manager contract address", - "example": "0x870679e138bcdf293b7ff14dd44b70fc97e12fc0" - }, - "apy": { - "type": "number", - "description": "Latest APY recorded for the AVS", - "example": 0.15973119826488588 - } - }, - "required": ["avsAddress", "apy"] - } - }, - "strategies": { - "type": "array", - "items": { - "type": "object", - "properties": { - "strategyAddress": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the restaking strategy", - "example": "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" - }, - "apy": { - "type": "number", - "description": "APY of the restaking strategy", - "example": 0.1 - } - }, - "required": ["strategyAddress", "apy"] - } + "total": { + "type": "number", + "description": "Total number of records in the database", + "example": 30 }, - "aggregateApy": { + "skip": { "type": "number", - "description": "The aggregate APY across all strategies", - "example": 1 + "description": "The number of skiped records for this query", + "example": 0 }, - "operatorEarningsEth": { - "type": "string", - "description": "Total earnings for the operator in ETH", - "example": "1000000000000000000" + "take": { + "type": "number", + "description": "The number of records returned for this query", + "example": 12 } }, - "required": ["avs", "strategies", "aggregateApy", "operatorEarningsEth"], - "description": "The rewards information for the operator" + "required": ["total", "skip", "take"] } }, - "required": [ - "address", - "metadataName", - "metadataDescription", - "metadataDiscord", - "metadataLogo", - "metadataTelegram", - "metadataWebsite", - "metadataX", - "totalStakers", - "totalAvs", - "apy", - "createdAtBlock", - "updatedAtBlock", - "createdAt", - "updatedAt", - "shares", - "avsRegistrations", - "rewards" - ] + "required": ["data", "meta"] } } } @@ -6120,66 +8231,159 @@ } } }, - "/operators/{address}/rewards": { + "/stakers/{address}/withdrawals/queued_withdrawable": { "get": { - "operationId": "getOperatorRewards", - "summary": "Retrieve rewards info for an operator", - "description": "Returns a list of strategies that the Operator is rewarded for, and the tokens they are rewarded in.", - "tags": ["Operators"], + "operationId": "getQueuedWithdrawableStakerWithdrawals", + "summary": "Retrieve queued and withdrawable withdrawals by staker address", + "description": "Returns all queued and withdrawable withdrawal data of the requested staker.", + "tags": ["Stakers"], "parameters": [ { "in": "path", "name": "address", - "description": "The address of the operator", + "description": "The address of the staker", "schema": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The address of the operator", - "example": "0x00107cfdeaddc0a3160ed2f6fedd627f313e7b1a" + "description": "The address of the staker", + "example": "0x9791fdb4e9c0495efc5a1f3f9271ef226251eb34" }, "required": true + }, + { + "in": "query", + "name": "skip", + "description": "The number of records to skip for pagination", + "schema": { + "type": "string", + "default": "0", + "description": "The number of records to skip for pagination", + "example": 0 + } + }, + { + "in": "query", + "name": "take", + "description": "The number of records to return for pagination", + "schema": { + "type": "string", + "default": "12", + "description": "The number of records to return for pagination", + "example": 12 + } } ], "responses": { "200": { - "description": "The reward strategies and tokens found for the Operator.", + "description": "The list of queued and withdrawable withdrawals.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "address": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the AVS operator", - "example": "0xdbed88d83176316fc46797b43adee927dc2ff2f5" - }, - "rewardTokens": { + "data": { "type": "array", "items": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$" - }, - "description": "List of tokens in which the operator receives rewards", - "example": [ - "0xba50933c268f567bdc86e1ac131be072c6b0b71a", - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - ] + "type": "object", + "properties": { + "withdrawalRoot": { + "type": "string", + "description": "The root hash of the withdrawal", + "example": "0x9e6728ef0a8ad6009107a886047aae35bc5ed7deaa68580b0d1f8f67e3e5ed31" + }, + "nonce": { + "type": "number", + "description": "The nonce of the withdrawal, ensuring unique hashes for otherwise identical withdrawals", + "example": 0 + }, + "stakerAddress": { + "type": "string", + "description": "The contract address of the staker who initiated the withdrawal", + "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" + }, + "delegatedTo": { + "type": "string", + "description": "The address to which the staker was delegated when the withdrawal was initiated", + "example": "0x0000000000000000000000000000000000000000" + }, + "withdrawerAddress": { + "type": "string", + "description": "The address of the withdrawer, authorized to complete the withdrawal and receive the funds", + "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" + }, + "shares": { + "type": "array", + "items": { + "type": "object", + "properties": { + "strategyAddress": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the restaking strategy", + "example": "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" + }, + "shares": { + "type": "string", + "description": "The amount of shares held in the strategy", + "example": "1277920000000000000000000" + } + }, + "required": ["strategyAddress", "shares"] + }, + "description": "The list of strategy shares", + "example": [ + { + "strategyAddress": "0x93c4b944d05dfe6df7645a86cd2206016c51564d", + "shares": "1000288824523326631" + } + ] + }, + "createdAtBlock": { + "type": "number", + "description": "The block number when the withdrawal was recorded by EigenExplorer", + "example": 19912470 + }, + "createdAt": { + "type": "string", + "description": "The time stamp when the withdrawal was recorded by EigenExplorer", + "example": "2024-07-07T23:53:35.000Z" + } + }, + "required": [ + "withdrawalRoot", + "nonce", + "stakerAddress", + "delegatedTo", + "withdrawerAddress", + "shares", + "createdAtBlock", + "createdAt" + ] + } }, - "rewardStrategies": { - "type": "array", - "items": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$" + "meta": { + "type": "object", + "properties": { + "total": { + "type": "number", + "description": "Total number of records in the database", + "example": 30 + }, + "skip": { + "type": "number", + "description": "The number of skiped records for this query", + "example": 0 + }, + "take": { + "type": "number", + "description": "The number of records returned for this query", + "example": 12 + } }, - "description": "List of strategies for which the operator receives rewards", - "example": [ - "0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6", - "0x13760f50a9d7377e4f20cb8cf9e4c26586c658ff" - ] + "required": ["total", "skip", "take"] } }, - "required": ["address", "rewardTokens", "rewardStrategies"] + "required": ["data", "meta"] } } } @@ -6208,56 +8412,24 @@ } } }, - "/withdrawals": { + "/stakers/{address}/withdrawals/completed": { "get": { - "operationId": "getAllWithdrawals", - "summary": "Retrieve all withdrawals", - "description": "Returns all withdrawal data, including the withdrawal root, nonce, withdrawal status, and other relevant information.", - "tags": ["Withdrawals"], + "operationId": "getCompletedStakerWithdrawals", + "summary": "Retrieve completed withdrawals by staker address", + "description": "Returns all completed withdrawal data of the requested staker.", + "tags": ["Stakers"], "parameters": [ { - "in": "query", - "name": "stakerAddress", + "in": "path", + "name": "address", "description": "The address of the staker", "schema": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$", "description": "The address of the staker", - "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" - } - }, - { - "in": "query", - "name": "delegatedTo", - "description": "The address of the operator to which the stake is delegated", - "schema": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The address of the operator to which the stake is delegated", - "example": "0x5accc90436492f24e6af278569691e2c942a676d" - } - }, - { - "in": "query", - "name": "strategyAddress", - "description": "The contract address of the restaking strategy", - "schema": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the restaking strategy", - "example": "0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6" - } - }, - { - "in": "query", - "name": "status", - "description": "The status of the withdrawal", - "schema": { - "type": "string", - "enum": ["queued", "queued_withdrawable", "completed"], - "description": "The status of the withdrawal", - "example": "queued" - } + "example": "0x9791fdb4e9c0495efc5a1f3f9271ef226251eb34" + }, + "required": true }, { "in": "query", @@ -6284,7 +8456,7 @@ ], "responses": { "200": { - "description": "The list of withdrawals.", + "description": "The list of completed withdrawals.", "content": { "application/json": { "schema": { @@ -6302,22 +8474,22 @@ }, "nonce": { "type": "number", - "description": "The nonce of the withdrawal", + "description": "The nonce of the withdrawal, ensuring unique hashes for otherwise identical withdrawals", "example": 0 }, "stakerAddress": { "type": "string", - "description": "The address of the staker", + "description": "The contract address of the staker who initiated the withdrawal", "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" }, "delegatedTo": { "type": "string", - "description": "The operator address to which staking is delegated", + "description": "The address to which the staker was delegated when the withdrawal was initiated", "example": "0x0000000000000000000000000000000000000000" }, "withdrawerAddress": { "type": "string", - "description": "The address of the withdrawer", + "description": "The address of the withdrawer, authorized to complete the withdrawal and receive the funds", "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" }, "shares": { @@ -6366,11 +8538,6 @@ "type": "string", "description": "The time stamp when the withdrawal was last updated", "example": "2024-07-07T23:53:35.000Z" - }, - "isCompleted": { - "type": "boolean", - "description": "Indicates if the withdrawal is completed", - "example": false } }, "required": [ @@ -6383,8 +8550,7 @@ "createdAtBlock", "createdAt", "updatedAtBlock", - "updatedAt", - "isCompleted" + "updatedAt" ] } }, @@ -6439,124 +8605,131 @@ } } }, - "/withdrawals/{withdrawalRoot}": { + "/stakers/{address}/deposits": { "get": { - "operationId": "getWithdrawalByWithdrawalRoot", - "summary": "Retrieve withdrawal by withdrawal root", - "description": "Returns the withdrawal data by withdrawal root.", - "tags": ["Withdrawals"], + "operationId": "getStakerDeposits", + "summary": "Retrieve all deposits by staker address", + "description": "Returns all deposit data of the requested staker, including the transaction hash, token address, strategy address, shares and other relevant information.", + "tags": ["Stakers"], "parameters": [ { "in": "path", - "name": "withdrawalRoot", - "description": "The root hash of the withdrawal", + "name": "address", + "description": "The address of the staker", "schema": { "type": "string", - "description": "The root hash of the withdrawal", - "example": "0x9e6728ef0a8ad6009107a886047aae35bc5ed7deaa68580b0d1f8f67e3e5ed31" + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the staker", + "example": "0x9791fdb4e9c0495efc5a1f3f9271ef226251eb34" }, "required": true + }, + { + "in": "query", + "name": "skip", + "description": "The number of records to skip for pagination", + "schema": { + "type": "string", + "default": "0", + "description": "The number of records to skip for pagination", + "example": 0 + } + }, + { + "in": "query", + "name": "take", + "description": "The number of records to return for pagination", + "schema": { + "type": "string", + "default": "12", + "description": "The number of records to return for pagination", + "example": 12 + } } ], "responses": { "200": { - "description": "The requested withdrawal record.", + "description": "The list of deposits.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "withdrawalRoot": { - "type": "string", - "description": "The root hash of the withdrawal", - "example": "0x9e6728ef0a8ad6009107a886047aae35bc5ed7deaa68580b0d1f8f67e3e5ed31" - }, - "nonce": { - "type": "number", - "description": "The nonce of the withdrawal", - "example": 0 - }, - "stakerAddress": { - "type": "string", - "description": "The address of the staker", - "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" - }, - "delegatedTo": { - "type": "string", - "description": "The operator address to which staking is delegated", - "example": "0x0000000000000000000000000000000000000000" - }, - "withdrawerAddress": { - "type": "string", - "description": "The address of the withdrawer", - "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" - }, - "shares": { + "data": { "type": "array", "items": { "type": "object", "properties": { + "transactionHash": { + "type": "string", + "description": "The hash of the transaction", + "example": "0x9d0a355df5a937516dfaed6721b0b461a16b8fad005f66d7dbf56b8a39136297" + }, + "stakerAddress": { + "type": "string", + "description": "The address of the staker", + "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" + }, + "tokenAddress": { + "type": "string", + "description": "The address of the token", + "example": "0xe95a203b1a91a908f9b9ce46459d101078c2c3cb" + }, "strategyAddress": { "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", "description": "The contract address of the restaking strategy", - "example": "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" + "example": "0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6" }, "shares": { "type": "string", "description": "The amount of shares held in the strategy", - "example": "1277920000000000000000000" + "example": "40000000000000000" + }, + "createdAtBlock": { + "type": "number", + "description": "The block number when the withdrawal was recorded by EigenExplorer", + "example": 19912470 + }, + "createdAt": { + "type": "string", + "description": "The time stamp when the withdrawal was recorded by EigenExplorer", + "example": "2024-07-07T23:53:35.000Z" } }, - "required": ["strategyAddress", "shares"] - }, - "description": "The list of strategy shares", - "example": [ - { - "strategyAddress": "0x93c4b944d05dfe6df7645a86cd2206016c51564d", - "shares": "1000288824523326631" - } - ] - }, - "createdAtBlock": { - "type": "number", - "description": "The block number when the withdrawal was recorded by EigenExplorer", - "example": 19912470 - }, - "createdAt": { - "type": "string", - "description": "The time stamp when the withdrawal was recorded by EigenExplorer", - "example": "2024-07-07T23:53:35.000Z" - }, - "updatedAtBlock": { - "type": "number", - "description": "The block number when the withdrawal was last updated", - "example": 19912470 - }, - "updatedAt": { - "type": "string", - "description": "The time stamp when the withdrawal was last updated", - "example": "2024-07-07T23:53:35.000Z" + "required": [ + "transactionHash", + "stakerAddress", + "tokenAddress", + "strategyAddress", + "shares", + "createdAtBlock", + "createdAt" + ] + } }, - "isCompleted": { - "type": "boolean", - "description": "Indicates if the withdrawal is completed", - "example": false + "meta": { + "type": "object", + "properties": { + "total": { + "type": "number", + "description": "Total number of records in the database", + "example": 30 + }, + "skip": { + "type": "number", + "description": "The number of skiped records for this query", + "example": 0 + }, + "take": { + "type": "number", + "description": "The number of records returned for this query", + "example": 12 + } + }, + "required": ["total", "skip", "take"] } }, - "required": [ - "withdrawalRoot", - "nonce", - "stakerAddress", - "delegatedTo", - "withdrawerAddress", - "shares", - "createdAtBlock", - "createdAt", - "updatedAtBlock", - "updatedAt", - "isCompleted" - ] + "required": ["data", "meta"] } } } @@ -6585,33 +8758,105 @@ } } }, - "/stakers": { + "/stakers/{address}/events/delegation": { "get": { - "operationId": "getAllStakers", - "summary": "Retrieve all stakers", - "description": "Returns all staker records. This endpoint supports pagination.", + "operationId": "getStakerDelegationEvents", + "summary": "Retrieve all delegation events for a given staker address", + "description": "Returns a list of all delegation events for a given staker address.", "tags": ["Stakers"], "parameters": [ + { + "in": "path", + "name": "address", + "description": "The address of the staker", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the staker", + "example": "0x9791fdb4e9c0495efc5a1f3f9271ef226251eb34" + }, + "required": true + }, { "in": "query", - "name": "withTvl", - "description": "Toggle whether the route should calculate the TVL from shares", + "name": "txHash", + "description": "The transaction hash associated with the event", + "schema": { + "type": "string", + "pattern": "^0x([A-Fa-f0-9]{64})$", + "description": "The transaction hash associated with the event" + } + }, + { + "in": "query", + "name": "startAt", + "description": "Start date in ISO string format", + "schema": { + "type": "string", + "description": "Start date in ISO string format" + } + }, + { + "in": "query", + "name": "endAt", + "description": "End date in ISO string format", + "schema": { + "type": "string", + "description": "End date in ISO string format" + } + }, + { + "in": "query", + "name": "type", + "description": "The type of the delegation event", + "schema": { + "type": "string", + "enum": ["DELEGATION", "UNDELEGATION", "SHARES_INCREASED", "SHARES_DECREASED"], + "description": "The type of the delegation event" + } + }, + { + "in": "query", + "name": "strategyAddress", + "description": "The contract address of the restaking strategy", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the restaking strategy" + } + }, + { + "in": "query", + "name": "operatorAddress", + "description": "The address of the operator", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the operator" + } + }, + { + "in": "query", + "name": "withTokenData", + "description": "Toggle whether the route should return underlying token address and underlying value", "schema": { "type": "string", "enum": ["true", "false"], "default": "false", - "description": "Toggle whether the route should calculate the TVL from shares", + "description": "Toggle whether the route should return underlying token address and underlying value", "example": "false" } }, { "in": "query", - "name": "updatedSince", - "description": "Fetch stakers updated since this timestamp", + "name": "withEthValue", + "description": "Toggle whether the route should return value denominated in ETH", "schema": { "type": "string", - "description": "Fetch stakers updated since this timestamp", - "example": "2024-04-11T08:31:11.000Z" + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should return value denominated in ETH", + "example": "false" } }, { @@ -6628,189 +8873,87 @@ { "in": "query", "name": "take", - "description": "The number of records to return for pagination", - "schema": { - "type": "string", - "default": "12", - "description": "The number of records to return for pagination", - "example": 12 - } - } - ], - "responses": { - "200": { - "description": "The list of staker records.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "properties": { - "address": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the staker", - "example": "0x0000006c21964af0d420af8992851a30fa13a68b" - }, - "operatorAddress": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The address of the operator", - "example": "0x71c6f7ed8c2d4925d0baf16f6a85bb1736d412eb" - }, - "createdAtBlock": { - "type": "string", - "description": "The block number at which the Staker made first delegation", - "example": "19631203" - }, - "updatedAtBlock": { - "type": "string", - "description": "The block number at which the Staker made last delegation", - "example": "19631203" - }, - "createdAt": { - "type": "string", - "description": "The time stamp at which the Staker made first delegation", - "example": "2024-04-11T08:31:11.000Z" - }, - "updatedAt": { - "type": "string", - "description": "The time stamp at which the Staker made last delegation", - "example": "2024-04-11T08:31:11.000Z" - }, - "shares": { - "type": "array", - "items": { - "type": "object", - "properties": { - "strategyAddress": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the restaking strategy", - "example": "0x93c4b944d05dfe6df7645a86cd2206016c51564a" - }, - "shares": { - "type": "string", - "description": "The amount of shares held in the strategy", - "example": "40000000000000000" - } - }, - "required": ["strategyAddress", "shares"] - } - }, - "tvl": { - "type": "object", - "properties": { - "tvl": { - "type": "number", - "description": "The combined TVL of all restaking strategies in ETH", - "example": 1000000 - }, - "tvlBeaconChain": { - "type": "number", - "description": "The TVL of Beacon Chain restaking strategy in ETH", - "example": 1000000 - }, - "tvlRestaking": { - "type": "number", - "description": "The combined TVL of all liquid restaking strategies in ETH", - "example": 1000000 - }, - "tvlWETH": { - "type": "number", - "description": "The TVL of WETH restaking strategy in ETH", - "example": 1000000 - }, - "tvlStrategies": { - "type": "object", - "additionalProperties": { - "type": "number", - "description": "The total value locked (TVL) in the strategy, denominated in the strategy's native token", - "example": 1000000 - }, - "description": "The TVL of each individual restaking strategy in its native token", - "example": { - "Eigen": 1000000, - "cbETH": 2000000 - } - }, - "tvlStrategiesEth": { - "type": "object", - "additionalProperties": { - "type": "number", - "description": "The total value locked (TVL) in the strategy, denominated in ETH", - "example": 1000000 - }, - "description": "The TVL of each individual restaking strategy in ETH", - "example": { - "Eigen": 1000000, - "cbETH": 2000000 - } - } - }, - "required": [ - "tvl", - "tvlBeaconChain", - "tvlRestaking", - "tvlWETH", - "tvlStrategies", - "tvlStrategiesEth" - ], - "description": "The total value locked (TVL) in the AVS staker", - "example": { - "tvl": 1000000, - "tvlBeaconChain": 1000000, - "tvlWETH": 1000000, - "tvlRestaking": 1000000, - "tvlStrategies": { - "Eigen": 1000000, - "cbETH": 2000000 - }, - "tvlStrategiesEth": { - "stETH": 1000000, - "cbETH": 2000000 - } - } - } - }, - "required": [ - "address", - "operatorAddress", - "createdAtBlock", - "updatedAtBlock", - "createdAt", - "updatedAt", - "shares" - ] - } + "description": "The number of records to return for pagination", + "schema": { + "type": "string", + "default": "12", + "description": "The number of records to return for pagination", + "example": 12 + } + } + ], + "responses": { + "200": { + "description": "The delegation events found for the staker.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tx": { + "type": "string", + "description": "The transaction hash", + "example": "0xae41958d0342a4485536f701c72723625131680f182eb21f95abdac6d74d0ff0" }, - "meta": { + "blockNumber": { + "type": "number", + "description": "The block number of the transaction", + "example": 21045085 + }, + "blockTime": { + "type": "string", + "description": "The block time of the transaction as an ISO 8601 string", + "example": "2024-10-25T20:41:11.000Z" + }, + "type": { + "type": "string", + "enum": [ + "DELEGATION", + "UNDELEGATION", + "SHARES_INCREASED", + "SHARES_DECREASED" + ], + "description": "The type of the event", + "example": "DELEGATION" + }, + "args": { "type": "object", "properties": { - "total": { - "type": "number", - "description": "Total number of records in the database", - "example": 30 + "operator": { + "type": "string", + "description": "The contract address of the AVS operator", + "example": "0x71c6f7ed8c2d4925d0baf16f6a85bb1736d412eb" }, - "skip": { - "type": "number", - "description": "The number of skiped records for this query", - "example": 0 + "strategy": { + "type": "string", + "description": "The contract address of the restaking strategy", + "example": "0x93c4b944d05dfe6df7645a86cd2206016c51564d" }, - "take": { + "shares": { "type": "number", - "description": "The number of records returned for this query", - "example": 12 + "description": "The change in the operator's delegated shares, added or subtracted from the total.", + "example": 62816824424188010 } }, - "required": ["total", "skip", "take"] + "required": ["operator"] + }, + "underlyingToken": { + "type": "string", + "description": "The contract address of the token associated with this strategy", + "example": "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" + }, + "underlyingValue": { + "type": "number", + "description": "The value of the shares in terms of the underlying token", + "example": 5 + }, + "ethValue": { + "type": "number", + "description": "The value of the shares in ETH", + "example": 1 } }, - "required": ["data", "meta"] + "required": ["tx", "blockNumber", "blockTime", "type", "args"] } } } @@ -6839,11 +8982,11 @@ } } }, - "/stakers/{address}": { + "/stakers/{address}/events/deposit": { "get": { - "operationId": "getStakerByAddress", - "summary": "Retrieve a staker by address", - "description": "Returns a staker record by address.", + "operationId": "getStakerDepositEvents", + "summary": "Retrieve all deposit events for a given staker address", + "description": "Returns a list of all deposit events for a given staker address.", "tags": ["Stakers"], "parameters": [ { @@ -6860,161 +9003,166 @@ }, { "in": "query", - "name": "withTvl", - "description": "Toggle whether the route should calculate the TVL from shares", + "name": "txHash", + "description": "The transaction hash associated with the event", + "schema": { + "type": "string", + "pattern": "^0x([A-Fa-f0-9]{64})$", + "description": "The transaction hash associated with the event" + } + }, + { + "in": "query", + "name": "startAt", + "description": "Start date in ISO string format", + "schema": { + "type": "string", + "description": "Start date in ISO string format" + } + }, + { + "in": "query", + "name": "endAt", + "description": "End date in ISO string format", + "schema": { + "type": "string", + "description": "End date in ISO string format" + } + }, + { + "in": "query", + "name": "tokenAddress", + "description": "The contract address of the token", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the token" + } + }, + { + "in": "query", + "name": "strategyAddress", + "description": "The contract address of the restaking strategy", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the restaking strategy" + } + }, + { + "in": "query", + "name": "withTokenData", + "description": "Toggle whether the route should return underlying token address and underlying value", "schema": { "type": "string", "enum": ["true", "false"], "default": "false", - "description": "Toggle whether the route should calculate the TVL from shares", + "description": "Toggle whether the route should return underlying token address and underlying value", + "example": "false" + } + }, + { + "in": "query", + "name": "withEthValue", + "description": "Toggle whether the route should return value denominated in ETH", + "schema": { + "type": "string", + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should return value denominated in ETH", "example": "false" } + }, + { + "in": "query", + "name": "skip", + "description": "The number of records to skip for pagination", + "schema": { + "type": "string", + "default": "0", + "description": "The number of records to skip for pagination", + "example": 0 + } + }, + { + "in": "query", + "name": "take", + "description": "The number of records to return for pagination", + "schema": { + "type": "string", + "default": "12", + "description": "The number of records to return for pagination", + "example": 12 + } } ], "responses": { "200": { - "description": "The record of the requested operator.", + "description": "The deposit events found for the staker.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "address": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the staker", - "example": "0x0000006c21964af0d420af8992851a30fa13a68b" - }, - "operatorAddress": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The address of the operator", - "example": "0x71c6f7ed8c2d4925d0baf16f6a85bb1736d412eb" - }, - "createdAtBlock": { + "tx": { "type": "string", - "description": "The block number at which the Staker made first delegation", - "example": "19631203" + "description": "The transaction hash", + "example": "0xae41958d0342a4485536f701c72723625131680f182eb21f95abdac6d74d0ff0" }, - "updatedAtBlock": { - "type": "string", - "description": "The block number at which the Staker made last delegation", - "example": "19631203" + "blockNumber": { + "type": "number", + "description": "The block number of the transaction", + "example": 21045085 }, - "createdAt": { + "blockTime": { "type": "string", - "description": "The time stamp at which the Staker made first delegation", - "example": "2024-04-11T08:31:11.000Z" + "description": "The block time of the transaction as an ISO 8601 string", + "example": "2024-10-25T20:41:11.000Z" }, - "updatedAt": { + "type": { "type": "string", - "description": "The time stamp at which the Staker made last delegation", - "example": "2024-04-11T08:31:11.000Z" - }, - "shares": { - "type": "array", - "items": { - "type": "object", - "properties": { - "strategyAddress": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the restaking strategy", - "example": "0x93c4b944d05dfe6df7645a86cd2206016c51564a" - }, - "shares": { - "type": "string", - "description": "The amount of shares held in the strategy", - "example": "40000000000000000" - } - }, - "required": ["strategyAddress", "shares"] - } + "enum": ["DEPOSIT"], + "description": "The type of the event", + "example": "DEPOSIT" }, - "tvl": { + "args": { "type": "object", "properties": { - "tvl": { - "type": "number", - "description": "The combined TVL of all restaking strategies in ETH", - "example": 1000000 - }, - "tvlBeaconChain": { - "type": "number", - "description": "The TVL of Beacon Chain restaking strategy in ETH", - "example": 1000000 + "token": { + "type": "string", + "description": "The contract address of the token deposited", + "example": "0xec53bf9167f50cdeb3ae105f56099aaab9061f83" }, - "tvlRestaking": { - "type": "number", - "description": "The combined TVL of all liquid restaking strategies in ETH", - "example": 1000000 + "strategy": { + "type": "string", + "description": "The contract address of the restaking strategy", + "example": "0xacb55c530acdb2849e6d4f36992cd8c9d50ed8f7" }, - "tvlWETH": { + "shares": { "type": "number", - "description": "The TVL of WETH restaking strategy in ETH", - "example": 1000000 - }, - "tvlStrategies": { - "type": "object", - "additionalProperties": { - "type": "number", - "description": "The total value locked (TVL) in the strategy, denominated in the strategy's native token", - "example": 1000000 - }, - "description": "The TVL of each individual restaking strategy in its native token", - "example": { - "Eigen": 1000000, - "cbETH": 2000000 - } - }, - "tvlStrategiesEth": { - "type": "object", - "additionalProperties": { - "type": "number", - "description": "The total value locked (TVL) in the strategy, denominated in ETH", - "example": 1000000 - }, - "description": "The TVL of each individual restaking strategy in ETH", - "example": { - "Eigen": 1000000, - "cbETH": 2000000 - } + "description": "The amount of new shares given to the staker in this strategy", + "example": 10190000000000000000 } }, - "required": [ - "tvl", - "tvlBeaconChain", - "tvlRestaking", - "tvlWETH", - "tvlStrategies", - "tvlStrategiesEth" - ], - "description": "The total value locked (TVL) in the AVS staker", - "example": { - "tvl": 1000000, - "tvlBeaconChain": 1000000, - "tvlWETH": 1000000, - "tvlRestaking": 1000000, - "tvlStrategies": { - "Eigen": 1000000, - "cbETH": 2000000 - }, - "tvlStrategiesEth": { - "stETH": 1000000, - "cbETH": 2000000 - } - } + "required": ["token", "strategy", "shares"] + }, + "underlyingToken": { + "type": "string", + "description": "The contract address of the token associated with this strategy", + "example": "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" + }, + "underlyingValue": { + "type": "number", + "description": "The value of the shares in terms of the underlying token", + "example": 5 + }, + "ethValue": { + "type": "number", + "description": "The value of the shares in ETH", + "example": 1 } - }, - "required": [ - "address", - "operatorAddress", - "createdAtBlock", - "updatedAtBlock", - "createdAt", - "updatedAt", - "shares" - ] + }, + "required": ["tx", "blockNumber", "blockTime", "type", "args"] } } } @@ -7043,11 +9191,11 @@ } } }, - "/stakers/{address}/withdrawals": { + "/stakers/{address}/events/withdrawal": { "get": { - "operationId": "getStakerWithdrawals", - "summary": "Retrieve all withdrawals by staker address", - "description": "Returns all withdrawal data of the requested staker, including the withdrawal root, nonce, withdrawal status, and other relevant information.", + "operationId": "getStakerWithdrawalEvents", + "summary": "Retrieve all withdrawal events for a given staker address", + "description": "Returns a list of all withdrawal events for a given staker address.", "tags": ["Stakers"], "parameters": [ { @@ -7062,6 +9210,98 @@ }, "required": true }, + { + "in": "query", + "name": "txHash", + "description": "The transaction hash associated with the event", + "schema": { + "type": "string", + "pattern": "^0x([A-Fa-f0-9]{64})$", + "description": "The transaction hash associated with the event" + } + }, + { + "in": "query", + "name": "startAt", + "description": "Start date in ISO string format", + "schema": { + "type": "string", + "description": "Start date in ISO string format" + } + }, + { + "in": "query", + "name": "endAt", + "description": "End date in ISO string format", + "schema": { + "type": "string", + "description": "End date in ISO string format" + } + }, + { + "in": "query", + "name": "type", + "description": "The type of the withdrawal event", + "schema": { + "type": "string", + "enum": ["WITHDRAWAL_QUEUED", "WITHDRAWAL_COMPLETED"], + "description": "The type of the withdrawal event" + } + }, + { + "in": "query", + "name": "withdrawalRoot", + "description": "The withdrawal root associated with the event", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{64}$", + "description": "The withdrawal root associated with the event" + } + }, + { + "in": "query", + "name": "delegatedTo", + "description": "The address to which funds were delegated", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address to which funds were delegated" + } + }, + { + "in": "query", + "name": "withdrawer", + "description": "The address of the withdrawer", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the withdrawer" + } + }, + { + "in": "query", + "name": "withTokenData", + "description": "Toggle whether the route should return underlying token address and underlying value", + "schema": { + "type": "string", + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should return underlying token address and underlying value", + "example": "false" + } + }, + { + "in": "query", + "name": "withEthValue", + "description": "Toggle whether the route should return value denominated in ETH", + "schema": { + "type": "string", + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should return value denominated in ETH", + "example": "false" + } + }, { "in": "query", "name": "skip", @@ -7087,133 +9327,100 @@ ], "responses": { "200": { - "description": "The list of withdrawals.", + "description": "The withdrawal events found for the staker.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "properties": { - "withdrawalRoot": { - "type": "string", - "description": "The root hash of the withdrawal", - "example": "0x9e6728ef0a8ad6009107a886047aae35bc5ed7deaa68580b0d1f8f67e3e5ed31" - }, - "nonce": { - "type": "number", - "description": "The nonce of the withdrawal", - "example": 0 - }, - "stakerAddress": { - "type": "string", - "description": "The address of the staker", - "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" - }, - "delegatedTo": { - "type": "string", - "description": "The operator address to which staking is delegated", - "example": "0x0000000000000000000000000000000000000000" - }, - "withdrawerAddress": { - "type": "string", - "description": "The address of the withdrawer", - "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" - }, - "shares": { - "type": "array", - "items": { - "type": "object", - "properties": { - "strategyAddress": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the restaking strategy", - "example": "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" - }, - "shares": { - "type": "string", - "description": "The amount of shares held in the strategy", - "example": "1277920000000000000000000" - } - }, - "required": ["strategyAddress", "shares"] - }, - "description": "The list of strategy shares", - "example": [ - { - "strategyAddress": "0x93c4b944d05dfe6df7645a86cd2206016c51564d", - "shares": "1000288824523326631" - } - ] - }, - "createdAtBlock": { - "type": "number", - "description": "The block number when the withdrawal was recorded by EigenExplorer", - "example": 19912470 - }, - "createdAt": { - "type": "string", - "description": "The time stamp when the withdrawal was recorded by EigenExplorer", - "example": "2024-07-07T23:53:35.000Z" - }, - "updatedAtBlock": { - "type": "number", - "description": "The block number when the withdrawal was last updated", - "example": 19912470 - }, - "updatedAt": { - "type": "string", - "description": "The time stamp when the withdrawal was last updated", - "example": "2024-07-07T23:53:35.000Z" - }, - "isCompleted": { - "type": "boolean", - "description": "Indicates if the withdrawal is completed", - "example": false - } - }, - "required": [ - "withdrawalRoot", - "nonce", - "stakerAddress", - "delegatedTo", - "withdrawerAddress", - "shares", - "createdAtBlock", - "createdAt", - "updatedAtBlock", - "updatedAt", - "isCompleted" - ] - } + "tx": { + "type": "string", + "description": "The transaction hash", + "example": "0xae41958d0342a4485536f701c72723625131680f182eb21f95abdac6d74d0ff0" }, - "meta": { + "blockNumber": { + "type": "number", + "description": "The block number of the transaction", + "example": 21045085 + }, + "blockTime": { + "type": "string", + "description": "The block time of the transaction as an ISO 8601 string", + "example": "2024-10-25T20:41:11.000Z" + }, + "type": { + "type": "string", + "enum": ["WITHDRAWAL_QUEUED", "WITHDRAWAL_COMPLETED"], + "description": "The type of the event", + "example": "WITHDRAWAL_QUEUED" + }, + "args": { "type": "object", "properties": { - "total": { - "type": "number", - "description": "Total number of records in the database", - "example": 30 + "withdrawalRoot": { + "type": "string", + "description": "The root hash of the withdrawal", + "example": "0xe6cdf9110330e1648039cb98e680aeb9d1c63e022764186f1131eb9432605421" }, - "skip": { + "delegatedTo": { + "type": "string", + "description": "The address to which the staker was delegated when the withdrawal was initiated", + "example": "0x4cd2086e1d708e65db5d4f5712a9ca46ed4bbd0a" + }, + "withdrawer": { + "type": "string", + "description": "The address of the withdrawer, authorized to complete the withdrawal and receive the funds", + "example": "0x513ea5a99988252f3b2cd8382ac077d7fd26ef48" + }, + "nonce": { "type": "number", - "description": "The number of skiped records for this query", + "description": "The nonce of the withdrawal, ensuring unique hashes for otherwise identical withdrawals", "example": 0 }, - "take": { + "startBlock": { "type": "number", - "description": "The number of records returned for this query", - "example": 12 + "description": "The block number when the withdrawal was created", + "example": 21054925 + }, + "strategies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "strategy": { + "type": "string", + "description": "The contract address of the restaking strategy", + "example": "0xacb55c530acdb2849e6d4f36992cd8c9d50ed8f7" + }, + "shares": { + "type": "string", + "description": "The amount of shares withdrawn for each strategy", + "example": "1000000000000000000" + }, + "underlyingToken": { + "type": "string", + "description": "The contract address of the token associated with this strategy", + "example": "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" + }, + "underlyingValue": { + "type": "number", + "description": "The value of the shares in terms of the underlying token", + "example": 5 + }, + "ethValue": { + "type": "number", + "description": "The value of the shares in ETH", + "example": 1 + } + }, + "required": ["strategy", "shares"] + } } }, - "required": ["total", "skip", "take"] + "required": ["withdrawalRoot"] } }, - "required": ["data", "meta"] + "required": ["tx", "blockNumber", "blockTime", "type", "args"] } } } @@ -7242,51 +9449,72 @@ } } }, - "/stakers/{address}/withdrawals/queued": { + "/deposits": { "get": { - "operationId": "getQueuedStakerWithdrawals", - "summary": "Retrieve queued withdrawals by staker address", - "description": "Returns all queued withdrawal data of the requested staker.", - "tags": ["Stakers"], + "operationId": "getAllDeposits", + "summary": "Retrieve all deposits", + "description": "Returns all deposit data, including the transaction hash, token address, and other relevant information.", + "tags": ["Deposits"], "parameters": [ { - "in": "path", - "name": "address", + "in": "query", + "name": "skip", + "description": "The number of records to skip for pagination", + "schema": { + "type": "string", + "default": "0", + "description": "The number of records to skip for pagination", + "example": 0 + } + }, + { + "in": "query", + "name": "take", + "description": "The number of records to return for pagination", + "schema": { + "type": "string", + "default": "12", + "description": "The number of records to return for pagination", + "example": 12 + } + }, + { + "in": "query", + "name": "stakerAddress", "description": "The address of the staker", "schema": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$", "description": "The address of the staker", - "example": "0x9791fdb4e9c0495efc5a1f3f9271ef226251eb34" - }, - "required": true + "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" + } }, { "in": "query", - "name": "skip", - "description": "The number of records to skip for pagination", + "name": "tokenAddress", + "description": "The address of the token deposited", "schema": { "type": "string", - "default": "0", - "description": "The number of records to skip for pagination", - "example": 0 + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the token deposited", + "example": "0xe95a203b1a91a908f9b9ce46459d101078c2c3cb" } }, { "in": "query", - "name": "take", - "description": "The number of records to return for pagination", + "name": "strategyAddress", + "description": "The contract address of the restaking strategy", "schema": { "type": "string", - "default": "12", - "description": "The number of records to return for pagination", - "example": 12 + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the restaking strategy", + "example": "0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6" } } ], "responses": { "200": { - "description": "The list of queued withdrawals.", + "description": "The list of deposits.", "content": { "application/json": { "schema": { @@ -7297,57 +9525,30 @@ "items": { "type": "object", "properties": { - "withdrawalRoot": { + "transactionHash": { "type": "string", - "description": "The root hash of the withdrawal", - "example": "0x9e6728ef0a8ad6009107a886047aae35bc5ed7deaa68580b0d1f8f67e3e5ed31" - }, - "nonce": { - "type": "number", - "description": "The nonce of the withdrawal", - "example": 0 + "description": "The hash of the transaction", + "example": "0x9d0a355df5a937516dfaed6721b0b461a16b8fad005f66d7dbf56b8a39136297" }, "stakerAddress": { "type": "string", "description": "The address of the staker", "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" }, - "delegatedTo": { + "tokenAddress": { "type": "string", - "description": "The operator address to which staking is delegated", - "example": "0x0000000000000000000000000000000000000000" + "description": "The address of the token", + "example": "0xe95a203b1a91a908f9b9ce46459d101078c2c3cb" }, - "withdrawerAddress": { + "strategyAddress": { "type": "string", - "description": "The address of the withdrawer", - "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" + "description": "The contract address of the restaking strategy", + "example": "0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6" }, "shares": { - "type": "array", - "items": { - "type": "object", - "properties": { - "strategyAddress": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the restaking strategy", - "example": "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" - }, - "shares": { - "type": "string", - "description": "The amount of shares held in the strategy", - "example": "1277920000000000000000000" - } - }, - "required": ["strategyAddress", "shares"] - }, - "description": "The list of strategy shares", - "example": [ - { - "strategyAddress": "0x93c4b944d05dfe6df7645a86cd2206016c51564d", - "shares": "1000288824523326631" - } - ] + "type": "string", + "description": "The amount of shares held in the strategy", + "example": "40000000000000000" }, "createdAtBlock": { "type": "number", @@ -7361,11 +9562,10 @@ } }, "required": [ - "withdrawalRoot", - "nonce", + "transactionHash", "stakerAddress", - "delegatedTo", - "withdrawerAddress", + "tokenAddress", + "strategyAddress", "shares", "createdAtBlock", "createdAt" @@ -7423,159 +9623,259 @@ } } }, - "/stakers/{address}/withdrawals/queued_withdrawable": { + "/rewards/strategies": { "get": { - "operationId": "getQueuedWithdrawableStakerWithdrawals", - "summary": "Retrieve queued and withdrawable withdrawals by staker address", - "description": "Returns all queued and withdrawable withdrawal data of the requested staker.", - "tags": ["Stakers"], - "parameters": [ - { - "in": "path", - "name": "address", - "description": "The address of the staker", - "schema": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The address of the staker", - "example": "0x9791fdb4e9c0495efc5a1f3f9271ef226251eb34" - }, - "required": true - }, - { - "in": "query", - "name": "skip", - "description": "The number of records to skip for pagination", - "schema": { - "type": "string", - "default": "0", - "description": "The number of records to skip for pagination", - "example": 0 - } - }, - { - "in": "query", - "name": "take", - "description": "The number of records to return for pagination", - "schema": { - "type": "string", - "default": "12", - "description": "The number of records to return for pagination", - "example": 12 - } - } - ], + "operationId": "getStrategies", + "summary": "Retrieve all strategies with their reward tokens", + "description": "Returns a list of strategies with their corresponding reward tokens, including strategy addresses and associated token addresses.", + "tags": ["Rewards"], "responses": { "200": { - "description": "The list of queued and withdrawable withdrawals.", + "description": "List of strategies along with associated reward tokens.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "data": { + "strategies": { "type": "array", "items": { "type": "object", "properties": { - "withdrawalRoot": { - "type": "string", - "description": "The root hash of the withdrawal", - "example": "0x9e6728ef0a8ad6009107a886047aae35bc5ed7deaa68580b0d1f8f67e3e5ed31" - }, - "nonce": { - "type": "number", - "description": "The nonce of the withdrawal", - "example": 0 - }, - "stakerAddress": { - "type": "string", - "description": "The address of the staker", - "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" - }, - "delegatedTo": { - "type": "string", - "description": "The operator address to which staking is delegated", - "example": "0x0000000000000000000000000000000000000000" - }, - "withdrawerAddress": { + "strategyAddress": { "type": "string", - "description": "The address of the withdrawer", - "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" + "description": "The contract address of the restaking strategy", + "example": "0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6" }, - "shares": { + "tokens": { "type": "array", "items": { - "type": "object", - "properties": { - "strategyAddress": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the restaking strategy", - "example": "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" - }, - "shares": { - "type": "string", - "description": "The amount of shares held in the strategy", - "example": "1277920000000000000000000" - } - }, - "required": ["strategyAddress", "shares"] + "type": "string" }, - "description": "The list of strategy shares", + "description": "List of reward token addresses associated with the strategy", "example": [ - { - "strategyAddress": "0x93c4b944d05dfe6df7645a86cd2206016c51564d", - "shares": "1000288824523326631" - } + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0xba50933c268f567bdc86e1ac131be072c6b0b71a" ] - }, - "createdAtBlock": { - "type": "number", - "description": "The block number when the withdrawal was recorded by EigenExplorer", - "example": 19912470 - }, - "createdAt": { - "type": "string", - "description": "The time stamp when the withdrawal was recorded by EigenExplorer", - "example": "2024-07-07T23:53:35.000Z" } }, - "required": [ - "withdrawalRoot", - "nonce", - "stakerAddress", - "delegatedTo", - "withdrawerAddress", - "shares", - "createdAtBlock", - "createdAt" - ] + "required": ["strategyAddress", "tokens"] } }, - "meta": { + "total": { + "type": "number", + "description": "The total number of strategies", + "example": 15 + } + }, + "required": ["strategies", "total"] + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "422": { + "$ref": "#/components/responses/422" + }, + "429": { + "$ref": "#/components/responses/429" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, + "/events/delegation": { + "get": { + "operationId": "getDelegationEvents", + "summary": "Retrieve all delegation events", + "description": "Returns a list of all delegation events.", + "tags": ["Events"], + "parameters": [ + { + "in": "query", + "name": "txHash", + "description": "The transaction hash associated with the event", + "schema": { + "type": "string", + "pattern": "^0x([A-Fa-f0-9]{64})$", + "description": "The transaction hash associated with the event" + } + }, + { + "in": "query", + "name": "startAt", + "description": "Start date in ISO string format", + "schema": { + "type": "string", + "description": "Start date in ISO string format" + } + }, + { + "in": "query", + "name": "endAt", + "description": "End date in ISO string format", + "schema": { + "type": "string", + "description": "End date in ISO string format" + } + }, + { + "in": "query", + "name": "type", + "description": "The type of the delegation event", + "schema": { + "type": "string", + "enum": ["DELEGATION", "UNDELEGATION", "SHARES_INCREASED", "SHARES_DECREASED"], + "description": "The type of the delegation event" + } + }, + { + "in": "query", + "name": "strategyAddress", + "description": "The contract address of the restaking strategy", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the restaking strategy" + } + }, + { + "in": "query", + "name": "withTokenData", + "description": "Toggle whether the route should return underlying token address and underlying value", + "schema": { + "type": "string", + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should return underlying token address and underlying value", + "example": "false" + } + }, + { + "in": "query", + "name": "withEthValue", + "description": "Toggle whether the route should return value denominated in ETH", + "schema": { + "type": "string", + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should return value denominated in ETH", + "example": "false" + } + }, + { + "in": "query", + "name": "skip", + "description": "The number of records to skip for pagination", + "schema": { + "type": "string", + "default": "0", + "description": "The number of records to skip for pagination", + "example": 0 + } + }, + { + "in": "query", + "name": "take", + "description": "The number of records to return for pagination", + "schema": { + "type": "string", + "default": "12", + "description": "The number of records to return for pagination", + "example": 12 + } + } + ], + "responses": { + "200": { + "description": "The delegation events found.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tx": { + "type": "string", + "description": "The transaction hash", + "example": "0xae41958d0342a4485536f701c72723625131680f182eb21f95abdac6d74d0ff0" + }, + "blockNumber": { + "type": "number", + "description": "The block number of the transaction", + "example": 21045085 + }, + "blockTime": { + "type": "string", + "description": "The block time of the transaction as an ISO 8601 string", + "example": "2024-10-25T20:41:11.000Z" + }, + "type": { + "type": "string", + "enum": [ + "DELEGATION", + "UNDELEGATION", + "SHARES_INCREASED", + "SHARES_DECREASED" + ], + "description": "The type of the event", + "example": "DELEGATION" + }, + "args": { "type": "object", "properties": { - "total": { - "type": "number", - "description": "Total number of records in the database", - "example": 30 + "operator": { + "type": "string", + "description": "The contract address of the AVS operator", + "example": "0x71c6f7ed8c2d4925d0baf16f6a85bb1736d412eb" }, - "skip": { - "type": "number", - "description": "The number of skiped records for this query", - "example": 0 + "staker": { + "type": "string", + "description": "The contract address of the staker", + "example": "0x42318adf0773b8af4aa8ed1670ea0af7761d07c7" }, - "take": { + "strategy": { + "type": "string", + "description": "The contract address of the restaking strategy", + "example": "0x93c4b944d05dfe6df7645a86cd2206016c51564d" + }, + "shares": { "type": "number", - "description": "The number of records returned for this query", - "example": 12 + "description": "The change in the operator's delegated shares, added or subtracted from the total.", + "example": 62816824424188010 } }, - "required": ["total", "skip", "take"] + "required": ["operator", "staker"] + }, + "underlyingToken": { + "type": "string", + "description": "The contract address of the token associated with this strategy", + "example": "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" + }, + "underlyingValue": { + "type": "number", + "description": "The value of the shares in terms of the underlying token", + "example": 5 + }, + "ethValue": { + "type": "number", + "description": "The value of the shares in ETH", + "example": 1 } }, - "required": ["data", "meta"] + "required": ["tx", "blockNumber", "blockTime", "type", "args"] } } } @@ -7604,171 +9904,224 @@ } } }, - "/stakers/{address}/withdrawals/completed": { + "/events/rewards": { "get": { - "operationId": "getCompletedStakerWithdrawals", - "summary": "Retrieve completed withdrawals by staker address", - "description": "Returns all completed withdrawal data of the requested staker.", - "tags": ["Stakers"], + "operationId": "getRewardsEvents", + "summary": "Retrieve all reward events", + "description": "Returns a list of all reward events.", + "tags": ["Events"], "parameters": [ { - "in": "path", - "name": "address", - "description": "The address of the staker", + "in": "query", + "name": "txHash", + "description": "The transaction hash associated with the event", + "schema": { + "type": "string", + "pattern": "^0x([A-Fa-f0-9]{64})$", + "description": "The transaction hash associated with the event" + } + }, + { + "in": "query", + "name": "startAt", + "description": "Start date in ISO string format", + "schema": { + "type": "string", + "description": "Start date in ISO string format" + } + }, + { + "in": "query", + "name": "endAt", + "description": "End date in ISO string format", + "schema": { + "type": "string", + "description": "End date in ISO string format" + } + }, + { + "in": "query", + "name": "rewardsSubmissionHash", + "description": "The reward submission hash associated with the event", + "schema": { + "type": "string", + "pattern": "^0x([A-Fa-f0-9]{64})$", + "description": "The reward submission hash associated with the event" + } + }, + { + "in": "query", + "name": "rewardsSubmissionToken", + "description": "The token address used for the rewards submission", "schema": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The address of the staker", - "example": "0x9791fdb4e9c0495efc5a1f3f9271ef226251eb34" - }, - "required": true + "description": "The token address used for the rewards submission" + } }, { "in": "query", - "name": "skip", - "description": "The number of records to skip for pagination", + "name": "withIndividualAmount", + "description": "Toggle whether the route should return individual share amount for each strategy", "schema": { "type": "string", - "default": "0", - "description": "The number of records to skip for pagination", - "example": 0 + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should return individual share amount for each strategy", + "example": "false" } }, { "in": "query", - "name": "take", - "description": "The number of records to return for pagination", + "name": "withEthValue", + "description": "Toggle whether the route should return value denominated in ETH", "schema": { "type": "string", - "default": "12", - "description": "The number of records to return for pagination", - "example": 12 + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should return value denominated in ETH", + "example": "false" } - } - ], - "responses": { - "200": { - "description": "The list of completed withdrawals.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "properties": { - "withdrawalRoot": { - "type": "string", - "description": "The root hash of the withdrawal", - "example": "0x9e6728ef0a8ad6009107a886047aae35bc5ed7deaa68580b0d1f8f67e3e5ed31" - }, - "nonce": { - "type": "number", - "description": "The nonce of the withdrawal", - "example": 0 - }, - "stakerAddress": { - "type": "string", - "description": "The address of the staker", - "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" - }, - "delegatedTo": { - "type": "string", - "description": "The operator address to which staking is delegated", - "example": "0x0000000000000000000000000000000000000000" - }, - "withdrawerAddress": { - "type": "string", - "description": "The address of the withdrawer", - "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" - }, - "shares": { - "type": "array", - "items": { - "type": "object", - "properties": { - "strategyAddress": { - "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the restaking strategy", - "example": "0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0" - }, - "shares": { - "type": "string", - "description": "The amount of shares held in the strategy", - "example": "1277920000000000000000000" - } - }, - "required": ["strategyAddress", "shares"] - }, - "description": "The list of strategy shares", - "example": [ - { - "strategyAddress": "0x93c4b944d05dfe6df7645a86cd2206016c51564d", - "shares": "1000288824523326631" - } - ] - }, - "createdAtBlock": { - "type": "number", - "description": "The block number when the withdrawal was recorded by EigenExplorer", - "example": 19912470 - }, - "createdAt": { - "type": "string", - "description": "The time stamp when the withdrawal was recorded by EigenExplorer", - "example": "2024-07-07T23:53:35.000Z" - }, - "updatedAtBlock": { - "type": "number", - "description": "The block number when the withdrawal was last updated", - "example": 19912470 - }, - "updatedAt": { - "type": "string", - "description": "The time stamp when the withdrawal was last updated", - "example": "2024-07-07T23:53:35.000Z" - } - }, - "required": [ - "withdrawalRoot", - "nonce", - "stakerAddress", - "delegatedTo", - "withdrawerAddress", - "shares", - "createdAtBlock", - "createdAt", - "updatedAtBlock", - "updatedAt" - ] - } + }, + { + "in": "query", + "name": "skip", + "description": "The number of records to skip for pagination", + "schema": { + "type": "string", + "default": "0", + "description": "The number of records to skip for pagination", + "example": 0 + } + }, + { + "in": "query", + "name": "take", + "description": "The number of records to return for pagination", + "schema": { + "type": "string", + "default": "12", + "description": "The number of records to return for pagination", + "example": 12 + } + } + ], + "responses": { + "200": { + "description": "The reward events found.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tx": { + "type": "string", + "description": "The transaction hash", + "example": "0xae41958d0342a4485536f701c72723625131680f182eb21f95abdac6d74d0ff0" }, - "meta": { + "blockNumber": { + "type": "number", + "description": "The block number of the transaction", + "example": 21045085 + }, + "blockTime": { + "type": "string", + "description": "The block time of the transaction as an ISO 8601 string", + "example": "2024-10-25T20:41:11.000Z" + }, + "type": { + "type": "string", + "enum": ["REWARDS"], + "description": "The type of the event", + "example": "REWARDS" + }, + "args": { "type": "object", "properties": { - "total": { + "avs": { + "type": "string", + "description": "AVS service manager contract address", + "example": "0x1de75eaab2df55d467494a172652579e6fa4540e" + }, + "submissionNonce": { "type": "number", - "description": "Total number of records in the database", - "example": 30 + "description": "The nonce of the rewards submission", + "example": 2 }, - "skip": { + "rewardsSubmissionHash": { + "type": "string", + "description": "The hash of the rewards submission", + "example": "0x1e391c015c923972811a27e1c6c3a874511e47033f1022021f29967a60ab2c87" + }, + "rewardsSubmissionToken": { + "type": "string", + "description": "The contract address of the token used for rewards distribution", + "example": "0xba50933c268f567bdc86e1ac131be072c6b0b71a" + }, + "rewardsSubmissionAmount": { + "type": "string", + "description": "The total amount of rewards allocated in this submission", + "example": "49000000000000000000000" + }, + "rewardsSubmissionStartTimeStamp": { "type": "number", - "description": "The number of skiped records for this query", - "example": 0 + "description": "The timestamp marking the start of this rewards distribution period", + "example": 1728518400 }, - "take": { + "rewardsSubmissionDuration": { "type": "number", - "description": "The number of records returned for this query", - "example": 12 + "description": "The duration (in seconds) over which the rewards are distributed", + "example": 6048000 + }, + "strategies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "strategy": { + "type": "string", + "description": "The contract address of the restaking strategy", + "example": "0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6" + }, + "multiplier": { + "type": "string", + "description": "The multiplier associated with this strategy", + "example": "1068966896363604679" + }, + "amount": { + "type": "string", + "description": "The amount of rewards allocated to this strategy from the total rewards in this submissionn", + "example": "3.7932452554246293e+21" + }, + "amountEthValue": { + "type": "number", + "description": "The value of the rewards amount allocated to this strategy in ETH", + "example": 0.0638779707245759 + } + }, + "required": ["strategy", "multiplier"] + }, + "description": "List of strategies involved in the rewards submission" } }, - "required": ["total", "skip", "take"] + "required": [ + "avs", + "submissionNonce", + "rewardsSubmissionHash", + "rewardsSubmissionToken", + "rewardsSubmissionAmount", + "rewardsSubmissionStartTimeStamp", + "rewardsSubmissionDuration", + "strategies" + ] + }, + "ethValue": { + "type": "number", + "description": "The value of the shares in ETH", + "example": 1 } }, - "required": ["data", "meta"] + "required": ["tx", "blockNumber", "blockTime", "type", "args"] } } } @@ -7797,24 +10150,84 @@ } } }, - "/stakers/{address}/deposits": { + "/events/deposit": { "get": { - "operationId": "getStakerDeposits", - "summary": "Retrieve all deposits by staker address", - "description": "Returns all deposit data of the requested staker, including the transaction hash, token address, strategy address, shares and other relevant information.", - "tags": ["Stakers"], + "operationId": "getDepositEvents", + "summary": "Retrieve all deposit events", + "description": "Returns a list of all deposit events.", + "tags": ["Events"], "parameters": [ { - "in": "path", - "name": "address", - "description": "The address of the staker", + "in": "query", + "name": "txHash", + "description": "The transaction hash associated with the event", + "schema": { + "type": "string", + "pattern": "^0x([A-Fa-f0-9]{64})$", + "description": "The transaction hash associated with the event" + } + }, + { + "in": "query", + "name": "startAt", + "description": "Start date in ISO string format", + "schema": { + "type": "string", + "description": "Start date in ISO string format" + } + }, + { + "in": "query", + "name": "endAt", + "description": "End date in ISO string format", + "schema": { + "type": "string", + "description": "End date in ISO string format" + } + }, + { + "in": "query", + "name": "tokenAddress", + "description": "The contract address of the token", "schema": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The address of the staker", - "example": "0x9791fdb4e9c0495efc5a1f3f9271ef226251eb34" - }, - "required": true + "description": "The contract address of the token" + } + }, + { + "in": "query", + "name": "strategyAddress", + "description": "The contract address of the restaking strategy", + "schema": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The contract address of the restaking strategy" + } + }, + { + "in": "query", + "name": "withTokenData", + "description": "Toggle whether the route should return underlying token address and underlying value", + "schema": { + "type": "string", + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should return underlying token address and underlying value", + "example": "false" + } + }, + { + "in": "query", + "name": "withEthValue", + "description": "Toggle whether the route should return value denominated in ETH", + "schema": { + "type": "string", + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should return value denominated in ETH", + "example": "false" + } }, { "in": "query", @@ -7841,87 +10254,76 @@ ], "responses": { "200": { - "description": "The list of deposits.", + "description": "The deposit events found.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "properties": { - "transactionHash": { - "type": "string", - "description": "The hash of the transaction", - "example": "0x9d0a355df5a937516dfaed6721b0b461a16b8fad005f66d7dbf56b8a39136297" - }, - "stakerAddress": { - "type": "string", - "description": "The address of the staker", - "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" - }, - "tokenAddress": { - "type": "string", - "description": "The address of the token", - "example": "0xe95a203b1a91a908f9b9ce46459d101078c2c3cb" - }, - "strategyAddress": { - "type": "string", - "description": "The contract address of the restaking strategy", - "example": "0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6" - }, - "shares": { - "type": "string", - "description": "The amount of shares held in the strategy", - "example": "40000000000000000" - }, - "createdAtBlock": { - "type": "number", - "description": "The block number when the withdrawal was recorded by EigenExplorer", - "example": 19912470 - }, - "createdAt": { - "type": "string", - "description": "The time stamp when the withdrawal was recorded by EigenExplorer", - "example": "2024-07-07T23:53:35.000Z" - } - }, - "required": [ - "transactionHash", - "stakerAddress", - "tokenAddress", - "strategyAddress", - "shares", - "createdAtBlock", - "createdAt" - ] - } + "tx": { + "type": "string", + "description": "The transaction hash", + "example": "0xae41958d0342a4485536f701c72723625131680f182eb21f95abdac6d74d0ff0" }, - "meta": { + "blockNumber": { + "type": "number", + "description": "The block number of the transaction", + "example": 21045085 + }, + "blockTime": { + "type": "string", + "description": "The block time of the transaction as an ISO 8601 string", + "example": "2024-10-25T20:41:11.000Z" + }, + "type": { + "type": "string", + "enum": ["DEPOSIT"], + "description": "The type of the event", + "example": "DEPOSIT" + }, + "args": { "type": "object", "properties": { - "total": { - "type": "number", - "description": "Total number of records in the database", - "example": 30 + "staker": { + "type": "string", + "description": "The contract address of the staker", + "example": "0xa0e32344405b2097e738718dc27d2c2daf73e706" }, - "skip": { - "type": "number", - "description": "The number of skiped records for this query", - "example": 0 + "token": { + "type": "string", + "description": "The contract address of the token deposited", + "example": "0xec53bf9167f50cdeb3ae105f56099aaab9061f83" }, - "take": { + "strategy": { + "type": "string", + "description": "The contract address of the restaking strategy", + "example": "0xacb55c530acdb2849e6d4f36992cd8c9d50ed8f7" + }, + "shares": { "type": "number", - "description": "The number of records returned for this query", - "example": 12 + "description": "The amount of new shares given to the staker in this strategy", + "example": 10190000000000000000 } }, - "required": ["total", "skip", "take"] + "required": ["staker", "token", "strategy", "shares"] + }, + "underlyingToken": { + "type": "string", + "description": "The contract address of the token associated with this strategy", + "example": "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" + }, + "underlyingValue": { + "type": "number", + "description": "The value of the shares in terms of the underlying token", + "example": 5 + }, + "ethValue": { + "type": "number", + "description": "The value of the shares in ETH", + "example": 1 } }, - "required": ["data", "meta"] + "required": ["tx", "blockNumber", "blockTime", "type", "args"] } } } @@ -7950,152 +10352,229 @@ } } }, - "/deposits": { + "/events/withdrawal": { "get": { - "operationId": "getAllDeposits", - "summary": "Retrieve all deposits", - "description": "Returns all deposit data, including the transaction hash, token address, and other relevant information.", - "tags": ["Deposits"], + "operationId": "getWithdrawalEvents", + "summary": "Retrieve all withdrawal events", + "description": "Returns a list of all withdrawal events.", + "tags": ["Events"], "parameters": [ { "in": "query", - "name": "skip", - "description": "The number of records to skip for pagination", + "name": "txHash", + "description": "The transaction hash associated with the event", + "schema": { + "type": "string", + "pattern": "^0x([A-Fa-f0-9]{64})$", + "description": "The transaction hash associated with the event" + } + }, + { + "in": "query", + "name": "startAt", + "description": "Start date in ISO string format", + "schema": { + "type": "string", + "description": "Start date in ISO string format" + } + }, + { + "in": "query", + "name": "endAt", + "description": "End date in ISO string format", + "schema": { + "type": "string", + "description": "End date in ISO string format" + } + }, + { + "in": "query", + "name": "type", + "description": "The type of the withdrawal event", + "schema": { + "type": "string", + "enum": ["WITHDRAWAL_QUEUED", "WITHDRAWAL_COMPLETED"], + "description": "The type of the withdrawal event" + } + }, + { + "in": "query", + "name": "withdrawalRoot", + "description": "The withdrawal root associated with the event", "schema": { "type": "string", - "default": "0", - "description": "The number of records to skip for pagination", - "example": 0 + "pattern": "^0x[a-fA-F0-9]{64}$", + "description": "The withdrawal root associated with the event" } }, { "in": "query", - "name": "take", - "description": "The number of records to return for pagination", + "name": "delegatedTo", + "description": "The address to which funds were delegated", "schema": { "type": "string", - "default": "12", - "description": "The number of records to return for pagination", - "example": 12 + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address to which funds were delegated" } }, { "in": "query", - "name": "stakerAddress", - "description": "The address of the staker", + "name": "withdrawer", + "description": "The address of the withdrawer", "schema": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The address of the staker", - "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" + "description": "The address of the withdrawer" } }, { "in": "query", - "name": "tokenAddress", - "description": "The address of the token deposited", + "name": "withTokenData", + "description": "Toggle whether the route should return underlying token address and underlying value", "schema": { "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The address of the token deposited", - "example": "0xe95a203b1a91a908f9b9ce46459d101078c2c3cb" + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should return underlying token address and underlying value", + "example": "false" } }, { "in": "query", - "name": "strategyAddress", - "description": "The contract address of the restaking strategy", + "name": "withEthValue", + "description": "Toggle whether the route should return value denominated in ETH", "schema": { "type": "string", - "pattern": "^0x[a-fA-F0-9]{40}$", - "description": "The contract address of the restaking strategy", - "example": "0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6" + "enum": ["true", "false"], + "default": "false", + "description": "Toggle whether the route should return value denominated in ETH", + "example": "false" + } + }, + { + "in": "query", + "name": "skip", + "description": "The number of records to skip for pagination", + "schema": { + "type": "string", + "default": "0", + "description": "The number of records to skip for pagination", + "example": 0 + } + }, + { + "in": "query", + "name": "take", + "description": "The number of records to return for pagination", + "schema": { + "type": "string", + "default": "12", + "description": "The number of records to return for pagination", + "example": 12 } } ], "responses": { "200": { - "description": "The list of deposits.", + "description": "The withdrawal events found.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "properties": { - "transactionHash": { - "type": "string", - "description": "The hash of the transaction", - "example": "0x9d0a355df5a937516dfaed6721b0b461a16b8fad005f66d7dbf56b8a39136297" - }, - "stakerAddress": { - "type": "string", - "description": "The address of the staker", - "example": "0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd" - }, - "tokenAddress": { - "type": "string", - "description": "The address of the token", - "example": "0xe95a203b1a91a908f9b9ce46459d101078c2c3cb" - }, - "strategyAddress": { - "type": "string", - "description": "The contract address of the restaking strategy", - "example": "0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6" - }, - "shares": { - "type": "string", - "description": "The amount of shares held in the strategy", - "example": "40000000000000000" - }, - "createdAtBlock": { - "type": "number", - "description": "The block number when the withdrawal was recorded by EigenExplorer", - "example": 19912470 - }, - "createdAt": { - "type": "string", - "description": "The time stamp when the withdrawal was recorded by EigenExplorer", - "example": "2024-07-07T23:53:35.000Z" - } - }, - "required": [ - "transactionHash", - "stakerAddress", - "tokenAddress", - "strategyAddress", - "shares", - "createdAtBlock", - "createdAt" - ] - } + "tx": { + "type": "string", + "description": "The transaction hash", + "example": "0xae41958d0342a4485536f701c72723625131680f182eb21f95abdac6d74d0ff0" }, - "meta": { + "blockNumber": { + "type": "number", + "description": "The block number of the transaction", + "example": 21045085 + }, + "blockTime": { + "type": "string", + "description": "The block time of the transaction as an ISO 8601 string", + "example": "2024-10-25T20:41:11.000Z" + }, + "type": { + "type": "string", + "enum": ["WITHDRAWAL_QUEUED", "WITHDRAWAL_COMPLETED"], + "description": "The type of the event", + "example": "WITHDRAWAL_QUEUED" + }, + "args": { "type": "object", "properties": { - "total": { - "type": "number", - "description": "Total number of records in the database", - "example": 30 + "staker": { + "type": "string", + "description": "The contract address of the staker who initiated the withdrawal", + "example": "0x513ea5a99988252f3b2cd8382ac077d7fd26ef48" }, - "skip": { + "withdrawalRoot": { + "type": "string", + "description": "The root hash of the withdrawal", + "example": "0xe6cdf9110330e1648039cb98e680aeb9d1c63e022764186f1131eb9432605421" + }, + "delegatedTo": { + "type": "string", + "description": "The address to which the staker was delegated when the withdrawal was initiated", + "example": "0x4cd2086e1d708e65db5d4f5712a9ca46ed4bbd0a" + }, + "withdrawer": { + "type": "string", + "description": "The address of the withdrawer, authorized to complete the withdrawal and receive the funds", + "example": "0x513ea5a99988252f3b2cd8382ac077d7fd26ef48" + }, + "nonce": { "type": "number", - "description": "The number of skiped records for this query", + "description": "The nonce of the withdrawal, ensuring unique hashes for otherwise identical withdrawals", "example": 0 }, - "take": { + "startBlock": { "type": "number", - "description": "The number of records returned for this query", - "example": 12 + "description": "The block number when the withdrawal was created", + "example": 21054925 + }, + "strategies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "strategy": { + "type": "string", + "description": "The contract address of the restaking strategy", + "example": "0xacb55c530acdb2849e6d4f36992cd8c9d50ed8f7" + }, + "shares": { + "type": "string", + "description": "The amount of shares withdrawn for each strategy", + "example": "1000000000000000000" + }, + "underlyingToken": { + "type": "string", + "description": "The contract address of the token associated with this strategy", + "example": "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" + }, + "underlyingValue": { + "type": "number", + "description": "The value of the shares in terms of the underlying token", + "example": 5 + }, + "ethValue": { + "type": "number", + "description": "The value of the shares in ETH", + "example": 1 + } + }, + "required": ["strategy", "shares"] + } } }, - "required": ["total", "skip", "take"] + "required": ["withdrawalRoot"] } }, - "required": ["data", "meta"] + "required": ["tx", "blockNumber", "blockTime", "type", "args"] } } } @@ -8124,52 +10603,127 @@ } } }, - "/rewards/strategies": { + "/events/registration-status": { "get": { - "operationId": "getStrategies", - "summary": "Retrieve all strategies with their reward tokens", - "description": "Returns a list of strategies with their corresponding reward tokens, including strategy addresses and associated token addresses.", - "tags": ["Rewards"], + "operationId": "getRegistrationEvents", + "summary": "Retrieve all registration events", + "description": "Returns a list of all registration events.", + "tags": ["Events"], + "parameters": [ + { + "in": "query", + "name": "txHash", + "description": "The transaction hash associated with the event", + "schema": { + "type": "string", + "pattern": "^0x([A-Fa-f0-9]{64})$", + "description": "The transaction hash associated with the event" + } + }, + { + "in": "query", + "name": "startAt", + "description": "Start date in ISO string format", + "schema": { + "type": "string", + "description": "Start date in ISO string format" + } + }, + { + "in": "query", + "name": "endAt", + "description": "End date in ISO string format", + "schema": { + "type": "string", + "description": "End date in ISO string format" + } + }, + { + "in": "query", + "name": "status", + "description": "The status of Registration", + "schema": { + "type": "string", + "enum": ["REGISTERED", "DEREGISTERED"], + "description": "The status of Registration" + } + }, + { + "in": "query", + "name": "skip", + "description": "The number of records to skip for pagination", + "schema": { + "type": "string", + "default": "0", + "description": "The number of records to skip for pagination", + "example": 0 + } + }, + { + "in": "query", + "name": "take", + "description": "The number of records to return for pagination", + "schema": { + "type": "string", + "default": "12", + "description": "The number of records to return for pagination", + "example": 12 + } + } + ], "responses": { "200": { - "description": "List of strategies along with associated reward tokens.", + "description": "The registration events found.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "strategies": { - "type": "array", - "items": { - "type": "object", - "properties": { - "strategyAddress": { - "type": "string", - "description": "The contract address of the restaking strategy", - "example": "0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6" - }, - "tokens": { - "type": "array", - "items": { - "type": "string" - }, - "description": "List of reward token addresses associated with the strategy", - "example": [ - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "0xba50933c268f567bdc86e1ac131be072c6b0b71a" - ] - } - }, - "required": ["strategyAddress", "tokens"] - } + "tx": { + "type": "string", + "description": "The transaction hash", + "example": "0xae41958d0342a4485536f701c72723625131680f182eb21f95abdac6d74d0ff0" }, - "total": { + "blockNumber": { "type": "number", - "description": "The total number of strategies", - "example": 15 + "description": "The block number of the transaction", + "example": 21045085 + }, + "blockTime": { + "type": "string", + "description": "The block time of the transaction as an ISO 8601 string", + "example": "2024-10-25T20:41:11.000Z" + }, + "type": { + "type": "string", + "enum": ["REGISTRATION_STATUS"], + "description": "The type of the event", + "example": "REGISTRATION_STATUS" + }, + "args": { + "type": "object", + "properties": { + "operator": { + "type": "string", + "description": "The contract address of the AVS operator", + "example": "0x9abce41e1486210ad83deb831afcdd214af5b49d" + }, + "avs": { + "type": "string", + "description": "AVS service manager contract address", + "example": "0xb73a87e8f7f9129816d40940ca19dfa396944c71" + }, + "status": { + "type": "string", + "enum": ["REGISTERED", "DEREGISTERED"], + "description": "The status of the registration", + "example": "REGISTERED" + } + }, + "required": ["operator", "avs", "status"] } }, - "required": ["strategies", "total"] + "required": ["tx", "blockNumber", "blockTime", "type", "args"] } } } diff --git a/packages/openapi/src/apiResponseSchema/events/eventsRespone.ts b/packages/openapi/src/apiResponseSchema/events/eventsRespone.ts new file mode 100644 index 00000000..12b35e0c --- /dev/null +++ b/packages/openapi/src/apiResponseSchema/events/eventsRespone.ts @@ -0,0 +1,267 @@ +import z from '../../../../api/src/schema/zod' + +export const EventDetailsSchema = z.object({ + tx: z.string().describe('The transaction hash').openapi({ + example: '0xae41958d0342a4485536f701c72723625131680f182eb21f95abdac6d74d0ff0' + }), + blockNumber: z + .number() + .describe('The block number of the transaction') + .openapi({ example: 21045085 }), + blockTime: z + .string() + .describe('The block time of the transaction as an ISO 8601 string') + .openapi({ + example: '2024-10-25T20:41:11.000Z' + }) +}) + +const StrategySchema = z.object({ + strategy: z.string().describe('The contract address of the restaking strategy').openapi({ + example: '0x0fe4f44bee93503346a3ac9ee5a26b130a5796d6' + }), + multiplier: z + .string() + .describe('The multiplier associated with this strategy') + .openapi({ example: '1068966896363604679' }), + amount: z + .string() + .optional() + .describe( + 'The amount of rewards allocated to this strategy from the total rewards in this submissionn' + ) + .openapi({ example: '3.7932452554246293e+21' }), + amountEthValue: z + .number() + .optional() + .describe('The value of the rewards amount allocated to this strategy in ETH') + .openapi({ example: 0.0638779707245759 }) +}) + +const UnderlyingSchema = z.object({ + underlyingToken: z + .string() + .optional() + .describe('The contract address of the token associated with this strategy') + .openapi({ + example: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84' + }), + underlyingValue: z + .number() + .optional() + .describe('The value of the shares in terms of the underlying token') + .openapi({ example: 5.0 }) +}) + +const EthValueSchema = z.object({ + ethValue: z + .number() + .optional() + .describe('The value of the shares in ETH') + .openapi({ example: 1.0 }) +}) + +export const GlobalDelegationEventSchema = EventDetailsSchema.extend({ + type: z + .enum(['DELEGATION', 'UNDELEGATION', 'SHARES_INCREASED', 'SHARES_DECREASED']) + .describe('The type of the event') + .openapi({ example: 'DELEGATION' }), + args: z.object({ + operator: z.string().describe('The contract address of the AVS operator').openapi({ + example: '0x71c6f7ed8c2d4925d0baf16f6a85bb1736d412eb' + }), + staker: z.string().describe('The contract address of the staker').openapi({ + example: '0x42318adf0773b8af4aa8ed1670ea0af7761d07c7' + }), + strategy: z + .string() + .optional() + .describe('The contract address of the restaking strategy') + .openapi({ + example: '0x93c4b944d05dfe6df7645a86cd2206016c51564d' + }), + shares: z + .number() + .optional() + .describe( + "The change in the operator's delegated shares, added or subtracted from the total." + ) + .openapi({ example: 62816824424188010 }) + }), + ...UnderlyingSchema.shape, + ...EthValueSchema.shape +}) + +export const GlobalRewardsEventSchema = EventDetailsSchema.extend({ + type: z.enum(['REWARDS']).describe('The type of the event').openapi({ example: 'REWARDS' }), + args: z.object({ + avs: z.string().describe('AVS service manager contract address').openapi({ + example: '0x1de75eaab2df55d467494a172652579e6fa4540e' + }), + submissionNonce: z + .number() + .describe('The nonce of the rewards submission') + .openapi({ example: 2 }), + rewardsSubmissionHash: z.string().describe('The hash of the rewards submission').openapi({ + example: '0x1e391c015c923972811a27e1c6c3a874511e47033f1022021f29967a60ab2c87' + }), + rewardsSubmissionToken: z + .string() + .describe('The contract address of the token used for rewards distribution') + .openapi({ + example: '0xba50933c268f567bdc86e1ac131be072c6b0b71a' + }), + rewardsSubmissionAmount: z + .string() + .describe('The total amount of rewards allocated in this submission') + .openapi({ + example: '49000000000000000000000' + }), + rewardsSubmissionStartTimeStamp: z + .number() + .describe('The timestamp marking the start of this rewards distribution period') + .openapi({ + example: 1728518400 + }), + rewardsSubmissionDuration: z + .number() + .describe('The duration (in seconds) over which the rewards are distributed') + .openapi({ example: 6048000 }), + strategies: z + .array(StrategySchema) + .describe('List of strategies involved in the rewards submission') + }), + ...EthValueSchema.shape +}) + +export const GlobalDepositEventSchema = EventDetailsSchema.extend({ + type: z.enum(['DEPOSIT']).describe('The type of the event').openapi({ example: 'DEPOSIT' }), + args: z.object({ + staker: z.string().describe('The contract address of the staker').openapi({ + example: '0xa0e32344405b2097e738718dc27d2c2daf73e706' + }), + token: z.string().describe('The contract address of the token deposited').openapi({ + example: '0xec53bf9167f50cdeb3ae105f56099aaab9061f83' + }), + strategy: z.string().describe('The contract address of the restaking strategy').openapi({ + example: '0xacb55c530acdb2849e6d4f36992cd8c9d50ed8f7' + }), + shares: z + .number() + .describe('The amount of new shares given to the staker in this strategy') + .openapi({ example: 10190000000000000000 }) + }), + ...UnderlyingSchema.shape, + ...EthValueSchema.shape +}) + +export const GlobalWithdrawalEventSchema = EventDetailsSchema.extend({ + type: z + .enum(['WITHDRAWAL_QUEUED', 'WITHDRAWAL_COMPLETED']) + .describe('The type of the event') + .openapi({ example: 'WITHDRAWAL_QUEUED' }), + args: z.object({ + staker: z + .string() + .optional() + .describe('The contract address of the staker who initiated the withdrawal') + .openapi({ + example: '0x513ea5a99988252f3b2cd8382ac077d7fd26ef48' + }), + withdrawalRoot: z.string().describe('The root hash of the withdrawal').openapi({ + example: '0xe6cdf9110330e1648039cb98e680aeb9d1c63e022764186f1131eb9432605421' + }), + delegatedTo: z + .string() + .optional() + .describe('The address to which the staker was delegated when the withdrawal was initiated') + .openapi({ + example: '0x4cd2086e1d708e65db5d4f5712a9ca46ed4bbd0a' + }), + withdrawer: z + .string() + .optional() + .describe( + 'The address of the withdrawer, authorized to complete the withdrawal and receive the funds' + ) + .openapi({ + example: '0x513ea5a99988252f3b2cd8382ac077d7fd26ef48' + }), + nonce: z + .number() + .optional() + .describe( + 'The nonce of the withdrawal, ensuring unique hashes for otherwise identical withdrawals' + ) + .openapi({ example: 0 }), + startBlock: z + .number() + .optional() + .describe('The block number when the withdrawal was created') + .openapi({ example: 21054925 }), + strategies: z + .array( + z.object({ + strategy: z.string().describe('The contract address of the restaking strategy').openapi({ + example: '0xacb55c530acdb2849e6d4f36992cd8c9d50ed8f7' + }), + shares: z + .string() + .describe('The amount of shares withdrawn for each strategy') + .openapi({ example: '1000000000000000000' }), + ...UnderlyingSchema.shape, + ...EthValueSchema.shape + }) + ) + .optional() + }) +}) + +export const GlobalRegistrationEventSchema = EventDetailsSchema.extend({ + type: z + .enum(['REGISTRATION_STATUS']) + .describe('The type of the event') + .openapi({ example: 'REGISTRATION_STATUS' }), + args: z.object({ + operator: z.string().describe('The contract address of the AVS operator').openapi({ + example: '0x9abce41e1486210ad83deb831afcdd214af5b49d' + }), + avs: z.string().describe('AVS service manager contract address').openapi({ + example: '0xb73a87e8f7f9129816d40940ca19dfa396944c71' + }), + status: z + .enum(['REGISTERED', 'DEREGISTERED']) + .describe('The status of the registration') + .openapi({ + example: 'REGISTERED' + }) + }) +}) + +export const OperatorDelegationEventSchema = GlobalDelegationEventSchema.extend({ + args: GlobalDelegationEventSchema.shape.args.omit({ operator: true }) +}) + +export const AvsRewardsEventSchema = GlobalRewardsEventSchema.extend({ + args: GlobalRewardsEventSchema.shape.args.omit({ avs: true }) +}) + +export const StakerDelegationEventSchema = GlobalDelegationEventSchema.extend({ + args: GlobalDelegationEventSchema.shape.args.omit({ staker: true }) +}) + +export const StakerDepositEventSchema = GlobalDepositEventSchema.extend({ + args: GlobalDepositEventSchema.shape.args.omit({ staker: true }) +}) + +export const StakerWithdrawalEventSchema = GlobalWithdrawalEventSchema.extend({ + args: GlobalWithdrawalEventSchema.shape.args.omit({ staker: true }) +}) + +export const OperatorRegistrationEventSchema = GlobalRegistrationEventSchema.extend({ + args: GlobalRegistrationEventSchema.shape.args.omit({ operator: true }) +}) + +export const AvsRegistrationEventSchema = GlobalRegistrationEventSchema.extend({ + args: GlobalRegistrationEventSchema.shape.args.omit({ avs: true }) +}) diff --git a/packages/openapi/src/apiResponseSchema/events/util.ts b/packages/openapi/src/apiResponseSchema/events/util.ts new file mode 100644 index 00000000..3c892902 --- /dev/null +++ b/packages/openapi/src/apiResponseSchema/events/util.ts @@ -0,0 +1,9 @@ +import z from '../../../../api/src/schema/zod' + +// Refinement Utility Function +export const applyAllRefinements = ( + schema: z.ZodTypeAny, + refinements: Array<(schema: z.ZodTypeAny) => z.ZodTypeAny> +) => { + return refinements.reduce((refinedSchema, refineFn) => refineFn(refinedSchema), schema) +} diff --git a/packages/openapi/src/apiResponseSchema/withdrawals/withdrawalsResponseSchema.ts b/packages/openapi/src/apiResponseSchema/withdrawals/withdrawalsResponseSchema.ts index 2cee065f..79851593 100644 --- a/packages/openapi/src/apiResponseSchema/withdrawals/withdrawalsResponseSchema.ts +++ b/packages/openapi/src/apiResponseSchema/withdrawals/withdrawalsResponseSchema.ts @@ -5,18 +5,25 @@ export const WithdrawalsResponseSchema = z.object({ withdrawalRoot: z.string().describe('The root hash of the withdrawal').openapi({ example: '0x9e6728ef0a8ad6009107a886047aae35bc5ed7deaa68580b0d1f8f67e3e5ed31' }), - nonce: z.number().describe('The nonce of the withdrawal').openapi({ example: 0 }), + nonce: z + .number() + .describe( + 'The nonce of the withdrawal, ensuring unique hashes for otherwise identical withdrawals' + ) + .openapi({ example: 0 }), stakerAddress: z .string() - .describe('The address of the staker') + .describe('The contract address of the staker who initiated the withdrawal') .openapi({ example: '0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd' }), delegatedTo: z .string() - .describe('The operator address to which staking is delegated') + .describe('The address to which the staker was delegated when the withdrawal was initiated') .openapi({ example: '0x0000000000000000000000000000000000000000' }), withdrawerAddress: z .string() - .describe('The address of the withdrawer') + .describe( + 'The address of the withdrawer, authorized to complete the withdrawal and receive the funds' + ) .openapi({ example: '0x74ede5f75247fbdb9266d2b3a7be63b3db7611dd' }), shares: z .array(StrategySharesSchema) diff --git a/packages/openapi/src/documentBase.ts b/packages/openapi/src/documentBase.ts index ceb3c136..bd7fc2e9 100644 --- a/packages/openapi/src/documentBase.ts +++ b/packages/openapi/src/documentBase.ts @@ -9,6 +9,7 @@ import { stakersRoutes } from './routes/stakers' import { depositsRoutes } from './routes/deposits' import { historicalRoutes } from './routes/historical' import { rewardsRoutes } from './routes/rewards' +import { eventRoutes } from './routes/events' export const document = createDocument({ openapi: '3.0.3', @@ -36,7 +37,8 @@ export const document = createDocument({ ...withdrawalsRoutes, ...stakersRoutes, ...depositsRoutes, - ...rewardsRoutes + ...rewardsRoutes, + ...eventRoutes }, components: { schemas: {}, diff --git a/packages/openapi/src/routes/avs/getAvsRegistrationEvents.ts b/packages/openapi/src/routes/avs/getAvsRegistrationEvents.ts new file mode 100644 index 00000000..d0dd25b9 --- /dev/null +++ b/packages/openapi/src/routes/avs/getAvsRegistrationEvents.ts @@ -0,0 +1,45 @@ +import z from '../../../../api/src/schema/zod' +import { openApiErrorResponses } from '../../apiResponseSchema/base/errorResponses' +import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/base/ethereumAddress' +import { ZodOpenApiOperationObject } from 'zod-openapi' +import { AvsRegistrationEventSchema } from '../../apiResponseSchema/events/eventsRespone' +import { + refineStartEndDates, + AvsRegistrationEventQuerySchemaBase +} from '../../../../api/src/schema/zod/schemas/eventSchemas' +import { PaginationQuerySchema } from '../../../../api/src/schema/zod/schemas/paginationQuery' +import { applyAllRefinements } from '../../apiResponseSchema/events/util' + +const AvsAddressParam = z.object({ + address: EthereumAddressSchema.describe('AVS service manager contract address').openapi({ + example: '0x870679e138bcdf293b7ff14dd44b70fc97e12fc0' + }) +}) + +const CombinedQuerySchemaBase = z + .object({}) + .merge(AvsRegistrationEventQuerySchemaBase) + .merge(PaginationQuerySchema) +const CombinedQuerySchema = applyAllRefinements(CombinedQuerySchemaBase, [refineStartEndDates]) + +export const getAvsRegistrationEvents: ZodOpenApiOperationObject = { + operationId: 'getAvsRegistrationEvents', + summary: 'Retrieve all registration events for a given AVS address', + description: 'Returns a list of all registration events for a given AVS address.', + tags: ['AVS'], + requestParams: { + path: AvsAddressParam, + query: CombinedQuerySchema + }, + responses: { + '200': { + description: 'The registration events found for the AVS.', + content: { + 'application/json': { + schema: AvsRegistrationEventSchema + } + } + }, + ...openApiErrorResponses + } +} diff --git a/packages/openapi/src/routes/avs/getAvsRewardsEvents.ts b/packages/openapi/src/routes/avs/getAvsRewardsEvents.ts new file mode 100644 index 00000000..65ff9433 --- /dev/null +++ b/packages/openapi/src/routes/avs/getAvsRewardsEvents.ts @@ -0,0 +1,45 @@ +import z from '../../../../api/src/schema/zod' +import { openApiErrorResponses } from '../../apiResponseSchema/base/errorResponses' +import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/base/ethereumAddress' +import { ZodOpenApiOperationObject } from 'zod-openapi' +import { AvsRewardsEventSchema } from '../../apiResponseSchema/events/eventsRespone' +import { + refineStartEndDates, + RewardsEventQuerySchemaBase +} from '../../../../api/src/schema/zod/schemas/eventSchemas' +import { PaginationQuerySchema } from '../../../../api/src/schema/zod/schemas/paginationQuery' +import { applyAllRefinements } from '../../apiResponseSchema/events/util' + +const AvsAddressParam = z.object({ + address: EthereumAddressSchema.describe('AVS service manager contract address').openapi({ + example: '0x870679e138bcdf293b7ff14dd44b70fc97e12fc0' + }) +}) + +const CombinedQuerySchemaBase = z + .object({}) + .merge(RewardsEventQuerySchemaBase) + .merge(PaginationQuerySchema) +const CombinedQuerySchema = applyAllRefinements(CombinedQuerySchemaBase, [refineStartEndDates]) + +export const getAvsRewardsEvents: ZodOpenApiOperationObject = { + operationId: 'getAvsRewardsEvents', + summary: 'Retrieve all reward events for a given AVS address', + description: 'Returns a list of all reward events for a given AVS address.', + tags: ['AVS'], + requestParams: { + path: AvsAddressParam, + query: CombinedQuerySchema + }, + responses: { + '200': { + description: 'The reward events found for the AVS.', + content: { + 'application/json': { + schema: AvsRewardsEventSchema + } + } + }, + ...openApiErrorResponses + } +} diff --git a/packages/openapi/src/routes/avs/index.ts b/packages/openapi/src/routes/avs/index.ts index a54942d1..d2598629 100644 --- a/packages/openapi/src/routes/avs/index.ts +++ b/packages/openapi/src/routes/avs/index.ts @@ -4,7 +4,9 @@ import { getAllAvs } from './getAllAvs' import { getAvsByAddress } from './getAvsByAddress' import { getAvsStakersByAddress } from './getAvsStakersByAddress' import { getAvsOperatorsByAddress } from './getAvsOperatorsByAddress' -import { getAvsRewards } from './getAVSRewards' +import { getAvsRewards } from './getAvsRewards' +import { getAvsRewardsEvents } from './getAvsRewardsEvents' +import { getAvsRegistrationEvents } from './getAvsRegistrationEvents' export const avsRoutes: ZodOpenApiPathsObject = { '/avs': { get: getAllAvs }, @@ -14,5 +16,7 @@ export const avsRoutes: ZodOpenApiPathsObject = { '/avs/{address}': { get: getAvsByAddress }, '/avs/{address}/stakers': { get: getAvsStakersByAddress }, '/avs/{address}/operators': { get: getAvsOperatorsByAddress }, - '/avs/{address}/rewards': { get: getAvsRewards } + '/avs/{address}/rewards': { get: getAvsRewards }, + '/avs/{address}/events/rewards': { get: getAvsRewardsEvents }, + '/avs/{address}/events/registration-status': { get: getAvsRegistrationEvents } } diff --git a/packages/openapi/src/routes/events/getDelegationEvents.ts b/packages/openapi/src/routes/events/getDelegationEvents.ts new file mode 100644 index 00000000..8e7766f6 --- /dev/null +++ b/packages/openapi/src/routes/events/getDelegationEvents.ts @@ -0,0 +1,49 @@ +import z from '../../../../api/src/schema/zod' +import { openApiErrorResponses } from '../../apiResponseSchema/base/errorResponses' +import { ZodOpenApiOperationObject } from 'zod-openapi' +import { GlobalDelegationEventSchema } from '../../apiResponseSchema/events/eventsRespone' +import { + DelegationEventQuerySchemaBase, + refineDelegationTypeRestrictions, + refineStartEndDates, + refineWithEthValueRequiresTokenData +} from '../../../../api/src/schema/zod/schemas/eventSchemas' +import { PaginationQuerySchema } from '../../../../api/src/schema/zod/schemas/paginationQuery' +import { applyAllRefinements } from '../../apiResponseSchema/events/util' +import { + WithTokenDataQuerySchema, + WithEthValueQuerySchema +} from '../../../../api/src/schema/zod/schemas/withTokenDataQuery' + +const CombinedQuerySchemaBase = z + .object({}) + .merge(DelegationEventQuerySchemaBase) + .merge(WithTokenDataQuerySchema) + .merge(WithEthValueQuerySchema) + .merge(PaginationQuerySchema) +const CombinedQuerySchema = applyAllRefinements(CombinedQuerySchemaBase, [ + refineStartEndDates, + refineWithEthValueRequiresTokenData, + refineDelegationTypeRestrictions +]) + +export const getDelegationEvents: ZodOpenApiOperationObject = { + operationId: 'getDelegationEvents', + summary: 'Retrieve all delegation events', + description: 'Returns a list of all delegation events.', + tags: ['Events'], + requestParams: { + query: CombinedQuerySchema + }, + responses: { + '200': { + description: 'The delegation events found.', + content: { + 'application/json': { + schema: GlobalDelegationEventSchema + } + } + }, + ...openApiErrorResponses + } +} diff --git a/packages/openapi/src/routes/events/getDepositEvents.ts b/packages/openapi/src/routes/events/getDepositEvents.ts new file mode 100644 index 00000000..3f658c37 --- /dev/null +++ b/packages/openapi/src/routes/events/getDepositEvents.ts @@ -0,0 +1,41 @@ +import z from '../../../../api/src/schema/zod' +import { openApiErrorResponses } from '../../apiResponseSchema/base/errorResponses' +import { ZodOpenApiOperationObject } from 'zod-openapi' +import { GlobalDepositEventSchema } from '../../apiResponseSchema/events/eventsRespone' +import { + DepositEventQuerySchemaBase, + refineStartEndDates, + refineWithEthValueRequiresTokenData +} from '../../../../api/src/schema/zod/schemas/eventSchemas' +import { PaginationQuerySchema } from '../../../../api/src/schema/zod/schemas/paginationQuery' +import { applyAllRefinements } from '../../apiResponseSchema/events/util' + +const CombinedQuerySchemaBase = z + .object({}) + .merge(DepositEventQuerySchemaBase) + .merge(PaginationQuerySchema) +const CombinedQuerySchema = applyAllRefinements(CombinedQuerySchemaBase, [ + refineStartEndDates, + refineWithEthValueRequiresTokenData +]) + +export const getDepositEvents: ZodOpenApiOperationObject = { + operationId: 'getDepositEvents', + summary: 'Retrieve all deposit events', + description: 'Returns a list of all deposit events.', + tags: ['Events'], + requestParams: { + query: CombinedQuerySchema + }, + responses: { + '200': { + description: 'The deposit events found.', + content: { + 'application/json': { + schema: GlobalDepositEventSchema + } + } + }, + ...openApiErrorResponses + } +} diff --git a/packages/openapi/src/routes/events/getRegistrationEvents.ts b/packages/openapi/src/routes/events/getRegistrationEvents.ts new file mode 100644 index 00000000..9e98cb3d --- /dev/null +++ b/packages/openapi/src/routes/events/getRegistrationEvents.ts @@ -0,0 +1,37 @@ +import z from '../../../../api/src/schema/zod' +import { openApiErrorResponses } from '../../apiResponseSchema/base/errorResponses' +import { ZodOpenApiOperationObject } from 'zod-openapi' +import { GlobalRegistrationEventSchema } from '../../apiResponseSchema/events/eventsRespone' +import { + refineStartEndDates, + RegistrationEventQuerySchemaBase +} from '../../../../api/src/schema/zod/schemas/eventSchemas' +import { PaginationQuerySchema } from '../../../../api/src/schema/zod/schemas/paginationQuery' +import { applyAllRefinements } from '../../apiResponseSchema/events/util' + +const CombinedQuerySchemaBase = z + .object({}) + .merge(RegistrationEventQuerySchemaBase) + .merge(PaginationQuerySchema) +const CombinedQuerySchema = applyAllRefinements(CombinedQuerySchemaBase, [refineStartEndDates]) + +export const getRegistrationsEvents: ZodOpenApiOperationObject = { + operationId: 'getRegistrationEvents', + summary: 'Retrieve all registration events', + description: 'Returns a list of all registration events.', + tags: ['Events'], + requestParams: { + query: CombinedQuerySchema + }, + responses: { + '200': { + description: 'The registration events found.', + content: { + 'application/json': { + schema: GlobalRegistrationEventSchema + } + } + }, + ...openApiErrorResponses + } +} diff --git a/packages/openapi/src/routes/events/getRewardsEvents.ts b/packages/openapi/src/routes/events/getRewardsEvents.ts new file mode 100644 index 00000000..4d7858a6 --- /dev/null +++ b/packages/openapi/src/routes/events/getRewardsEvents.ts @@ -0,0 +1,37 @@ +import z from '../../../../api/src/schema/zod' +import { openApiErrorResponses } from '../../apiResponseSchema/base/errorResponses' +import { ZodOpenApiOperationObject } from 'zod-openapi' +import { GlobalRewardsEventSchema } from '../../apiResponseSchema/events/eventsRespone' +import { + refineStartEndDates, + RewardsEventQuerySchemaBase +} from '../../../../api/src/schema/zod/schemas/eventSchemas' +import { PaginationQuerySchema } from '../../../../api/src/schema/zod/schemas/paginationQuery' +import { applyAllRefinements } from '../../apiResponseSchema/events/util' + +const CombinedQuerySchemaBase = z + .object({}) + .merge(RewardsEventQuerySchemaBase) + .merge(PaginationQuerySchema) +const CombinedQuerySchema = applyAllRefinements(CombinedQuerySchemaBase, [refineStartEndDates]) + +export const getRewardsEvents: ZodOpenApiOperationObject = { + operationId: 'getRewardsEvents', + summary: 'Retrieve all reward events', + description: 'Returns a list of all reward events.', + tags: ['Events'], + requestParams: { + query: CombinedQuerySchema + }, + responses: { + '200': { + description: 'The reward events found.', + content: { + 'application/json': { + schema: GlobalRewardsEventSchema + } + } + }, + ...openApiErrorResponses + } +} diff --git a/packages/openapi/src/routes/events/getWithdrawalEvents.ts b/packages/openapi/src/routes/events/getWithdrawalEvents.ts new file mode 100644 index 00000000..5e818c32 --- /dev/null +++ b/packages/openapi/src/routes/events/getWithdrawalEvents.ts @@ -0,0 +1,43 @@ +import z from '../../../../api/src/schema/zod' +import { openApiErrorResponses } from '../../apiResponseSchema/base/errorResponses' +import { ZodOpenApiOperationObject } from 'zod-openapi' +import { GlobalWithdrawalEventSchema } from '../../apiResponseSchema/events/eventsRespone' +import { + refineStartEndDates, + refineWithdrawalTypeRestrictions, + refineWithEthValueRequiresTokenData, + WithdrawalEventQuerySchemaBase +} from '../../../../api/src/schema/zod/schemas/eventSchemas' +import { PaginationQuerySchema } from '../../../../api/src/schema/zod/schemas/paginationQuery' +import { applyAllRefinements } from '../../apiResponseSchema/events/util' + +const CombinedQuerySchemaBase = z + .object({}) + .merge(WithdrawalEventQuerySchemaBase) + .merge(PaginationQuerySchema) +const CombinedQuerySchema = applyAllRefinements(CombinedQuerySchemaBase, [ + refineStartEndDates, + refineWithEthValueRequiresTokenData, + refineWithdrawalTypeRestrictions +]) + +export const getWithdrawalEvents: ZodOpenApiOperationObject = { + operationId: 'getWithdrawalEvents', + summary: 'Retrieve all withdrawal events', + description: 'Returns a list of all withdrawal events.', + tags: ['Events'], + requestParams: { + query: CombinedQuerySchema + }, + responses: { + '200': { + description: 'The withdrawal events found.', + content: { + 'application/json': { + schema: GlobalWithdrawalEventSchema + } + } + }, + ...openApiErrorResponses + } +} diff --git a/packages/openapi/src/routes/events/index.ts b/packages/openapi/src/routes/events/index.ts new file mode 100644 index 00000000..5de225fc --- /dev/null +++ b/packages/openapi/src/routes/events/index.ts @@ -0,0 +1,14 @@ +import { ZodOpenApiPathsObject } from 'zod-openapi' +import { getDelegationEvents } from './getDelegationEvents' +import { getDepositEvents } from './getDepositEvents' +import { getRewardsEvents } from './getRewardsEvents' +import { getWithdrawalEvents } from './getWithdrawalEvents' +import { getRegistrationsEvents } from './getRegistrationEvents' + +export const eventRoutes: ZodOpenApiPathsObject = { + '/events/delegation': { get: getDelegationEvents }, + '/events/rewards': { get: getRewardsEvents }, + '/events/deposit': { get: getDepositEvents }, + '/events/withdrawal': { get: getWithdrawalEvents }, + '/events/registration-status': { get: getRegistrationsEvents } +} diff --git a/packages/openapi/src/routes/operators/getOperatorDelegationEvents.ts b/packages/openapi/src/routes/operators/getOperatorDelegationEvents.ts new file mode 100644 index 00000000..9223f302 --- /dev/null +++ b/packages/openapi/src/routes/operators/getOperatorDelegationEvents.ts @@ -0,0 +1,51 @@ +import { openApiErrorResponses } from '../../apiResponseSchema/base/errorResponses' +import z from '../../../../api/src/schema/zod' +import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/base/ethereumAddress' +import { ZodOpenApiOperationObject } from 'zod-openapi' +import { OperatorDelegationEventSchema } from '../../apiResponseSchema/events/eventsRespone' +import { + OperatorDelegationEventQuerySchemaBase, + refineDelegationTypeRestrictions, + refineStartEndDates, + refineWithEthValueRequiresTokenData +} from '../../../../api/src/schema/zod/schemas/eventSchemas' +import { PaginationQuerySchema } from '../../../../api/src/schema/zod/schemas/paginationQuery' +import { applyAllRefinements } from '../../apiResponseSchema/events/util' + +const OperatorAddressParam = z.object({ + address: EthereumAddressSchema.describe('The address of the operator').openapi({ + example: '0x00107cfdeaddc0a3160ed2f6fedd627f313e7b1a' + }) +}) + +const CombinedQuerySchemaBase = z + .object({}) + .merge(OperatorDelegationEventQuerySchemaBase) + .merge(PaginationQuerySchema) +const CombinedQuerySchema = applyAllRefinements(CombinedQuerySchemaBase, [ + refineStartEndDates, + refineWithEthValueRequiresTokenData, + refineDelegationTypeRestrictions +]) + +export const getOperatorDelegationEvents: ZodOpenApiOperationObject = { + operationId: 'getOperatorDelegationEvents', + summary: 'Retrieve all delegation events for a given operator address', + description: 'Returns a list of all delegation events for a given operator address.', + tags: ['Operators'], + requestParams: { + path: OperatorAddressParam, + query: CombinedQuerySchema + }, + responses: { + '200': { + description: 'The delegation events found for the operator.', + content: { + 'application/json': { + schema: OperatorDelegationEventSchema + } + } + }, + ...openApiErrorResponses + } +} diff --git a/packages/openapi/src/routes/operators/getOperatorRegistrationEvents.ts b/packages/openapi/src/routes/operators/getOperatorRegistrationEvents.ts new file mode 100644 index 00000000..7c89cdf3 --- /dev/null +++ b/packages/openapi/src/routes/operators/getOperatorRegistrationEvents.ts @@ -0,0 +1,46 @@ +import { openApiErrorResponses } from '../../apiResponseSchema/base/errorResponses' +import z from '../../../../api/src/schema/zod' +import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/base/ethereumAddress' +import { ZodOpenApiOperationObject } from 'zod-openapi' +import { OperatorRegistrationEventSchema } from '../../apiResponseSchema/events/eventsRespone' +import { + OperatorRegistrationEventQuerySchemaBase, + refineStartEndDates +} from '../../../../api/src/schema/zod/schemas/eventSchemas' +import { PaginationQuerySchema } from '../../../../api/src/schema/zod/schemas/paginationQuery' +import { applyAllRefinements } from '../../apiResponseSchema/events/util' + +const OperatorAddressParam = z.object({ + address: EthereumAddressSchema.describe('The address of the operator').openapi({ + example: '0x00107cfdeaddc0a3160ed2f6fedd627f313e7b1a' + }) +}) + +const CombinedQuerySchemaBase = z + .object({}) + .merge(OperatorRegistrationEventQuerySchemaBase) + .merge(PaginationQuerySchema) + +const CombinedQuerySchema = applyAllRefinements(CombinedQuerySchemaBase, [refineStartEndDates]) + +export const getOperatorRegistrationEvents: ZodOpenApiOperationObject = { + operationId: 'getOperatorRegistrationEvents', + summary: 'Retrieve all registration events for a given operator address', + description: 'Returns a list of all registration events for a given operator address.', + tags: ['Operators'], + requestParams: { + path: OperatorAddressParam, + query: CombinedQuerySchema + }, + responses: { + '200': { + description: 'The registration events found for the operator.', + content: { + 'application/json': { + schema: OperatorRegistrationEventSchema + } + } + }, + ...openApiErrorResponses + } +} diff --git a/packages/openapi/src/routes/operators/index.ts b/packages/openapi/src/routes/operators/index.ts index 0b5fa5e0..fb50f65e 100644 --- a/packages/openapi/src/routes/operators/index.ts +++ b/packages/openapi/src/routes/operators/index.ts @@ -3,10 +3,14 @@ import { getAllOperators } from './getAllOperators' import { getOperatorByAddress } from './getOperatorByAddress' import { getAllOperatorAddresses } from './getAllOperatorAddresses' import { getOperatorRewards } from './getOperatorRewards' +import { getOperatorDelegationEvents } from './getOperatorDelegationEvents' +import { getOperatorRegistrationEvents } from './getOperatorRegistrationEvents' export const operatorsRoutes: ZodOpenApiPathsObject = { '/operators': { get: getAllOperators }, '/operators/addresses': { get: getAllOperatorAddresses }, '/operators/{address}': { get: getOperatorByAddress }, - '/operators/{address}/rewards': { get: getOperatorRewards } + '/operators/{address}/rewards': { get: getOperatorRewards }, + '/operators/{address}/events/delegation': { get: getOperatorDelegationEvents }, + '/operators/{address}/events/registration-status': { get: getOperatorRegistrationEvents } } diff --git a/packages/openapi/src/routes/stakers/getStakerDelegationEvents.ts b/packages/openapi/src/routes/stakers/getStakerDelegationEvents.ts new file mode 100644 index 00000000..553e930e --- /dev/null +++ b/packages/openapi/src/routes/stakers/getStakerDelegationEvents.ts @@ -0,0 +1,51 @@ +import z from '../../../../api/src/schema/zod' +import { ZodOpenApiOperationObject } from 'zod-openapi' +import { openApiErrorResponses } from '../../apiResponseSchema/base/errorResponses' +import { PaginationQuerySchema } from '../../../../api/src/schema/zod/schemas/paginationQuery' +import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/base/ethereumAddress' +import { StakerDelegationEventSchema } from '../../apiResponseSchema/events/eventsRespone' +import { + refineDelegationTypeRestrictions, + refineStartEndDates, + refineWithEthValueRequiresTokenData, + StakerDelegationEventQuerySchemaBase +} from '../../../../api/src/schema/zod/schemas/eventSchemas' +import { applyAllRefinements } from '../../apiResponseSchema/events/util' + +const StakerAddressParam = z.object({ + address: EthereumAddressSchema.describe('The address of the staker').openapi({ + example: '0x9791fdb4e9c0495efc5a1f3f9271ef226251eb34' + }) +}) + +const CombinedQuerySchemaBase = z + .object({}) + .merge(StakerDelegationEventQuerySchemaBase) + .merge(PaginationQuerySchema) +const CombinedQuerySchema = applyAllRefinements(CombinedQuerySchemaBase, [ + refineStartEndDates, + refineWithEthValueRequiresTokenData, + refineDelegationTypeRestrictions +]) + +export const getStakerDelegationEvents: ZodOpenApiOperationObject = { + operationId: 'getStakerDelegationEvents', + summary: 'Retrieve all delegation events for a given staker address', + description: 'Returns a list of all delegation events for a given staker address.', + tags: ['Stakers'], + requestParams: { + path: StakerAddressParam, + query: CombinedQuerySchema + }, + responses: { + '200': { + description: 'The delegation events found for the staker.', + content: { + 'application/json': { + schema: StakerDelegationEventSchema + } + } + }, + ...openApiErrorResponses + } +} diff --git a/packages/openapi/src/routes/stakers/getStakerDepositEvents.ts b/packages/openapi/src/routes/stakers/getStakerDepositEvents.ts new file mode 100644 index 00000000..8e272747 --- /dev/null +++ b/packages/openapi/src/routes/stakers/getStakerDepositEvents.ts @@ -0,0 +1,49 @@ +import z from '../../../../api/src/schema/zod' +import { ZodOpenApiOperationObject } from 'zod-openapi' +import { openApiErrorResponses } from '../../apiResponseSchema/base/errorResponses' +import { PaginationQuerySchema } from '../../../../api/src/schema/zod/schemas/paginationQuery' +import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/base/ethereumAddress' +import { StakerDepositEventSchema } from '../../apiResponseSchema/events/eventsRespone' +import { + DepositEventQuerySchemaBase, + refineStartEndDates, + refineWithEthValueRequiresTokenData +} from '../../../../api/src/schema/zod/schemas/eventSchemas' +import { applyAllRefinements } from '../../apiResponseSchema/events/util' + +const StakerAddressParam = z.object({ + address: EthereumAddressSchema.describe('The address of the staker').openapi({ + example: '0x9791fdb4e9c0495efc5a1f3f9271ef226251eb34' + }) +}) + +const CombinedQuerySchemaBase = z + .object({}) + .merge(DepositEventQuerySchemaBase) + .merge(PaginationQuerySchema) +const CombinedQuerySchema = applyAllRefinements(CombinedQuerySchemaBase, [ + refineStartEndDates, + refineWithEthValueRequiresTokenData +]) + +export const getStakerDepositEvents: ZodOpenApiOperationObject = { + operationId: 'getStakerDepositEvents', + summary: 'Retrieve all deposit events for a given staker address', + description: 'Returns a list of all deposit events for a given staker address.', + tags: ['Stakers'], + requestParams: { + path: StakerAddressParam, + query: CombinedQuerySchema + }, + responses: { + '200': { + description: 'The deposit events found for the staker.', + content: { + 'application/json': { + schema: StakerDepositEventSchema + } + } + }, + ...openApiErrorResponses + } +} diff --git a/packages/openapi/src/routes/stakers/getStakerWithdrawalEvents.ts b/packages/openapi/src/routes/stakers/getStakerWithdrawalEvents.ts new file mode 100644 index 00000000..c9c0211b --- /dev/null +++ b/packages/openapi/src/routes/stakers/getStakerWithdrawalEvents.ts @@ -0,0 +1,51 @@ +import z from '../../../../api/src/schema/zod' +import { ZodOpenApiOperationObject } from 'zod-openapi' +import { openApiErrorResponses } from '../../apiResponseSchema/base/errorResponses' +import { PaginationQuerySchema } from '../../../../api/src/schema/zod/schemas/paginationQuery' +import { EthereumAddressSchema } from '../../../../api/src/schema/zod/schemas/base/ethereumAddress' +import { StakerWithdrawalEventSchema } from '../../apiResponseSchema/events/eventsRespone' +import { + refineStartEndDates, + refineWithdrawalTypeRestrictions, + refineWithEthValueRequiresTokenData, + WithdrawalEventQuerySchemaBase +} from '../../../../api/src/schema/zod/schemas/eventSchemas' +import { applyAllRefinements } from '../../apiResponseSchema/events/util' + +const StakerAddressParam = z.object({ + address: EthereumAddressSchema.describe('The address of the staker').openapi({ + example: '0x9791fdb4e9c0495efc5a1f3f9271ef226251eb34' + }) +}) + +const CombinedQuerySchemaBase = z + .object({}) + .merge(WithdrawalEventQuerySchemaBase) + .merge(PaginationQuerySchema) +const CombinedQuerySchema = applyAllRefinements(CombinedQuerySchemaBase, [ + refineStartEndDates, + refineWithEthValueRequiresTokenData, + refineWithdrawalTypeRestrictions +]) + +export const getStakerWithdrawalEvents: ZodOpenApiOperationObject = { + operationId: 'getStakerWithdrawalEvents', + summary: 'Retrieve all withdrawal events for a given staker address', + description: 'Returns a list of all withdrawal events for a given staker address.', + tags: ['Stakers'], + requestParams: { + path: StakerAddressParam, + query: CombinedQuerySchema + }, + responses: { + '200': { + description: 'The withdrawal events found for the staker.', + content: { + 'application/json': { + schema: StakerWithdrawalEventSchema + } + } + }, + ...openApiErrorResponses + } +} diff --git a/packages/openapi/src/routes/stakers/index.ts b/packages/openapi/src/routes/stakers/index.ts index fbf95bde..270beb37 100644 --- a/packages/openapi/src/routes/stakers/index.ts +++ b/packages/openapi/src/routes/stakers/index.ts @@ -6,6 +6,9 @@ import { getQueuedStakerWithdrawals } from './getQueuedStakerWithdrawals' import { getQueuedWithdrawableStakerWithdrawals } from './getQueuedWithdrawableStakerWithdrawals' import { getCompletedStakerWithdrawals } from './getCompletedStakerWithdrawals' import { getStakerDeposits } from './getStakerDeposits' +import { getStakerDelegationEvents } from './getStakerDelegationEvents' +import { getStakerDepositEvents } from './getStakerDepositEvents' +import { getStakerWithdrawalEvents } from './getStakerWithdrawalEvents' export const stakersRoutes: ZodOpenApiPathsObject = { '/stakers': { get: getAllStakers }, @@ -24,5 +27,14 @@ export const stakersRoutes: ZodOpenApiPathsObject = { }, '/stakers/{address}/deposits': { get: getStakerDeposits + }, + '/stakers/{address}/events/delegation': { + get: getStakerDelegationEvents + }, + '/stakers/{address}/events/deposit': { + get: getStakerDepositEvents + }, + '/stakers/{address}/events/withdrawal': { + get: getStakerWithdrawalEvents } } From 50000ad2626fbf388c603ed9bf223bfe0d4c6ab1 Mon Sep 17 00:00:00 2001 From: Udit Veerwani <25996904+uditdc@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:26:22 +0530 Subject: [PATCH 17/17] Remove rate limits on health route (#304)