diff --git a/packages/api/src/routes/auth/authController.ts b/packages/api/src/routes/auth/authController.ts new file mode 100644 index 00000000..8c1d31b7 --- /dev/null +++ b/packages/api/src/routes/auth/authController.ts @@ -0,0 +1,85 @@ +import type { Request, Response } from 'express' +import { handleAndReturnErrorResponse } from '../../schema/errors' +import { EthereumAddressSchema } from '../../schema/zod/schemas/base/ethereumAddress' +import { RequestHeadersSchema } from '../../schema/zod/schemas/auth' +import prisma from '../../utils/prismaClient' + +/** + * Function for route /auth/users/:address/check-status + * 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 paramCheck = EthereumAddressSchema.safeParse(req.params.address) + if (!paramCheck.success) { + return handleAndReturnErrorResponse(req, res, paramCheck.error) + } + + try { + const { address } = req.params + + const user = await prisma.user.findUnique({ + where: { address: address.toLowerCase() }, + include: { staker: true } + }) + + const isRegistered = !!user + const isStaker = !!user?.staker + const isTracked = !!user?.isTracked + + res.send({ isRegistered, isStaker, isTracked }) + } 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 paramCheck = EthereumAddressSchema.safeParse(req.params.address) + if (!paramCheck.success) { + 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 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..93b211e6 --- /dev/null +++ b/packages/api/src/routes/auth/authRoutes.ts @@ -0,0 +1,12 @@ +import express from 'express' +import routeCache from 'route-cache' +import { checkUserStatus, registerUser } from './authController' + +const router = express.Router() + +// API routes for /auth + +router.get('/users/:address/check-status', routeCache.cacheSeconds(30), checkUserStatus) +router.post('/users/:address/register', routeCache.cacheSeconds(240), registerUser) + +export default router diff --git a/packages/api/src/routes/avs/avsController.ts b/packages/api/src/routes/avs/avsController.ts index 14c93252..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 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..e908cdf1 --- /dev/null +++ b/packages/api/src/schema/zod/schemas/auth.ts @@ -0,0 +1,14 @@ +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] + } + : {} + }) diff --git a/packages/prisma/migrations/20241115112149_include_staker_rewards/migration.sql b/packages/prisma/migrations/20241123154805_include_staker_rewards/migration.sql similarity index 94% rename from packages/prisma/migrations/20241115112149_include_staker_rewards/migration.sql rename to packages/prisma/migrations/20241123154805_include_staker_rewards/migration.sql index 771543d6..a94f293c 100644 --- a/packages/prisma/migrations/20241115112149_include_staker_rewards/migration.sql +++ b/packages/prisma/migrations/20241123154805_include_staker_rewards/migration.sql @@ -26,6 +26,7 @@ CREATE TABLE "StakerTokenRewards" ( -- 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") @@ -49,6 +50,9 @@ CREATE TABLE "EventLogs_DistributionRootSubmitted" ( -- CreateIndex CREATE INDEX "StakerTokenRewards_stakerAddress_idx" ON "StakerTokenRewards"("stakerAddress"); +-- CreateIndex +CREATE INDEX "User_address_idx" ON "User"("address"); + -- CreateIndex CREATE INDEX "EventLogs_DistributionRootSubmitted_rewardsCalculationEndTi_idx" ON "EventLogs_DistributionRootSubmitted"("rewardsCalculationEndTimestamp"); diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 688b5131..9b6002d8 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -190,9 +190,12 @@ model StakerTokenRewards { model User { address String @id + isTracked Boolean createdAt DateTime @default(now()) staker Staker? @relation(fields: [address], references: [address]) + + @@index([address]) } model Deposit { diff --git a/packages/seeder/src/seedStakerTokenRewards.ts b/packages/seeder/src/seedStakerTokenRewards.ts index 8c8dd7dd..f4f40914 100644 --- a/packages/seeder/src/seedStakerTokenRewards.ts +++ b/packages/seeder/src/seedStakerTokenRewards.ts @@ -266,6 +266,7 @@ async function writeToDb( const dbTransactions: any[] = [] if (action === 'update') { + // Delete all existing snapshots of tracked Stakers dbTransactions.push( prismaClient.stakerTokenRewards.deleteMany({ where: { @@ -275,8 +276,23 @@ async function writeToDb( } }) ) + } else { + // Mark User as tracked + dbTransactions.push( + prismaClient.user.updateMany({ + where: { + address: { + in: batch.map((record) => record.stakerAddress) + } + }, + data: { + isTracked: true + } + }) + ) } + // Write all snapshots to db dbTransactions.push( prismaClient.stakerTokenRewards.createMany({ data: batch,