From b6d0b7bc02a1506bb0d876be69847c63aaa871b4 Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Thu, 12 Dec 2024 12:39:36 -0600 Subject: [PATCH] feat: relayer config get (#1330) Signed-off-by: Pablo Maldonado --- api/_exclusivity/utils.ts | 38 +++++++++++++++++++- api/_types/exclusivity.types.ts | 28 ++++++++++++++- api/relayer-config.ts | 61 +++++++++++++++++++++++++++------ test/api/relayer-config.test.ts | 51 +++++++++++++++++++++++++-- 4 files changed, 162 insertions(+), 16 deletions(-) diff --git a/api/_exclusivity/utils.ts b/api/_exclusivity/utils.ts index ca7b9ad37..1202d7ef7 100644 --- a/api/_exclusivity/utils.ts +++ b/api/_exclusivity/utils.ts @@ -1,6 +1,11 @@ import { ethers } from "ethers"; import { bnZero } from "../../src/utils/sdk"; -import { RelayerFillLimit } from "../_types"; +import { + ConfigUpdateGet, + RelayerConfigUpdate, + RelayerFillLimit, +} from "../_types"; +import { getCachedRelayerFillLimit } from "./cache"; import { setCachedRelayerFillLimit } from "./cache"; export const MAX_MESSAGE_AGE_SECONDS = 300; @@ -16,6 +21,20 @@ export const getRelayerFromSignature = (signature: string, message: string) => { return ethers.utils.verifyMessage(message, signature); }; +export const authenticateRelayer = ( + authorization: string | undefined, + body: RelayerConfigUpdate | ConfigUpdateGet +) => { + if (!authorization) { + return null; + } + const relayer = getRelayerFromSignature(authorization, JSON.stringify(body)); + if (getWhiteListedRelayers().includes(relayer)) { + return relayer; + } + return null; +}; + export const isTimestampValid = ( timestamp: number, maxAgeSeconds: number @@ -58,3 +77,20 @@ export async function updateLimits( })) ); } + +export async function getLimits( + relayer: string, + originChainId: number, + destinationChainId: number, + inputToken: string, + outputToken: string +): Promise { + const cachedLimits = await getCachedRelayerFillLimit( + relayer, + originChainId, + destinationChainId, + inputToken, + outputToken + ); + return cachedLimits ?? []; +} diff --git a/api/_types/exclusivity.types.ts b/api/_types/exclusivity.types.ts index 17f1cba62..b0bcf631c 100644 --- a/api/_types/exclusivity.types.ts +++ b/api/_types/exclusivity.types.ts @@ -1,5 +1,10 @@ import { array, boolean, Infer, object, optional } from "superstruct"; -import { positiveFloatStr, positiveIntStr, validAddress } from "../_utils"; +import { + positiveFloatStr, + positiveInt, + positiveIntStr, + validAddress, +} from "../_utils"; import { TypedVercelRequest } from "./generic.types"; export const RelayerFillLimitSchema = object({ @@ -29,6 +34,27 @@ export type TypedRelayerConfigUpdateRequest = TypedVercelRequest< RelayerConfigUpdate >; +export type ConfigUpdateGet = { + timestamp: number; + originChainId: string; + destinationChainId: string; + inputToken: string; + outputToken: string; +}; + +export const ConfigUpdateGetSchema = object({ + timestamp: positiveInt, + originChainId: positiveIntStr(), + destinationChainId: positiveIntStr(), + inputToken: validAddress(), + outputToken: validAddress(), +}); + +export type TypedRelayerConfigUpdateGetRequest = TypedVercelRequest< + ConfigUpdateGet, + never +>; + // // Example config. // export const RelayerConfigUpdate: RelayerFillLimit[] = [ // { diff --git a/api/relayer-config.ts b/api/relayer-config.ts index 52fc8e837..af4e9e792 100644 --- a/api/relayer-config.ts +++ b/api/relayer-config.ts @@ -1,38 +1,77 @@ import { VercelResponse } from "@vercel/node"; import { - getRelayerFromSignature, - getWhiteListedRelayers, + authenticateRelayer, + getLimits, isTimestampValid, MAX_MESSAGE_AGE_SECONDS, updateLimits, } from "./_exclusivity/utils"; import { + ConfigUpdateGetSchema, RelayerConfigUpdate, RelayerFillLimitArraySchema, + TypedRelayerConfigUpdateGetRequest, TypedRelayerConfigUpdateRequest, } from "./_types"; const handler = async ( - request: TypedRelayerConfigUpdateRequest, + request: TypedRelayerConfigUpdateRequest | TypedRelayerConfigUpdateGetRequest, + response: VercelResponse +) => { + switch (request.method) { + case "GET": + return handleGet(request as TypedRelayerConfigUpdateGetRequest, response); + case "POST": + return handlePost(request as TypedRelayerConfigUpdateRequest, response); + default: + return response.status(405).end(`Method ${request.method} Not Allowed`); + } +}; + +const handleGet = async ( + request: TypedRelayerConfigUpdateGetRequest, response: VercelResponse ) => { - if (request.method !== "POST") { - return response.status(405).end(`Method ${request.method} Not Allowed`); + const { authorization } = request.headers; + + const [error, query] = ConfigUpdateGetSchema.validate(request.query); + if (error) { + return response + .status(400) + .json({ message: "Invalid configuration payload" }); + } + + const relayer = authenticateRelayer(authorization, query); + if (!relayer) { + return response.status(401).json({ message: "Unauthorized" }); } + const { originChainId, destinationChainId, inputToken, outputToken } = query; + + const limits = await getLimits( + relayer, + Number(originChainId), + Number(destinationChainId), + inputToken, + outputToken + ); + return response.status(200).json(limits); +}; + +const handlePost = async ( + request: TypedRelayerConfigUpdateRequest, + response: VercelResponse +) => { const body = request.body as RelayerConfigUpdate; const { authorization } = request.headers; const { relayerFillLimits, timestamp } = body; + if (!isTimestampValid(timestamp, MAX_MESSAGE_AGE_SECONDS)) { return response.status(400).json({ message: "Message too old" }); } - if (!authorization) { - return response.status(401).json({ message: "Unauthorized" }); - } - const relayer = getRelayerFromSignature(authorization, JSON.stringify(body)); - - if (!getWhiteListedRelayers().includes(relayer)) { + const relayer = authenticateRelayer(authorization, body); + if (!relayer) { return response.status(401).json({ message: "Unauthorized" }); } diff --git a/test/api/relayer-config.test.ts b/test/api/relayer-config.test.ts index 24852dad8..78967aadf 100644 --- a/test/api/relayer-config.test.ts +++ b/test/api/relayer-config.test.ts @@ -4,6 +4,9 @@ import * as utils from "../../api/_exclusivity/utils"; import { RelayerConfigUpdate, TypedRelayerConfigUpdateRequest, + TypedRelayerConfigUpdateGetRequest, + ConfigUpdateGet, + RelayerFillLimit, } from "../../api/_types"; import handler from "../../api/relayer-config"; const { MAX_MESSAGE_AGE_SECONDS } = utils; @@ -30,7 +33,7 @@ describe("Relayer Config API", () => { test("POST request with valid timestamp", async () => { const message: RelayerConfigUpdate = { - timestamp: Date.now() / 1000, + timestamp: Math.floor(Date.now() / 1000), relayerFillLimits: [ { originChainId: "1", @@ -63,7 +66,7 @@ describe("Relayer Config API", () => { test("POST request with invalid timestamp", async () => { const message: RelayerConfigUpdate = { - timestamp: Date.now() / 1000 - MAX_MESSAGE_AGE_SECONDS - 1, + timestamp: Math.floor(Date.now() / 1000) - MAX_MESSAGE_AGE_SECONDS - 1, relayerFillLimits: [], }; const signature = await whitelistedRelayer.signMessage( @@ -86,7 +89,7 @@ describe("Relayer Config API", () => { test("POST request with invalid signature", async () => { const message: RelayerConfigUpdate = { - timestamp: Date.now() / 1000, + timestamp: Math.floor(Date.now() / 1000), relayerFillLimits: [], }; const signature = await unauthorizedRelayer.signMessage( @@ -106,4 +109,46 @@ describe("Relayer Config API", () => { expect(response.status).toHaveBeenCalledWith(401); expect(response.json).toHaveBeenCalledWith({ message: "Unauthorized" }); }); + + test("GET request with valid signature and query params", async () => { + const query: ConfigUpdateGet = { + timestamp: Math.floor(Date.now() / 1000), + originChainId: "1", + destinationChainId: "42161", + inputToken: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + outputToken: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + }; + const signature = await whitelistedRelayer.signMessage( + JSON.stringify(query) + ); + + const request = { + method: "GET", + headers: { + authorization: signature, + }, + query, + } as TypedRelayerConfigUpdateGetRequest; + + // Mock getLimits to return some test data + const limits: RelayerFillLimit[] = [ + { + originChainId: "1", + destinationChainId: "42161", + inputToken: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + outputToken: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + minOutputAmount: "1", + maxOutputAmount: "2", + balanceMultiplier: "1", + minProfitThreshold: "0.0001", + minExclusivityPeriod: "1", + }, + ]; + jest.spyOn(utils, "getLimits").mockResolvedValue(limits); + + await handler(request, response); + + expect(response.status).toHaveBeenCalledWith(200); + expect(response.json).toHaveBeenCalledWith(limits); + }); });