Skip to content

Commit

Permalink
Merge pull request #323 from EigenExplorer/dev
Browse files Browse the repository at this point in the history
Dev to Main v0.3.7
  • Loading branch information
uditdc authored Jan 8, 2025
2 parents 9b47880 + ba63551 commit 9917b12
Show file tree
Hide file tree
Showing 13 changed files with 222 additions and 232 deletions.
8 changes: 4 additions & 4 deletions packages/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ NETWORK_CHAIN_WSS_URL = ""
DATABASE_URL = ""
DIRECT_URL = ""
CMC_API_KEY = ""
EE_AUTH_TOKEN = ""

# Optional for dev environments. If ommitted, API Token restrictions are lifted and no comms with dev-portal DB.
SUPABASE_SERVICE_ROLE_KEY = ""
SUPABASE_FETCH_ALL_USERS_URL = "https://<SUPABASE_PROJECT_REF>.supabase.co/functions/v1/fetch-all-users"
SUPABASE_FETCH_ACCESS_LEVEL_URL = "https://<SUPABASE_PROJECT_REF>.supabase.co/functions/v1/fetch-access-level"
SUPABASE_POST_REQUESTS_URL = "https://<SUPABASE_PROJECT_REF>.supabase.co/functions/v1/post-requests"
SUPABASE_PROJECT_REF = ""
SUPABASE_EF_SELECTORS = "a:b:c"
33 changes: 29 additions & 4 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import logger from 'morgan'
import helmet from 'helmet'
import cors from 'cors'
import apiRouter from './routes'
import { requestsStore } from './utils/authCache'
import { triggerUserRequestsSync } from './utils/requestsUpdateManager'
import { EigenExplorerApiError, handleAndReturnErrorResponse } from './schema/errors'
import { isAuthRequired, refreshAuthStore } from './utils/authMiddleware'

const PORT = process.env.PORT ? Number.parseInt(process.env.PORT) : 3002

Expand All @@ -22,12 +25,34 @@ app.use(express.json())
app.use(express.urlencoded({ extended: false }))
app.use(cookieParser())

// Route cost increment in cache for caller's API Token
if (isAuthRequired()) {
refreshAuthStore()
app.use((req, res, next) => {
res.on('finish', () => {
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
const cost = req.cost || 1
requestsStore.set(key, currentCalls + cost)
triggerUserRequestsSync(apiToken)
}
}
} catch {}
})
next()
})
}

// Routes
app.use('/', apiRouter)

app.get('/favicon.ico', (req, res) => res.sendStatus(204))

