Skip to content

Commit ddae908

Browse files
committed
add api key
1 parent 6fb6e54 commit ddae908

File tree

8 files changed

+95
-21
lines changed

8 files changed

+95
-21
lines changed

.env.template

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ CORS_ORIGIN="*" # Allowed CORS origin, adjust as necessary
77

88
# Rate Limiting
99
COMMON_RATE_LIMIT_WINDOW_MS="1000" # Window size for rate limiting (ms)
10-
COMMON_RATE_LIMIT_MAX_REQUESTS="20" # Max number of requests per window per IP
10+
COMMON_RATE_LIMIT_MAX_REQUESTS="20" # Max number of requests per window per API key
1111

1212
# RPCs
1313
# mainnet
@@ -32,4 +32,9 @@ OKX_API_KEY=""
3232
OKX_PASSPHRASE=""
3333
OKX_SECRET_KEY=""
3434
ODOS_API_KEY=""
35-
ODOS_REFERRAL_CODE=""
35+
ODOS_REFERRAL_CODE=""
36+
37+
# API keys
38+
API_KEY_1=""
39+
API_KEY_2=""
40+
# ...

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ pnpm run lint:fix # fix
3939

4040
Swagger UI is served at the root, it is also available at [swap.euler.finance](https://swap.euler.finance). Request and response schemas are also available [here](./src/api/routes/swap/swapModel.ts)
4141

42+
To try out the `swap` call in Swagger use string `example_api_key` as the `x-api-key` header value.
43+
44+
## Authorization and rate limits
45+
46+
The `/swap` endpoint is protected with an API key expected in `x-api-key` header. Accepted keys are configured through environment variables starting with `API_KEY_`.
47+
48+
Rate limits can be configured with environment variables:
49+
- `COMMON_RATE_LIMIT_MAX_REQUESTS` defines the max number of requests allowed per API key, per window.
50+
- `COMMON_RATE_LIMIT_WINDOW_MS` defines window duration in miliseconds.
51+
52+
The example key is allowed 1 request per window.
53+
4254
## Fetching quotes and executing trades
4355

4456
The `/swap` endpoint fetches token trade quotes which can be used with the swapping peripheries in the Euler Vault Kit [periphery contracts](https://github.com/euler-xyz/evk-periphery/tree/master/src/Swaps). See [periphery docs](https://github.com/euler-xyz/evk-periphery/blob/master/docs/swaps.md) for detailed description of the swapping architecture in Euler V2. The API response includes both encoded payloads as well as raw data for calls to the `Swapper` (`swap` field of the response) and `SwapVerifier` (`verify` field) contracts. These payloads can be used directly in EVC batches.

src/api/routes/swap/swapModel.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { EXAMPLE_API_KEY } from "@/common/constants"
12
import { SwapVerificationType, SwapperMode } from "@/swapService/interface"
23
import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"
34
import { InvalidAddressError, getAddress, isHex } from "viem"
@@ -133,6 +134,16 @@ const swapApiResponseSwapSchema = z.object({
133134
.openapi({ description: "Raw Swapper multicall items" }),
134135
})
135136

137+
const getSwapHeadersSchema = z.object({
138+
"x-api-key": z.string().openapi({
139+
param: {
140+
description:
141+
"API key. You can use the example key provided to try this call out",
142+
},
143+
example: EXAMPLE_API_KEY,
144+
}),
145+
})
146+
136147
const getSwapSchema = z.object({
137148
query: z.object({
138149
chainId: z
@@ -320,4 +331,4 @@ const swapResponseSchema = z.object({
320331
.openapi({ description: "Swap route details" }),
321332
})
322333

323-
export { getSwapSchema, swapResponseSchema }
334+
export { getSwapSchema, getSwapHeadersSchema, swapResponseSchema }

src/api/routes/swap/swapRouter.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import express, { type Router, type Request, type Response } from "express"
33

44
import { createApiResponse } from "@/api-docs/openAPIResponseBuilders"
55

6+
import { apiKeyAuth } from "@/common/middleware/apiKey"
67
import { ServiceResponse } from "@/common/models/serviceResponse"
78
import {
89
handleServiceResponse,
@@ -21,6 +22,7 @@ import { InvalidAddressError, isHex } from "viem"
2122
import { z } from "zod"
2223
import {
2324
type SwapResponse,
25+
getSwapHeadersSchema,
2426
getSwapSchema,
2527
swapResponseSchema,
2628
} from "./swapModel"
@@ -33,17 +35,17 @@ swapRegistry.registerPath({
3335
method: "get",
3436
path: "/swap",
3537
tags: ["Get swap quote"],
36-
request: { query: getSwapSchema.shape.query },
38+
request: { query: getSwapSchema.shape.query, headers: getSwapHeadersSchema },
3739
responses: createApiResponse(swapResponseSchema, "Success"),
3840
})
3941

4042
swapRouter.get(
4143
"/",
42-
validateRequest(getSwapSchema),
44+
[validateRequest(getSwapSchema), apiKeyAuth],
4345
async (req: Request, res: Response) => {
4446
const serviceResponse = await findSwap(req)
4547
console.log("===== END =====")
46-
return handleServiceResponse(serviceResponse, res)
48+
return handleServiceResponse(serviceResponse, req, res)
4749
},
4850
)
4951

@@ -70,14 +72,6 @@ async function findSwap(
7072

7173
return ServiceResponse.success<SwapResponse>(data)
7274
} catch (error) {
73-
console.log(
74-
"error: ",
75-
error.statusCode,
76-
error.message,
77-
error.errorMessage,
78-
JSON.stringify(error.data),
79-
req.url,
80-
)
8175
if (error instanceof ApiError) {
8276
return ServiceResponse.failure(
8377
error.message,

src/common/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const EXAMPLE_API_KEY = "example_api_key"

src/common/middleware/apiKey.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { Handler, NextFunction, Request, Response } from "express"
2+
import { StatusCodes } from "http-status-codes"
3+
import { EXAMPLE_API_KEY } from "../constants"
4+
import { ServiceResponse } from "../models/serviceResponse"
5+
import { handleServiceResponse } from "../utils/httpHandlers"
6+
7+
const apiKeysFromEnv = (regExp: RegExp): string[] =>
8+
Object.keys(process.env)
9+
.filter((key) => regExp.test(key))
10+
.map((key) => process.env[key] as string)
11+
12+
export const apiKeyAuth: Handler = (
13+
req: Request,
14+
res: Response,
15+
next: NextFunction,
16+
) => {
17+
const apiKeys = apiKeysFromEnv(/^API_KEY_/)
18+
apiKeys.push(EXAMPLE_API_KEY)
19+
20+
const apiKey = req.header("x-api-key")
21+
if (!apiKey || !apiKeys.includes(apiKey)) {
22+
const serviceResponse = ServiceResponse.failure(
23+
"Unauthorized",
24+
StatusCodes.UNAUTHORIZED,
25+
{ ip: req.ip },
26+
)
27+
handleServiceResponse(serviceResponse, req, res)
28+
}
29+
next()
30+
}

src/common/middleware/rateLimiter.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import type { Request } from "express"
22
import { rateLimit } from "express-rate-limit"
3+
import { EXAMPLE_API_KEY } from "../constants"
34

45
const rateLimiter = rateLimit({
56
legacyHeaders: true,
6-
limit: Number(process.env.COMMON_RATE_LIMIT_MAX_REQUESTS || 20),
7+
limit: (req: Request) =>
8+
req.headers["x-api-key"] === EXAMPLE_API_KEY
9+
? 1
10+
: Number(process.env.COMMON_RATE_LIMIT_MAX_REQUESTS || 20),
711
message: "Too many requests, please try again later.",
812
standardHeaders: true,
913
windowMs: Number(process.env.COMMON_RATE_LIMIT_WINDOW_MS || 1000),
10-
keyGenerator: (req: Request) => req.ip as string,
14+
keyGenerator: (req: Request) => req.headers["x-api-key"] as string,
1115
})
1216

1317
export default rateLimiter

src/common/utils/httpHandlers.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,42 @@
1-
import type { NextFunction, Request, Response } from "express"
1+
import type { NextFunction, Request, RequestHandler, Response } from "express"
22
import { StatusCodes } from "http-status-codes"
33
import type { ZodError, ZodSchema } from "zod"
44

55
import { ServiceResponse } from "@/common/models/serviceResponse"
66

77
export const handleServiceResponse = (
88
serviceResponse: ServiceResponse<any>,
9+
request: Request,
910
response: Response,
1011
) => {
11-
return response.status(serviceResponse.statusCode).send(serviceResponse)
12+
if (serviceResponse.statusCode !== StatusCodes.OK) {
13+
console.log(
14+
"error: ",
15+
serviceResponse.statusCode,
16+
serviceResponse.message,
17+
JSON.stringify(serviceResponse.data),
18+
request.url,
19+
)
20+
}
21+
console.log(serviceResponse)
22+
response.status(serviceResponse.statusCode).send(serviceResponse)
1223
}
1324

1425
export const validateRequest =
15-
(schema: ZodSchema) => (req: Request, res: Response, next: NextFunction) => {
26+
(schema: ZodSchema): RequestHandler =>
27+
(req: Request, res: Response, next: NextFunction) => {
1628
try {
17-
schema.parse({ body: req.body, query: req.query, params: req.params })
29+
schema.parse({
30+
body: req.body,
31+
query: req.query,
32+
params: req.params,
33+
headers: req.headers,
34+
})
1835
next()
1936
} catch (err) {
2037
const errorMessage = `Invalid input: ${(err as ZodError).errors.map((e) => e.message).join(", ")}`
2138
const statusCode = StatusCodes.BAD_REQUEST
2239
const serviceResponse = ServiceResponse.failure(errorMessage, statusCode)
23-
return handleServiceResponse(serviceResponse, res)
40+
return handleServiceResponse(serviceResponse, req, res)
2441
}
2542
}

0 commit comments

Comments
 (0)