From b50d215b9c22cc61f81a11cd9b6838291fa8030b Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Mon, 23 Dec 2024 16:55:38 +0100 Subject: [PATCH] feat: add base /relay handler --- api/relay/_utils.ts | 175 +++++++++++++++++++++++++++++++++++ api/relay/index.ts | 65 +++++++++++++ scripts/tests/swap-permit.ts | 42 ++++++++- 3 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 api/relay/_utils.ts create mode 100644 api/relay/index.ts diff --git a/api/relay/_utils.ts b/api/relay/_utils.ts new file mode 100644 index 000000000..f46783d63 --- /dev/null +++ b/api/relay/_utils.ts @@ -0,0 +1,175 @@ +import { assert, Infer, type } from "superstruct"; +import { utils } from "ethers"; + +import { hexString, positiveIntStr, validAddress } from "../_utils"; +import { getPermitTypedData } from "../_permit"; +import { InvalidParamError } from "../_errors"; +import { + getDepositTypedData, + getSwapAndDepositTypedData, +} from "../_spoke-pool-periphery"; + +export const GAS_SPONSOR_ADDRESS = "0x0000000000000000000000000000000000000000"; + +const SubmissionFeesSchema = type({ + amount: positiveIntStr(), + recipient: validAddress(), +}); + +const BaseDepositDataSchema = type({ + inputToken: validAddress(), + outputToken: validAddress(), + outputAmount: positiveIntStr(), + depositor: validAddress(), + recipient: validAddress(), + destinationChainId: positiveIntStr(), + exclusiveRelayer: validAddress(), + quoteTimestamp: positiveIntStr(), + fillDeadline: positiveIntStr(), + exclusivityParameter: positiveIntStr(), + message: hexString(), +}); + +const SwapAndDepositDataSchema = type({ + submissionFees: SubmissionFeesSchema, + depositData: BaseDepositDataSchema, + swapToken: validAddress(), + exchange: validAddress(), + transferType: positiveIntStr(), + swapTokenAmount: positiveIntStr(), + minExpectedInputTokenAmount: positiveIntStr(), + routerCalldata: hexString(), +}); + +export const DepositWithPermitArgsSchema = type({ + signatureOwner: validAddress(), + depositData: type({ + submissionFees: SubmissionFeesSchema, + baseDepositData: BaseDepositDataSchema, + inputAmount: positiveIntStr(), + }), + deadline: positiveIntStr(), +}); + +export const SwapAndDepositWithPermitArgsSchema = type({ + signatureOwner: validAddress(), + swapAndDepositData: SwapAndDepositDataSchema, + deadline: positiveIntStr(), +}); + +export const allowedMethodNames = [ + "depositWithPermit", + "swapAndBridgeWithPermit", +]; + +export function validateMethodArgs(methodName: string, args: any) { + if (methodName === "depositWithPermit") { + assert(args, DepositWithPermitArgsSchema); + return { + args: args as Infer, + methodName, + } as const; + } else if (methodName === "swapAndBridgeWithPermit") { + assert(args, SwapAndDepositWithPermitArgsSchema); + return { + args: args as Infer, + methodName, + } as const; + } + throw new Error(`Invalid method name: ${methodName}`); +} + +export async function verifySignatures(params: { + methodNameAndArgs: ReturnType; + signatures: { + permit: string; + deposit: string; + }; + originChainId: number; + entryPointContractAddress: string; +}) { + const { + methodNameAndArgs, + signatures, + originChainId, + entryPointContractAddress, + } = params; + const { methodName, args } = methodNameAndArgs; + + let signatureOwner: string; + let getPermitTypedDataPromise: ReturnType; + let getDepositTypedDataPromise: ReturnType< + typeof getDepositTypedData | typeof getSwapAndDepositTypedData + >; + + if (methodName === "depositWithPermit") { + const { signatureOwner: _signatureOwner, deadline, depositData } = args; + signatureOwner = _signatureOwner; + getPermitTypedDataPromise = getPermitTypedData({ + tokenAddress: depositData.baseDepositData.inputToken, + chainId: originChainId, + ownerAddress: signatureOwner, + spenderAddress: entryPointContractAddress, + value: depositData.inputAmount, + deadline: Number(deadline), + }); + getDepositTypedDataPromise = getDepositTypedData({ + chainId: originChainId, + depositData, + }); + } else if (methodName === "swapAndBridgeWithPermit") { + const { + signatureOwner: _signatureOwner, + deadline, + swapAndDepositData, + } = args; + signatureOwner = _signatureOwner; + getPermitTypedDataPromise = getPermitTypedData({ + tokenAddress: swapAndDepositData.swapToken, + chainId: originChainId, + ownerAddress: signatureOwner, + spenderAddress: entryPointContractAddress, + value: swapAndDepositData.swapTokenAmount, + deadline: Number(deadline), + }); + getDepositTypedDataPromise = getSwapAndDepositTypedData({ + chainId: originChainId, + swapAndDepositData, + }); + } else { + throw new Error( + `Can not verify signatures for invalid method name: ${methodName}` + ); + } + + const [permitTypedData, depositTypedData] = await Promise.all([ + getPermitTypedDataPromise, + getDepositTypedDataPromise, + ]); + + const recoveredPermitSignerAddress = utils.verifyTypedData( + permitTypedData.eip712.domain, + permitTypedData.eip712.types, + permitTypedData.eip712.message, + signatures.permit + ); + if (recoveredPermitSignerAddress !== signatureOwner) { + throw new InvalidParamError({ + message: "Invalid permit signature", + param: "signatures.permit", + }); + } + + const recoveredDepositSignerAddress = utils.verifyTypedData( + depositTypedData.eip712.domain, + depositTypedData.eip712.types, + depositTypedData.eip712.message, + signatures.deposit + ); + if (recoveredDepositSignerAddress !== signatureOwner) { + throw new InvalidParamError({ + message: "Invalid deposit signature", + param: "signatures.deposit", + }); + } +} diff --git a/api/relay/index.ts b/api/relay/index.ts new file mode 100644 index 000000000..1716a2acc --- /dev/null +++ b/api/relay/index.ts @@ -0,0 +1,65 @@ +import { VercelRequest, VercelResponse } from "@vercel/node"; +import { object, number, assert, enums } from "superstruct"; + +import { handleErrorCondition } from "../_errors"; +import { getLogger, hexString, validAddress } from "../_utils"; +import { + allowedMethodNames, + validateMethodArgs, + verifySignatures, +} from "./_utils"; + +const BaseRelayRequestBodySchema = object({ + chainId: number(), + to: validAddress(), + methodName: enums(allowedMethodNames), + argsWithoutSignatures: object(), + signatures: object({ + permit: hexString(), + deposit: hexString(), + }), +}); + +export default async function handler( + request: VercelRequest, + response: VercelResponse +) { + const logger = getLogger(); + logger.debug({ + at: "Relay", + message: "Request body", + body: request.body, + }); + + try { + if (request.method !== "POST") { + return response.status(405).json({ error: "Method not allowed" }); + } + + assert(request.body, BaseRelayRequestBodySchema); + + // Validate method-specific request body + const methodNameAndArgs = validateMethodArgs( + request.body.methodName, + request.body.argsWithoutSignatures + ); + + // Verify signatures + const { signatures } = request.body; + await verifySignatures({ + methodNameAndArgs, + signatures, + originChainId: request.body.chainId, + entryPointContractAddress: request.body.to, + }); + + // TODO: Execute transaction based on configured strategies + + return response.status(200).json({ + success: true, + // Add relevant response data + }); + } catch (error) { + return handleErrorCondition("api/relay", response, logger, error); + } +} diff --git a/scripts/tests/swap-permit.ts b/scripts/tests/swap-permit.ts index 29c8296eb..ecc9a9ea3 100644 --- a/scripts/tests/swap-permit.ts +++ b/scripts/tests/swap-permit.ts @@ -1,8 +1,46 @@ -import { swap } from "./_swap-utils"; +import { Wallet } from "ethers"; + +import { getProvider } from "../../api/_utils"; +import { fetchSwapQuote, SWAP_API_BASE_URL } from "./_swap-utils"; +import axios from "axios"; async function swapWithPermit() { console.log("Swapping with permit..."); - await swap("permit"); + const swapQuote = await fetchSwapQuote("permit"); + + if (process.env.DEV_WALLET_PK) { + const wallet = new Wallet(process.env.DEV_WALLET_PK!).connect( + getProvider(swapQuote.swapTx.chainId) + ); + + console.log("EIP712 Permit:", swapQuote.eip712.permit); + console.log( + "EIP712 Deposit:", + swapQuote.eip712.deposit.message.submissionFees + ); + + // sign permit + deposit + const permitSig = await wallet._signTypedData( + swapQuote.eip712.permit.domain, + swapQuote.eip712.permit.types, + swapQuote.eip712.permit.message + ); + console.log("Signed permit:", permitSig); + + const depositSig = await wallet._signTypedData( + swapQuote.eip712.deposit.domain, + swapQuote.eip712.deposit.types, + swapQuote.eip712.deposit.message + ); + console.log("Signed deposit:", depositSig); + + // relay + const relayResponse = await axios.post(`${SWAP_API_BASE_URL}/api/relay`, { + ...swapQuote.swapTx, + signatures: { permit: permitSig, deposit: depositSig }, + }); + console.log("Relay response:", relayResponse.data); + } } swapWithPermit()