diff --git a/api/relay/_queue.ts b/api/relay/_queue.ts index c008aa23b..a9e7928ab 100644 --- a/api/relay/_queue.ts +++ b/api/relay/_queue.ts @@ -10,11 +10,9 @@ const client = new Client({ export async function pushRelayRequestToQueue({ request, strategy, - requestId, }: { request: RelayRequest; strategy: RelayStrategy; - requestId: string; }) { const strategyName = strategy.strategyName; const queue = getRelayRequestQueue(strategyName, request.chainId); @@ -24,11 +22,13 @@ export async function pushRelayRequestToQueue({ const baseUrl = resolveVercelEndpoint(true); const response = await queue.enqueueJSON({ + headers: new Headers({ + "Upstash-Content-Based-Deduplication": "true", + }), url: `${baseUrl}/api/relay/jobs/process`, // callbackUrl: `${baseUrl}/api/relay/jobs/success`, // failureCallbackUrl: `${baseUrl}/api/relay/jobs/failure`, body: { - requestId, request, strategyName, }, diff --git a/api/relay/_utils.ts b/api/relay/_utils.ts index 55659e7a8..df3356e7a 100644 --- a/api/relay/_utils.ts +++ b/api/relay/_utils.ts @@ -197,39 +197,46 @@ export function encodeCalldataForRelayRequest(request: RelayRequest) { return encodedCalldata; } +export function getRelayRequestHash(request: RelayRequest) { + return utils.keccak256( + utils.defaultAbiCoder.encode( + ["bytes", "bytes"], + [request.signatures.permit, request.signatures.deposit] + ) + ); +} + type CachedRelayRequest = | { status: "pending"; request: RelayRequest; - requestId: string; + messageId: string; } | { status: "success"; request: RelayRequest; txHash: string; - requestId: string; + messageId: string; } | { status: "failure"; request: RelayRequest; error: Error; - requestId: string; + messageId: string; } | { status: "unknown"; - requestId: string; }; export async function getCachedRelayRequest( - requestId: string + requestOrHash: RelayRequest | string ): Promise { const cachedRelayRequest = await redisCache.get( - getRelayRequestCacheKey(requestId) + getRelayRequestCacheKey(requestOrHash) ); if (!cachedRelayRequest) { return { - requestId, status: "unknown", }; } @@ -238,14 +245,14 @@ export async function getCachedRelayRequest( } export async function setCachedRelayRequestPending(params: { - requestId: string; + messageId: string; request: RelayRequest; }) { await redisCache.set( - getRelayRequestCacheKey(params.requestId), + getRelayRequestCacheKey(params.request), { status: "pending", - requestId: params.requestId, + messageId: params.messageId, request: params.request, }, 60 * 60 * 24 // 1 day @@ -253,16 +260,27 @@ export async function setCachedRelayRequestPending(params: { } export async function setCachedRelayRequestFailure(params: { - requestId: string; request: RelayRequest; error: Error; }) { + const cachedRelayRequest = await getCachedRelayRequest(params.request); + + if (!cachedRelayRequest || cachedRelayRequest.status === "unknown") { + throw new Error("Request not found in cache"); + } + + if (cachedRelayRequest.status !== "pending") { + throw new Error( + "Can not set 'failure' status for request that is not pending" + ); + } + await redisCache.set( - getRelayRequestCacheKey(params.requestId), + getRelayRequestCacheKey(params.request), { status: "failure", - requestId: params.requestId, - request: params.request, + messageId: cachedRelayRequest.messageId, + request: cachedRelayRequest.request, error: params.error, }, 60 * 60 * 24 // 1 day @@ -270,22 +288,37 @@ export async function setCachedRelayRequestFailure(params: { } export async function setCachedRelayRequestSuccess(params: { - requestId: string; request: RelayRequest; txHash: string; }) { + const cachedRelayRequest = await getCachedRelayRequest(params.request); + + if (!cachedRelayRequest || cachedRelayRequest.status === "unknown") { + throw new Error("Request not found in cache"); + } + + if (cachedRelayRequest.status !== "pending") { + throw new Error( + "Can not set 'success' status for request that is not pending" + ); + } + await redisCache.set( - getRelayRequestCacheKey(params.requestId), + getRelayRequestCacheKey(params.request), { status: "success", - requestId: params.requestId, - request: params.request, + messageId: cachedRelayRequest.messageId, + request: cachedRelayRequest.request, txHash: params.txHash, }, 60 * 60 * 24 // 1 day ); } -function getRelayRequestCacheKey(requestId: string) { - return `relay-request:${requestId}`; +function getRelayRequestCacheKey(requestOrHash: RelayRequest | string) { + const requestHash = + typeof requestOrHash === "string" + ? requestOrHash + : getRelayRequestHash(requestOrHash); + return `relay-request:${requestHash}`; } diff --git a/api/relay/index.ts b/api/relay/index.ts index b637b53f0..c68beb93a 100644 --- a/api/relay/index.ts +++ b/api/relay/index.ts @@ -71,16 +71,19 @@ export default async function handler( methodNameAndArgs, signatures, }; - const queueResponse = await pushRelayRequestToQueue(relayRequest, strategy); + const queueResponse = await pushRelayRequestToQueue({ + request: relayRequest, + strategy, + }); // Store requestId in database await setCachedRelayRequestPending({ - requestId: queueResponse.messageId, + messageId: queueResponse.messageId, request: relayRequest, }); response.status(200).json({ - requestId: queueResponse.messageId, + messageId: queueResponse.messageId, }); } catch (error) { return handleErrorCondition("api/relay", response, logger, error); diff --git a/api/relay/jobs/process.ts b/api/relay/jobs/process.ts index ea874f237..8fac665e1 100644 --- a/api/relay/jobs/process.ts +++ b/api/relay/jobs/process.ts @@ -1,14 +1,15 @@ import { VercelRequest, VercelResponse } from "@vercel/node"; -import { assert, enums, type, string } from "superstruct"; +import { assert, enums, type } from "superstruct"; import { Receiver } from "@upstash/qstash"; -import { handleErrorCondition } from "../../_errors"; +import { handleErrorCondition, InvalidParamError } from "../../_errors"; import { getLogger } from "../../_utils"; import { validateMethodArgs, verifySignatures, setCachedRelayRequestSuccess, setCachedRelayRequestFailure, + getCachedRelayRequest, } from "../_utils"; import { RelayRequest } from "../_types"; import { strategiesByName } from "../_strategies"; @@ -21,7 +22,6 @@ const messageReceiver = new Receiver({ const RelayProcessJobBodySchema = type({ strategyName: enums(Object.keys(strategiesByName)), - requestId: string(), request: BaseRelayRequestBodySchema, }); @@ -50,7 +50,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { } assert(req.body, RelayProcessJobBodySchema); - const { request, strategyName, requestId } = req.body; + const { request, strategyName } = req.body; // Validate method-specific request body const methodNameAndArgs = validateMethodArgs( @@ -75,27 +75,37 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { signatures, }; + // Get cached request + const cachedRequest = await getCachedRelayRequest(relayRequest); + + if (!cachedRequest || cachedRequest.status !== "pending") { + throw new InvalidParamError({ + param: "request", + message: "Request not found in cache or is not pending", + }); + } + + const { messageId } = cachedRequest; + // Handle request via strategy try { const txHash = await strategy.relay(relayRequest); // Store requestId in database await setCachedRelayRequestSuccess({ - requestId, request: relayRequest, txHash, }); res.status(200).json({ - requestId, + messageId, txHash, }); } catch (error) { await setCachedRelayRequestFailure({ - requestId, request: relayRequest, error: error as Error, }); res.status(500).json({ - requestId, + messageId, error: error as Error, }); } diff --git a/api/relay/status.ts b/api/relay/status.ts index 30824a544..0a69f12bd 100644 --- a/api/relay/status.ts +++ b/api/relay/status.ts @@ -1,13 +1,13 @@ import { VercelResponse } from "@vercel/node"; -import { assert, type, string, Infer } from "superstruct"; +import { assert, type, Infer } from "superstruct"; import { handleErrorCondition } from "../_errors"; -import { getLogger } from "../_utils"; +import { getLogger, hexString } from "../_utils"; import { getCachedRelayRequest } from "./_utils"; import { TypedVercelRequest } from "../_types"; const RelayRequestStatusSchema = type({ - requestId: string(), + requestHash: hexString(), }); type RelayRequestStatusType = Infer; @@ -27,7 +27,7 @@ export default async function handler( assert(request.query, RelayRequestStatusSchema); const cachedRelayRequest = await getCachedRelayRequest( - request.query.requestId + request.query.requestHash ); response.status(200).json(cachedRelayRequest);