// catch 404 and forward to error handler
// Catch 404 and forward to error handler
app.use((req, res) => {
const err = new EigenExplorerApiError({
code: 'not_found',
Expand All @@ -36,13 +61,13 @@ app.use((req, res) => {
handleAndReturnErrorResponse(req, res, err)
})

// error handler
// Error handler
app.use((err: Error, req: Request, res: Response) => {
// set locals, only providing error in development
// Set locals, only providing error in development
res.locals.message = err.message
res.locals.error = req.app.get('env') === 'development' ? err : {}

// render the error page
// Render the error page
res.status(500)
res.render('error')
})
Expand Down
75 changes: 11 additions & 64 deletions packages/api/src/routes/auth/authController.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { Request, Response } from 'express'
import { handleAndReturnErrorResponse } from '../../schema/errors'
import { EigenExplorerApiError, 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 { RegisterUserBodySchema } from '../../schema/zod/schemas/auth'
import { verifyMessage } from 'viem'
import prisma from '../../utils/prismaClient'
import crypto from 'node:crypto'
Expand All @@ -16,22 +15,16 @@ import crypto from 'node:crypto'
* @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
const accessLevel = req.accessLevel || 0

if (!apiToken || apiToken !== authToken) {
throw new Error('Unauthorized access.')
if (accessLevel !== 999) {
throw new EigenExplorerApiError({ code: 'unauthorized', message: 'Unauthorized access.' })
}

const { address } = req.params
Expand Down Expand Up @@ -64,17 +57,11 @@ export async function checkUserStatus(req: Request, res: Response) {
* @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
const accessLevel = req.accessLevel || 0

if (!apiToken || apiToken !== authToken) {
throw new Error('Unauthorized access.')
if (accessLevel !== 999) {
throw new EigenExplorerApiError({ code: 'unauthorized', message: 'Unauthorized access.' })
}

const nonce = `0x${crypto.randomBytes(32).toString('hex')}`
Expand All @@ -94,11 +81,6 @@ export async function generateNonce(req: Request, res: Response) {
* @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)
Expand All @@ -110,11 +92,10 @@ export async function registerUser(req: Request, res: Response) {
}

try {
const apiToken = headerCheck.data['X-API-Token']
const authToken = process.env.EE_AUTH_TOKEN
const accessLevel = req.accessLevel || 0

if (!apiToken || apiToken !== authToken) {
throw new Error('Unauthorized access.')
if (accessLevel !== 999) {
throw new EigenExplorerApiError({ code: 'unauthorized', message: 'Unauthorized access.' })
}

const { address } = req.params
Expand Down Expand Up @@ -150,37 +131,3 @@ 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)
}
}
2 changes: 0 additions & 2 deletions packages/api/src/routes/auth/authRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
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.post('/refresh-store', 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)
Expand Down
15 changes: 4 additions & 11 deletions packages/api/src/routes/avs/avsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ 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 { EigenExplorerApiError, handleAndReturnErrorResponse } from '../../schema/errors'
import {
getStrategiesWithShareUnderlying,
sharesToTVL,
Expand Down Expand Up @@ -720,17 +719,11 @@ 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
const accessLevel = req.accessLevel || 0

if (!apiToken || apiToken !== authToken) {
throw new Error('Unauthorized access.')
if (accessLevel !== 999) {
throw new EigenExplorerApiError({ code: 'unauthorized', message: 'Unauthorized access.' })
}

const { address } = req.params
Expand Down
36 changes: 23 additions & 13 deletions packages/api/src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import express from 'express'
import express, { type Router } from 'express'
import avsRoutes from './avs/avsRoutes'
import strategiesRoutes from './strategies/strategiesRoutes'
import operatorRoutes from './operators/operatorRoutes'
Expand All @@ -10,10 +10,14 @@ 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'
import { authenticator, isAuthRequired, rateLimiter } from '../utils/authMiddleware'

const apiRouter = express.Router()

const setMiddleware = (router: Router) => {
return isAuthRequired() ? [authenticator, rateLimiter, router] : [router]
}

// Health route
apiRouter.get('/health', (_, res) => res.send({ status: 'ok' }))

Expand All @@ -23,16 +27,22 @@ apiRouter.get('/version', (_, res) =>
)

// Remaining routes
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('/events', authenticator, rateLimiter, eventRoutes)
apiRouter.use('/auth', authenticator, rateLimiter, authRoutes)
const routes = {
'/avs': avsRoutes,
'/strategies': strategiesRoutes,
'/operators': operatorRoutes,
'/stakers': stakerRoutes,
'/metrics': metricRoutes,
'/withdrawals': withdrawalRoutes,
'/deposits': depositRoutes,
'/aux': auxiliaryRoutes,
'/rewards': rewardRoutes,
'/events': eventRoutes,
'/auth': authRoutes
}

for (const [path, router] of Object.entries(routes)) {
apiRouter.use(path, ...setMiddleware(router))
}

export default apiRouter
15 changes: 4 additions & 11 deletions packages/api/src/routes/operators/operatorController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import {
OperatorDelegationEventQuerySchema,
OperatorRegistrationEventQuerySchema
} from '../../schema/zod/schemas/eventSchemas'
import { RequestHeadersSchema } from '../../schema/zod/schemas/auth'
import { handleAndReturnErrorResponse } from '../../schema/errors'
import { EigenExplorerApiError, handleAndReturnErrorResponse } from '../../schema/errors'
import {
getStrategiesWithShareUnderlying,
sharesToTVL,
Expand Down Expand Up @@ -485,17 +484,11 @@ 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
const accessLevel = req.accessLevel || 0

if (!apiToken || apiToken !== authToken) {
throw new Error('Unauthorized access.')
if (accessLevel !== 999) {
throw new EigenExplorerApiError({ code: 'unauthorized', message: 'Unauthorized access.' })
}

const { address } = req.params
Expand Down
13 changes: 0 additions & 13 deletions packages/api/src/schema/zod/schemas/auth.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
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)
Expand Down
11 changes: 4 additions & 7 deletions packages/api/src/utils/authCache.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
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.
* Cache that stores `accessLevel` & `accountRestricted` per API token
*
*/
export const authStore = new NodeCache({ stdTTL: 7 * 24 * 60 * 60 }) // 1 week
refreshAuthStore()
export const authStore = new NodeCache({ stdTTL: 60 * 60 }) // 1 hour

/**
* Init cache that collects `newRequests` per api token.
* Cache that collects `newRequests` per API token
*
*/
export const requestsStore = new NodeCache({ stdTTL: 7 * 24 * 60 * 60 }) // 1 week
export const requestsStore = new NodeCache({ stdTTL: 24 * 60 * 60 }) // 1 day
Loading

0 comments on commit 9917b12

Please sign in to comment.