Skip to content

Commit

Permalink
feat: add base /relay handler
Browse files Browse the repository at this point in the history
  • Loading branch information
dohaki committed Dec 23, 2024
1 parent c79fc21 commit b50d215
Show file tree
Hide file tree
Showing 3 changed files with 280 additions and 2 deletions.
175 changes: 175 additions & 0 deletions api/relay/_utils.ts
Original file line number Diff line number Diff line change
@@ -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<typeof DepositWithPermitArgsSchema>,
methodName,
} as const;
} else if (methodName === "swapAndBridgeWithPermit") {
assert(args, SwapAndDepositWithPermitArgsSchema);
return {
args: args as Infer<typeof SwapAndDepositWithPermitArgsSchema>,
methodName,
} as const;
}
throw new Error(`Invalid method name: ${methodName}`);
}

export async function verifySignatures(params: {
methodNameAndArgs: ReturnType<typeof validateMethodArgs>;
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<typeof getPermitTypedData>;
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",
});
}
}
65 changes: 65 additions & 0 deletions api/relay/index.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
42 changes: 40 additions & 2 deletions scripts/tests/swap-permit.ts
Original file line number Diff line number Diff line change
@@ -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()
Expand Down

0 comments on commit b50d215

Please sign in to comment.