diff --git a/api/_eip712.ts b/api/_eip712.ts new file mode 100644 index 000000000..077148f68 --- /dev/null +++ b/api/_eip712.ts @@ -0,0 +1,23 @@ +import { utils } from "ethers"; + +export function hashDomainSeparator(params: { + name: string; + version: string | number; + chainId: number; + verifyingContract: string; +}): string { + return utils.keccak256( + utils.defaultAbiCoder.encode( + ["bytes32", "bytes32", "bytes32", "uint256", "address"], + [ + utils.id( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + utils.id(params.name), + utils.id(params.version.toString()), + params.chainId, + params.verifyingContract, + ] + ) + ); +} diff --git a/api/_permit.ts b/api/_permit.ts index 6d189d8a7..f484c7bb7 100644 --- a/api/_permit.ts +++ b/api/_permit.ts @@ -2,6 +2,7 @@ import { BigNumberish, ethers } from "ethers"; import { getProvider } from "./_utils"; import { ERC20_PERMIT_ABI } from "./_abis"; +import { hashDomainSeparator } from "./_eip712"; export class PermitNotSupportedError extends Error { constructor(tokenAddress: string, cause?: Error) { @@ -129,20 +130,12 @@ export async function getPermitArgsFromContract(params: { ? Number(versionFromContract) : params.eip712DomainVersion || 1; - const domainSeparatorHash = ethers.utils.keccak256( - ethers.utils.defaultAbiCoder.encode( - ["bytes32", "bytes32", "bytes32", "uint256", "address"], - [ - ethers.utils.id( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - ), - ethers.utils.id(name), - ethers.utils.id(eip712DomainVersion.toString()), - params.chainId, - params.tokenAddress, - ] - ) - ); + const domainSeparatorHash = hashDomainSeparator({ + name, + version: eip712DomainVersion, + chainId: params.chainId, + verifyingContract: params.tokenAddress, + }); if (domainSeparator !== domainSeparatorHash) { throw new PermitDomainSeparatorMismatchError(params.tokenAddress); diff --git a/api/_transfer-with-auth.ts b/api/_transfer-with-auth.ts index 650a51992..5e88a7b6e 100644 --- a/api/_transfer-with-auth.ts +++ b/api/_transfer-with-auth.ts @@ -1,7 +1,7 @@ import { BigNumberish, ethers } from "ethers"; import { getProvider } from "./_utils"; import { ERC_TRANSFER_WITH_AUTH_ABI } from "./_abis"; -import { utils } from "ethers"; +import { hashDomainSeparator } from "./_eip712"; export class TransferWithAuthNotSupportedError extends Error { constructor(tokenAddress: string, cause?: Error) { @@ -22,28 +22,6 @@ export class TransferWithAuthDomainSeparatorMismatchError extends Error { } } -export function hashDomainSeparator(params: { - name: string; - version: string | number; - chainId: number; - verifyingContract: string; -}): string { - return utils.keccak256( - utils.defaultAbiCoder.encode( - ["bytes32", "bytes32", "bytes32", "uint256", "address"], - [ - utils.id( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - ), - utils.id(params.name), - utils.id(params.version.toString()), - params.chainId, - params.verifyingContract, - ] - ) - ); -} - export async function getReceiveWithAuthTypedData(params: { tokenAddress: string; chainId: number; diff --git a/api/swap/_utils.ts b/api/swap/_utils.ts index 3b42f41f0..ec14681b4 100644 --- a/api/swap/_utils.ts +++ b/api/swap/_utils.ts @@ -22,6 +22,8 @@ import { } from "../_dexes/types"; import { AMOUNT_TYPE } from "../_dexes/utils"; import { encodeApproveCalldata } from "../_multicall-handler"; +import { AuthTxPayload } from "./auth/_utils"; +import { PermitTxPayload } from "./permit/_utils"; export const BaseSwapQueryParamsSchema = type({ amount: positiveIntStr(), @@ -110,6 +112,8 @@ export async function handleBaseSwapQueryParams( }), ]); + const refundToken = refundOnOrigin ? inputToken : outputToken; + return { inputToken, outputToken, @@ -124,6 +128,7 @@ export async function handleBaseSwapQueryParams( recipient, depositor, slippageTolerance, + refundToken, }; } @@ -262,3 +267,112 @@ export function stringifyBigNumProps(value: T): T { }) ) as T; } + +export function buildBaseSwapResponseJson(params: { + inputTokenAddress: string; + originChainId: number; + inputAmount: BigNumber; + allowance: BigNumber; + balance: BigNumber; + approvalTxns?: { + to: string; + data: string; + }[]; + originSwapQuote?: SwapQuote; + bridgeQuote: CrossSwapQuotes["bridgeQuote"]; + destinationSwapQuote?: SwapQuote; + refundToken: Token; + approvalSwapTx?: { + from: string; + to: string; + data: string; + value?: BigNumber; + gas?: BigNumber; + gasPrice: BigNumber; + }; + permitSwapTx?: AuthTxPayload | PermitTxPayload; +}) { + return stringifyBigNumProps({ + checks: { + allowance: params.approvalSwapTx + ? { + token: params.inputTokenAddress, + spender: params.approvalSwapTx.to, + actual: params.allowance, + expected: params.inputAmount, + } + : // TODO: Handle permit2 required allowance + { + token: params.inputTokenAddress, + spender: constants.AddressZero, + actual: 0, + expected: 0, + }, + balance: { + token: params.inputTokenAddress, + actual: params.balance, + expected: params.inputAmount, + }, + }, + approvalTxns: params.approvalTxns, + steps: { + originSwap: params.originSwapQuote + ? { + tokenIn: params.originSwapQuote.tokenIn, + tokenOut: params.originSwapQuote.tokenOut, + inputAmount: params.originSwapQuote.expectedAmountIn, + outputAmount: params.originSwapQuote.expectedAmountOut, + minOutputAmount: params.originSwapQuote.minAmountOut, + maxInputAmount: params.originSwapQuote.maximumAmountIn, + } + : undefined, + bridge: { + inputAmount: params.bridgeQuote.inputAmount, + outputAmount: params.bridgeQuote.outputAmount, + tokenIn: params.bridgeQuote.inputToken, + tokenOut: params.bridgeQuote.outputToken, + }, + destinationSwap: params.destinationSwapQuote + ? { + tokenIn: params.destinationSwapQuote.tokenIn, + tokenOut: params.destinationSwapQuote.tokenOut, + inputAmount: params.destinationSwapQuote.expectedAmountIn, + maxInputAmount: params.destinationSwapQuote.maximumAmountIn, + outputAmount: params.destinationSwapQuote.expectedAmountOut, + minOutputAmount: params.destinationSwapQuote.minAmountOut, + } + : undefined, + }, + refundToken: + params.refundToken.symbol === "ETH" + ? { + ...params.refundToken, + symbol: "WETH", + } + : params.refundToken, + inputAmount: + params.originSwapQuote?.expectedAmountIn ?? + params.bridgeQuote.inputAmount, + expectedOutputAmount: + params.destinationSwapQuote?.expectedAmountOut ?? + params.bridgeQuote.outputAmount, + minOutputAmount: + params.destinationSwapQuote?.minAmountOut ?? + params.bridgeQuote.outputAmount, + expectedFillTime: params.bridgeQuote.suggestedFees.estimatedFillTimeSec, + swapTx: params.approvalSwapTx + ? { + simulationSuccess: !!params.approvalSwapTx.gas, + chainId: params.originChainId, + to: params.approvalSwapTx.to, + data: params.approvalSwapTx.data, + value: params.approvalSwapTx.value, + gas: params.approvalSwapTx.gas, + gasPrice: params.approvalSwapTx.gasPrice, + } + : params.permitSwapTx + ? params.permitSwapTx.swapTx + : undefined, + eip712: params.permitSwapTx?.eip712, + }); +} diff --git a/api/swap/approval/_utils.ts b/api/swap/approval/_utils.ts index 25af6c5e7..859369d9f 100644 --- a/api/swap/approval/_utils.ts +++ b/api/swap/approval/_utils.ts @@ -144,7 +144,7 @@ export async function buildCrossSwapTxForAllowanceHolder( return { from: crossSwapQuotes.crossSwap.depositor, to: toAddress, - data: integratorId ? tagIntegratorId(integratorId, tx.data!) : tx.data, + data: integratorId ? tagIntegratorId(integratorId, tx.data!) : tx.data!, value: tx.value, }; } diff --git a/api/swap/approval/index.ts b/api/swap/approval/index.ts index 72f0232be..44f87c9d6 100644 --- a/api/swap/approval/index.ts +++ b/api/swap/approval/index.ts @@ -14,6 +14,7 @@ import { handleBaseSwapQueryParams, BaseSwapQueryParams, getApprovalTxns, + buildBaseSwapResponseJson, } from "../_utils"; import { getBalanceAndAllowance } from "../../_erc20"; import { getCrossSwapQuotes } from "../../_dexes/cross-swap-service"; @@ -56,6 +57,7 @@ const handler = async ( recipient, depositor, slippageTolerance, + refundToken, } = await handleBaseSwapQueryParams(request.query); const crossSwapQuotes = await getCrossSwapQuotes( @@ -134,81 +136,23 @@ const handler = async ( }); } - const refundToken = crossSwap.refundOnOrigin - ? bridgeQuote.inputToken - : bridgeQuote.outputToken; - - const responseJson = { - // fees: crossSwapQuotes.fees, - checks: { - allowance: { - token: inputTokenAddress, - spender: crossSwapTx.to, - actual: allowance.toString(), - expected: inputAmount.toString(), - }, - balance: { - token: inputTokenAddress, - actual: balance.toString(), - expected: inputAmount.toString(), - }, + const responseJson = buildBaseSwapResponseJson({ + originChainId, + inputTokenAddress, + inputAmount, + approvalSwapTx: { + ...crossSwapTx, + gas: originTxGas, + gasPrice: originTxGasPrice, }, + allowance, + balance, approvalTxns, - steps: { - originSwap: originSwapQuote - ? { - tokenIn: originSwapQuote.tokenIn, - tokenOut: originSwapQuote.tokenOut, - inputAmount: originSwapQuote.expectedAmountIn.toString(), - outputAmount: originSwapQuote.expectedAmountOut.toString(), - minOutputAmount: originSwapQuote.minAmountOut.toString(), - maxInputAmount: originSwapQuote.maximumAmountIn.toString(), - } - : undefined, - bridge: { - inputAmount: bridgeQuote.inputAmount.toString(), - outputAmount: bridgeQuote.outputAmount.toString(), - tokenIn: bridgeQuote.inputToken, - tokenOut: bridgeQuote.outputToken, - }, - destinationSwap: destinationSwapQuote - ? { - tokenIn: destinationSwapQuote.tokenIn, - tokenOut: destinationSwapQuote.tokenOut, - inputAmount: destinationSwapQuote.expectedAmountIn.toString(), - maxInputAmount: destinationSwapQuote.maximumAmountIn.toString(), - outputAmount: destinationSwapQuote.expectedAmountOut.toString(), - minOutputAmount: destinationSwapQuote.minAmountOut.toString(), - } - : undefined, - }, - swapTx: { - simulationSuccess: !!originTxGas, - chainId: originChainId, - to: crossSwapTx.to, - data: crossSwapTx.data, - value: crossSwapTx.value?.toString(), - gas: originTxGas?.toString(), - gasPrice: originTxGasPrice?.toString(), - }, - refundToken: - refundToken.symbol === "ETH" - ? { - ...refundToken, - symbol: "WETH", - } - : refundToken, - inputAmount: - originSwapQuote?.expectedAmountIn.toString() ?? - bridgeQuote.inputAmount.toString(), - expectedOutputAmount: - destinationSwapQuote?.expectedAmountOut.toString() ?? - bridgeQuote.outputAmount.toString(), - minOutputAmount: - destinationSwapQuote?.minAmountOut.toString() ?? - bridgeQuote.outputAmount.toString(), - expectedFillTime: bridgeQuote.suggestedFees.estimatedFillTimeSec, - }; + originSwapQuote, + bridgeQuote, + destinationSwapQuote, + refundToken, + }); mark.stop(); logger.debug({ at: "Swap/approval", diff --git a/api/swap/auth/_utils.ts b/api/swap/auth/_utils.ts index 323e7a43a..81828dd40 100644 --- a/api/swap/auth/_utils.ts +++ b/api/swap/auth/_utils.ts @@ -15,6 +15,8 @@ import { import { BigNumberish, BytesLike, utils } from "ethers"; import { SpokePoolV3PeripheryInterface } from "../../_typechain/SpokePoolV3Periphery"; +export type AuthTxPayload = Awaited>; + export async function buildAuthTxPayload({ crossSwapQuotes, authDeadline, @@ -154,10 +156,9 @@ export async function buildAuthTxPayload({ return { eip712: { - receiveWithAuthorization: authTypedData.eip712, + permit: authTypedData.eip712, deposit: depositTypedData.eip712, }, - swapTx: { chainId: originChainId, to: entryPointContract.address, diff --git a/api/swap/auth/index.ts b/api/swap/auth/index.ts index 21b919b66..8af00b28b 100644 --- a/api/swap/auth/index.ts +++ b/api/swap/auth/index.ts @@ -1,5 +1,6 @@ import { VercelResponse } from "@vercel/node"; import { assert, Infer, optional, type } from "superstruct"; +import { BigNumber } from "ethers"; import { TypedVercelRequest } from "../../_types"; import { getLogger, handleErrorCondition, positiveIntStr } from "../../_utils"; @@ -7,7 +8,7 @@ import { getCrossSwapQuotes } from "../../_dexes/cross-swap-service"; import { handleBaseSwapQueryParams, BaseSwapQueryParams, - stringifyBigNumProps, + buildBaseSwapResponseJson, } from "../_utils"; import { getSwapRouter02Strategy } from "../../_dexes/uniswap/swap-router-02"; import { InvalidParamError } from "../../_errors"; @@ -15,6 +16,7 @@ import { QuoteFetchStrategies } from "../../_dexes/utils"; import { buildAuthTxPayload } from "./_utils"; import { GAS_SPONSOR_ADDRESS } from "../../relay/_utils"; import * as sdk from "@across-protocol/sdk"; +import { getBalance } from "../../_erc20"; export const authSwapQueryParamsSchema = type({ authDeadline: optional(positiveIntStr()), @@ -76,6 +78,7 @@ const handler = async ( recipient, depositor, slippageTolerance, + refundToken, } = await handleBaseSwapQueryParams(restQuery); const crossSwapQuotes = await getCrossSwapQuotes( @@ -106,7 +109,25 @@ const handler = async ( }, }); - const responseJson = stringifyBigNumProps(crossSwapTxForAuth); + const balance = await getBalance({ + chainId: inputToken.chainId, + tokenAddress: inputToken.address, + owner: crossSwapQuotes.crossSwap.depositor, + }); + + const responseJson = buildBaseSwapResponseJson({ + inputTokenAddress: inputToken.address, + originChainId: inputToken.chainId, + permitSwapTx: crossSwapTxForAuth, + inputAmount: amount, + bridgeQuote: crossSwapQuotes.bridgeQuote, + originSwapQuote: crossSwapQuotes.originSwapQuote, + destinationSwapQuote: crossSwapQuotes.destinationSwapQuote, + refundToken, + balance, + // Allowance does not matter for auth-based flows + allowance: BigNumber.from(0), + }); logger.debug({ at: "Swap/auth", diff --git a/api/swap/index.ts b/api/swap/index.ts index d97798590..fe0edb0ff 100644 --- a/api/swap/index.ts +++ b/api/swap/index.ts @@ -1,4 +1,5 @@ import { VercelResponse } from "@vercel/node"; +import axios from "axios"; import { TypedVercelRequest } from "../_types"; import { @@ -9,13 +10,12 @@ import { import { handleBaseSwapQueryParams, BaseSwapQueryParams } from "./_utils"; import { getPermitArgsFromContract } from "../_permit"; import { getReceiveWithAuthArgsFromContract } from "../_transfer-with-auth"; -import axios from "axios"; type SwapFlowType = "permit" | "transfer-with-auth" | "approval"; function makeSwapHandler(path: string) { return (params: unknown) => - axios.get(`${resolveVercelEndpoint()}/${path}`, { params }); + axios.get(`${resolveVercelEndpoint(true)}/api/swap/${path}`, { params }); } const swapFlowTypeToHandler = { permit: makeSwapHandler("permit"), @@ -62,9 +62,9 @@ export default async function handler( } const handler = swapFlowTypeToHandler[swapFlowType]; - const responseJson = await handler(request.query); + const { data } = await handler(request.query); const enrichedResponseJson = { - ...responseJson, + ...data, swapFlowType, }; diff --git a/api/swap/permit/_utils.ts b/api/swap/permit/_utils.ts index b6136691b..f42fa81b1 100644 --- a/api/swap/permit/_utils.ts +++ b/api/swap/permit/_utils.ts @@ -16,6 +16,8 @@ import { } from "../../_dexes/utils"; import { SpokePoolV3PeripheryInterface } from "../../_typechain/SpokePoolV3Periphery"; +export type PermitTxPayload = Awaited>; + export async function buildPermitTxPayload({ crossSwapQuotes, permitDeadline, diff --git a/api/swap/permit/index.ts b/api/swap/permit/index.ts index 579c4106f..cdf04ece7 100644 --- a/api/swap/permit/index.ts +++ b/api/swap/permit/index.ts @@ -1,4 +1,5 @@ import { VercelResponse } from "@vercel/node"; +import { BigNumber } from "ethers"; import { assert, Infer, optional, type } from "superstruct"; import { TypedVercelRequest } from "../../_types"; @@ -7,13 +8,14 @@ import { getCrossSwapQuotes } from "../../_dexes/cross-swap-service"; import { handleBaseSwapQueryParams, BaseSwapQueryParams, - stringifyBigNumProps, + buildBaseSwapResponseJson, } from "../_utils"; import { getSwapRouter02Strategy } from "../../_dexes/uniswap/swap-router-02"; import { InvalidParamError } from "../../_errors"; import { buildPermitTxPayload } from "./_utils"; import { QuoteFetchStrategies } from "../../_dexes/utils"; import { GAS_SPONSOR_ADDRESS } from "../../relay/_utils"; +import { getBalance } from "../../_erc20"; export const PermitSwapQueryParamsSchema = type({ permitDeadline: optional(positiveIntStr()), @@ -71,6 +73,7 @@ const handler = async ( recipient, depositor, slippageTolerance, + refundToken, } = await handleBaseSwapQueryParams(restQuery); const crossSwapQuotes = await getCrossSwapQuotes( @@ -100,7 +103,25 @@ const handler = async ( }, }); - const responseJson = stringifyBigNumProps(crossSwapTxForPermit); + const balance = await getBalance({ + chainId: inputToken.chainId, + tokenAddress: inputToken.address, + owner: crossSwapQuotes.crossSwap.depositor, + }); + + const responseJson = buildBaseSwapResponseJson({ + inputTokenAddress: inputToken.address, + originChainId: inputToken.chainId, + permitSwapTx: crossSwapTxForPermit, + inputAmount: amount, + bridgeQuote: crossSwapQuotes.bridgeQuote, + originSwapQuote: crossSwapQuotes.originSwapQuote, + destinationSwapQuote: crossSwapQuotes.destinationSwapQuote, + refundToken, + balance, + // Allowance does not matter for permit-based flows + allowance: BigNumber.from(0), + }); logger.debug({ at: "Swap/permit", diff --git a/scripts/tests/_swap-utils.ts b/scripts/tests/_swap-utils.ts index 6b4f52e4f..d5fcdaea1 100644 --- a/scripts/tests/_swap-utils.ts +++ b/scripts/tests/_swap-utils.ts @@ -1,9 +1,16 @@ import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants"; -import { ethers } from "ethers"; +import { ethers, Wallet } from "ethers"; import dotenv from "dotenv"; import axios from "axios"; + +import { buildBaseSwapResponseJson } from "../../api/swap/_utils"; + dotenv.config(); +export type BaseSwapResponse = Awaited< + ReturnType +>; + export const { SWAP_API_BASE_URL = "http://localhost:3000" } = process.env; export const depositor = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D"; @@ -169,17 +176,105 @@ export function filterTestCases( return filteredTestCases; } -export async function fetchSwapQuote(slug: "approval" | "permit" | "auth") { +export async function fetchSwapQuote(slug?: "approval" | "permit" | "auth") { const filterString = process.argv[2]; const testCases = [...MIN_OUTPUT_CASES, ...EXACT_OUTPUT_CASES]; const filteredTestCases = filterTestCases(testCases, filterString); for (const testCase of filteredTestCases) { console.log("\nTest case:", testCase.labels.join(" ")); console.log("Params:", testCase.params); - const response = await axios.get(`${SWAP_API_BASE_URL}/api/swap/${slug}`, { - params: testCase.params, - }); + const response = await axios.get( + `${SWAP_API_BASE_URL}/api/swap${slug ? `/${slug}` : ""}`, + { + params: testCase.params, + } + ); console.log(JSON.stringify(response.data, null, 2)); - return response.data as T; + return response.data as BaseSwapResponse; + } +} + +export async function signAndWaitPermitFlow(params: { + wallet: Wallet; + swapResponse: BaseSwapResponse; +}) { + if (!params.swapResponse.eip712) { + throw new Error("No EIP712 data for permit"); + } + + // sign permit + deposit + const permitSig = await params.wallet._signTypedData( + params.swapResponse.eip712.permit.domain, + params.swapResponse.eip712.permit.types, + params.swapResponse.eip712.permit.message + ); + console.log("Signed permit:", permitSig); + + const depositSig = await params.wallet._signTypedData( + params.swapResponse.eip712.deposit.domain, + params.swapResponse.eip712.deposit.types, + params.swapResponse.eip712.deposit.message + ); + console.log("Signed deposit:", depositSig); + + // relay + const relayResponse = await axios.post(`${SWAP_API_BASE_URL}/api/relay`, { + ...params.swapResponse.swapTx, + signatures: { permit: permitSig, deposit: depositSig }, + }); + console.log("Relay response:", relayResponse.data); + + // track relay + while (true) { + const relayStatusResponse = await axios.get( + `${SWAP_API_BASE_URL}/api/relay/status?requestHash=${relayResponse.data.requestHash}` + ); + console.log("Relay status response:", relayStatusResponse.data); + + if (relayStatusResponse.data.status === "success") { + break; + } + + await new Promise((resolve) => setTimeout(resolve, 1_000)); + } +} + +export async function signAndWaitAllowanceFlow(params: { + wallet: Wallet; + swapResponse: BaseSwapResponse; +}) { + if (!params.swapResponse.swapTx || !("data" in params.swapResponse.swapTx)) { + throw new Error("No swap tx for allowance flow"); + } + + if (params.swapResponse.approvalTxns) { + console.log("Approval needed..."); + let step = 1; + for (const approvalTxn of params.swapResponse.approvalTxns) { + const stepLabel = `(${step}/${params.swapResponse.approvalTxns.length})`; + const tx = await params.wallet.sendTransaction({ + to: approvalTxn.to, + data: approvalTxn.data, + }); + console.log(`${stepLabel} Approval tx hash:`, tx.hash); + await tx.wait(); + console.log(`${stepLabel} Approval tx mined`); + step++; + } + } + + try { + const tx = await params.wallet.sendTransaction({ + to: params.swapResponse.swapTx.to, + data: params.swapResponse.swapTx.data, + value: params.swapResponse.swapTx.value, + gasLimit: params.swapResponse.swapTx.gas, + gasPrice: params.swapResponse.swapTx.gasPrice, + }); + console.log("Tx hash: ", tx.hash); + await tx.wait(); + console.log("Tx mined"); + } catch (e) { + console.error("Tx reverted", e); } } diff --git a/scripts/tests/swap-allowance.ts b/scripts/tests/swap-allowance.ts index 91bbdad27..cde9dc469 100644 --- a/scripts/tests/swap-allowance.ts +++ b/scripts/tests/swap-allowance.ts @@ -1,47 +1,23 @@ import { Wallet } from "ethers"; import { getProvider } from "../../api/_utils"; -import { fetchSwapQuote } from "./_swap-utils"; +import { fetchSwapQuote, signAndWaitAllowanceFlow } from "./_swap-utils"; async function swapWithAllowance() { console.log("Swapping with allowance..."); const swapQuote = await fetchSwapQuote("approval"); + if (!swapQuote || !swapQuote.swapTx || !("data" in swapQuote.swapTx)) { + console.log("No swap quote with tx data for approval"); + return; + } + if (process.env.DEV_WALLET_PK) { const wallet = new Wallet(process.env.DEV_WALLET_PK!).connect( - getProvider(swapQuote.params.originChainId) + getProvider(swapQuote.swapTx.chainId) ); - if (swapQuote.approvalTxns) { - console.log("Approval needed..."); - let step = 1; - for (const approvalTxn of swapQuote.approvalTxns) { - const stepLabel = `(${step}/${swapQuote.approvalTxns.length})`; - const tx = await wallet.sendTransaction({ - to: approvalTxn.to, - data: approvalTxn.data, - }); - console.log(`${stepLabel} Approval tx hash:`, tx.hash); - await tx.wait(); - console.log(`${stepLabel} Approval tx mined`); - step++; - } - } - - try { - const tx = await wallet.sendTransaction({ - to: swapQuote.swapTx.to, - data: swapQuote.swapTx.data, - value: swapQuote.swapTx.value, - gasLimit: swapQuote.swapTx.gas, - gasPrice: swapQuote.swapTx.gasPrice, - }); - console.log("Tx hash: ", tx.hash); - await tx.wait(); - console.log("Tx mined"); - } catch (e) { - console.error("Tx reverted", e); - } + await signAndWaitAllowanceFlow({ wallet, swapResponse: swapQuote }); } } diff --git a/scripts/tests/swap-auth.ts b/scripts/tests/swap-auth.ts index 31a6e2467..b03b066f0 100644 --- a/scripts/tests/swap-auth.ts +++ b/scripts/tests/swap-auth.ts @@ -1,18 +1,14 @@ import { Wallet } from "ethers"; import { getProvider } from "../../api/_utils"; -import { fetchSwapQuote, SWAP_API_BASE_URL } from "./_swap-utils"; -import { buildAuthTxPayload } from "../../api/swap/auth/_utils"; -import axios from "axios"; - -type AuthPayload = Awaited>; +import { fetchSwapQuote, signAndWaitPermitFlow } from "./_swap-utils"; async function swapWithAuth() { console.log("Swapping with auth..."); - const swapQuote = await fetchSwapQuote("auth"); + const swapQuote = await fetchSwapQuote("auth"); - if (!swapQuote) { - console.log("No Quote"); + if (!swapQuote || !swapQuote.swapTx || !swapQuote.eip712) { + console.log("No swap quote with EIP712 data for auth"); return; } @@ -21,41 +17,7 @@ async function swapWithAuth() { getProvider(swapQuote.swapTx.chainId) ); - // sign permit + deposit - const permitSig = await wallet._signTypedData( - swapQuote.eip712.receiveWithAuthorization.domain, - swapQuote.eip712.receiveWithAuthorization.types, - swapQuote.eip712.receiveWithAuthorization.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); - - // track relay - while (true) { - const relayStatusResponse = await axios.get( - `${SWAP_API_BASE_URL}/api/relay/status?requestHash=${relayResponse.data.requestHash}` - ); - console.log("Relay status response:", relayStatusResponse.data); - - if (relayStatusResponse.data.status === "success") { - break; - } - - await new Promise((resolve) => setTimeout(resolve, 1_000)); - } + await signAndWaitPermitFlow({ wallet, swapResponse: swapQuote }); } } diff --git a/scripts/tests/swap-permit.ts b/scripts/tests/swap-permit.ts index ce3c4e409..8a3c13ed0 100644 --- a/scripts/tests/swap-permit.ts +++ b/scripts/tests/swap-permit.ts @@ -1,18 +1,14 @@ import { Wallet } from "ethers"; import { getProvider } from "../../api/_utils"; -import { fetchSwapQuote, SWAP_API_BASE_URL } from "./_swap-utils"; -import { buildPermitTxPayload } from "../../api/swap/permit/_utils"; -import axios from "axios"; - -type PermitPayload = Awaited>; +import { fetchSwapQuote, signAndWaitPermitFlow } from "./_swap-utils"; async function swapWithPermit() { console.log("Swapping with permit..."); - const swapQuote = await fetchSwapQuote("permit"); + const swapQuote = await fetchSwapQuote("permit"); - if (!swapQuote) { - console.log("No Quote"); + if (!swapQuote || !swapQuote.swapTx || !swapQuote.eip712) { + console.log("No swap quote with EIP712 data for permit"); return; } @@ -21,41 +17,7 @@ async function swapWithPermit() { getProvider(swapQuote.swapTx.chainId) ); - // 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); - - // track relay - while (true) { - const relayStatusResponse = await axios.get( - `${SWAP_API_BASE_URL}/api/relay/status?requestHash=${relayResponse.data.requestHash}` - ); - console.log("Relay status response:", relayStatusResponse.data); - - if (relayStatusResponse.data.status === "success") { - break; - } - - await new Promise((resolve) => setTimeout(resolve, 1_000)); - } + await signAndWaitPermitFlow({ wallet, swapResponse: swapQuote }); } } diff --git a/scripts/tests/swap-unified.ts b/scripts/tests/swap-unified.ts new file mode 100644 index 000000000..fcd14fad2 --- /dev/null +++ b/scripts/tests/swap-unified.ts @@ -0,0 +1,45 @@ +import { Wallet } from "ethers"; + +import { getProvider } from "../../api/_utils"; +import { + fetchSwapQuote, + signAndWaitAllowanceFlow, + signAndWaitPermitFlow, +} from "./_swap-utils"; + +async function swapUnified() { + console.log("Swapping with unified endpoint..."); + const swapQuote = await fetchSwapQuote(); + + if (!swapQuote || !swapQuote.swapTx) { + console.log("No swap quote"); + return; + } + + if (process.env.DEV_WALLET_PK) { + const wallet = new Wallet(process.env.DEV_WALLET_PK!).connect( + getProvider(swapQuote.swapTx.chainId) + ); + + // if a permit-based flow is available, the unified endpoint will prefer that over an + // allowance-based flow and return the relevant EIP712 data. + if (swapQuote.eip712) { + // sign permit + relay + track + await signAndWaitPermitFlow({ wallet, swapResponse: swapQuote }); + } + // if no permit-based flow is available, we can use the allowance-based flow + else { + // sign and send approval txns + swap txn + await signAndWaitAllowanceFlow({ wallet, swapResponse: swapQuote }); + } + } +} + +swapUnified() + .then(() => console.log("Done")) + .catch((e) => { + console.error(e); + if (e.response?.data) { + console.log("Tx for debug sim:", e.response.data.transaction); + } + });