Skip to content

Commit

Permalink
Merge branch 'epic/relayer-config' into pxrl/updateFormat
Browse files Browse the repository at this point in the history
  • Loading branch information
pxrl authored Dec 12, 2024
2 parents c0ba044 + b6d0b7b commit 27fa55f
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 16 deletions.
38 changes: 37 additions & 1 deletion api/_exclusivity/utils.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -66,3 +85,20 @@ export async function updateLimits(
}))
);
}

export async function getLimits(
relayer: string,
originChainId: number,
destinationChainId: number,
inputToken: string,
outputToken: string
): Promise<RelayerFillLimit[]> {
const cachedLimits = await getCachedRelayerFillLimit(
relayer,
originChainId,
destinationChainId,
inputToken,
outputToken
);
return cachedLimits ?? [];
}
28 changes: 27 additions & 1 deletion api/_types/exclusivity.types.ts
Original file line number Diff line number Diff line change
@@ -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 RelayerRoute = object({
Expand Down Expand Up @@ -40,6 +45,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[] = [
// {
Expand Down
61 changes: 50 additions & 11 deletions api/relayer-config.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,67 @@
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
) => {
if (request.method !== "POST") {
return response.status(405).end(`Method ${request.method} Not Allowed`);
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
) => {
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 {
Expand All @@ -30,16 +72,13 @@ const handler = async (
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" });
}

Expand Down
51 changes: 48 additions & 3 deletions test/api/relayer-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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),
originChainId: "1",
inputToken: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
destinationChainId: "42161",
Expand Down Expand Up @@ -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,
originChainId: "1",
inputToken: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
destinationChainId: "10",
Expand All @@ -90,7 +93,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),
originChainId: "1",
inputToken: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
destinationChainId: "10",
Expand All @@ -114,4 +117,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);
});
});

0 comments on commit 27fa55f

Please sign in to comment.