From edd35186ff20d986551b102b0939e1cfcb536df3 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Thu, 7 Nov 2024 22:22:03 +0700 Subject: [PATCH 01/47] feat: /swap handler skeleton --- api/swap.ts | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 api/swap.ts diff --git a/api/swap.ts b/api/swap.ts new file mode 100644 index 000000000..7112a138d --- /dev/null +++ b/api/swap.ts @@ -0,0 +1,77 @@ +import { VercelResponse } from "@vercel/node"; +import { assert, Infer, type, string, optional } from "superstruct"; + +import { TypedVercelRequest } from "./_types"; +import { + getLogger, + handleErrorCondition, + positiveFloatStr, + positiveIntStr, + validAddress, + boolStr, +} from "./_utils"; + +const SwapQueryParamsSchema = type({ + minOutputAmount: positiveIntStr(), + inputToken: validAddress(), + outputToken: validAddress(), + originChainId: positiveIntStr(), + destinationChainId: positiveIntStr(), + recipient: validAddress(), + integratorId: string(), + refundAddress: optional(validAddress()), + refundOnOrigin: optional(boolStr()), + slippageTolerance: optional(positiveFloatStr(50)), // max. 50% slippage +}); + +type SwapQueryParams = Infer; + +const handler = async ( + { query }: TypedVercelRequest, + response: VercelResponse +) => { + const logger = getLogger(); + logger.debug({ + at: "Swap", + message: "Query data", + query, + }); + try { + assert(query, SwapQueryParamsSchema); + + let { + inputToken, + outputToken, + minOutputAmount, + originChainId, + destinationChainId, + recipient, + integratorId, + refundAddress, + refundOnOrigin, + slippageTolerance = "0.5", // Default to 0.5% slippage + } = query; + + // 1. Get auxiliary data + // - Swap type: major to major (same), major to major (different), major to any, any to major, any to any + // - Token details + // - Token prices + + // 2. Get swap quotes and calldata based on the swap type + + // 3. Get suggested fees with message and input amount + + // 4. Build tx and return + + logger.debug({ + at: "Swap", + message: "Response data", + responseJson: {}, + }); + response.status(200).json({}); + } catch (error: unknown) { + return handleErrorCondition("swap", response, logger, error); + } +}; + +export default handler; From 9c39be4ede05cd323921c4fe776436711926817b Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Sun, 10 Nov 2024 22:14:10 +0700 Subject: [PATCH 02/47] feat: uniswap v3 exact output support + UniversalSwapAndBridge (#1268) * feat: add exact output trade type support uniswap * test with base * fixup * fixup * fixup * ci: fix dep review * fixup * test uniswap universal * fixup * test 1inch * fixup * fixup --- .github/workflows/dependency-review.yaml | 6 +- api/_dexes/1inch.ts | 25 +- api/_dexes/types.ts | 21 +- api/_dexes/uniswap.ts | 293 ++++++-------- api/_dexes/utils.ts | 26 +- api/swap-quote.ts | 20 +- package.json | 5 +- scripts/generate-routes.ts | 16 +- ...6fA914353c44b2E33eBE05f21846F1048bEda.json | 15 +- src/utils/bridge.ts | 16 +- src/utils/config.ts | 8 +- yarn.lock | 359 ++++++++++++++---- 12 files changed, 484 insertions(+), 326 deletions(-) diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml index f4f824e7c..ca75093f2 100644 --- a/.github/workflows/dependency-review.yaml +++ b/.github/workflows/dependency-review.yaml @@ -16,6 +16,8 @@ jobs: with: fail-on-severity: high # Comma-separated list of GHSA IDs to allow. - # We allow @openzeppelin/contracts@3.4.1-solc-0.7-2 critical vulnerabilities + # We allow the following critical vulnerabilities # because they are not exploitable in our usage of the package. - allow-ghsas: GHSA-fg47-3c2x-m2wr, GHSA-88g8-f5mf-f5rj, GHSA-3h5v-q93c-6h6q + # - @openzeppelin/contracts@3.4.1-solc-0.7-2 + # - @openzeppelin/contracts@4.7.0 + allow-ghsas: GHSA-fg47-3c2x-m2wr, GHSA-88g8-f5mf-f5rj, GHSA-3h5v-q93c-6h6q, GHSA-qh9x-gcfh-pcrw, GHSA-4g63-c64m-25w9, GHSA-xrc4-737v-9q75, GHSA-4h98-2769-gh6h, GHSA-4h98-2769-gh6h, GHSA-93hq-5wgc-jc82 diff --git a/api/_dexes/1inch.ts b/api/_dexes/1inch.ts index 98b47026e..b3febda27 100644 --- a/api/_dexes/1inch.ts +++ b/api/_dexes/1inch.ts @@ -1,27 +1,24 @@ import axios from "axios"; -import { AcrossSwap, SwapQuoteAndCalldata } from "./types"; +import { Swap, OriginSwapQuoteAndCalldata } from "./types"; import { getSwapAndBridgeAddress } from "./utils"; -export async function get1inchQuoteAndCalldata( - swap: AcrossSwap -): Promise { - const swapAndBridgeAddress = getSwapAndBridgeAddress( - "1inch", - swap.swapToken.chainId - ); - const apiBaseUrl = `https://api.1inch.dev/swap/v6.0/${swap.swapToken.chainId}`; +export async function get1inchQuoteForOriginSwapExactInput( + swap: Omit +): Promise { + const swapAndBridgeAddress = getSwapAndBridgeAddress("1inch", swap.chainId); + const apiBaseUrl = `https://api.1inch.dev/swap/v6.0/${swap.chainId}`; const apiHeaders = { Authorization: `Bearer ${process.env.ONEINCH_API_KEY}`, accept: "application/json", }; const swapParams = { - src: swap.swapToken.address, - dst: swap.acrossInputToken.address, - amount: swap.swapTokenAmount, + src: swap.tokenIn.address, + dst: swap.tokenOut.address, + amount: swap.amount, from: swapAndBridgeAddress, - slippage: swap.slippage, + slippage: swap.slippageTolerance, disableEstimate: true, allowPartialFill: false, receiver: swapAndBridgeAddress, @@ -47,6 +44,6 @@ export async function get1inchQuoteAndCalldata( value: response.data.tx.value, swapAndBridgeAddress, dex: "1inch", - slippage: swap.slippage, + slippage: swap.slippageTolerance, }; } diff --git a/api/_dexes/types.ts b/api/_dexes/types.ts index 84e17a905..51782b50d 100644 --- a/api/_dexes/types.ts +++ b/api/_dexes/types.ts @@ -5,22 +5,19 @@ export type Token = { chainId: number; }; -/** - * @property `swapToken` - Address of the token that will be swapped for acrossInputToken. - * @property `acrossInputToken` - Address of the token that will be bridged via Across as the inputToken. - * @property `swapTokenAmount` - The amount of swapToken to be swapped for acrossInputToken. - * @property `slippage` - The slippage tolerance for the swap in decimals, e.g. 1 for 1%. - */ -export type AcrossSwap = { - swapToken: Token; - acrossInputToken: Token; - swapTokenAmount: string; - slippage: number; +export type Swap = { + chainId: number; + tokenIn: Token; + tokenOut: Token; + amount: string; + recipient: string; + slippageTolerance: number; + type: "EXACT_INPUT" | "MIN_OUTPUT"; }; export type SupportedDex = "1inch" | "uniswap"; -export type SwapQuoteAndCalldata = { +export type OriginSwapQuoteAndCalldata = { minExpectedInputTokenAmount: string; routerCalldata: string; value: string; diff --git a/api/_dexes/uniswap.ts b/api/_dexes/uniswap.ts index d693dc5a4..0e0100c69 100644 --- a/api/_dexes/uniswap.ts +++ b/api/_dexes/uniswap.ts @@ -1,49 +1,33 @@ import { ethers } from "ethers"; +import { CurrencyAmount, Percent, Token, TradeType } from "@uniswap/sdk-core"; import { - FeeAmount, - Pool, - Route, - SwapOptions, - SwapQuoter, - SwapRouter, - Trade, - computePoolAddress, -} from "@uniswap/v3-sdk"; -import { - Currency, - CurrencyAmount, - Percent, - Token, - TradeType, -} from "@uniswap/sdk-core"; -import JSBI from "jsbi"; -import IUniswapV3PoolABI from "@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json"; + AlphaRouter, + SwapOptionsSwapRouter02, + SwapType, +} from "@uniswap/smart-order-router"; import { CHAIN_IDs } from "@across-protocol/constants"; import { utils } from "@across-protocol/sdk"; -import { callViaMulticall3, getProvider } from "../_utils"; +import { getProvider } from "../_utils"; import { TOKEN_SYMBOLS_MAP } from "../_constants"; import { - AcrossSwap, - SwapQuoteAndCalldata, + OriginSwapQuoteAndCalldata, Token as AcrossToken, + Swap, } from "./types"; -import { getSwapAndBridgeAddress } from "./utils"; - -// https://docs.uniswap.org/contracts/v3/reference/deployments/ -const POOL_FACTORY_CONTRACT_ADDRESS = { - [CHAIN_IDs.MAINNET]: "0x1F98431c8aD98523631AE4a59f267346ea31F984", - [CHAIN_IDs.OPTIMISM]: "0x1F98431c8aD98523631AE4a59f267346ea31F984", - [CHAIN_IDs.ARBITRUM]: "0x1F98431c8aD98523631AE4a59f267346ea31F984", - [CHAIN_IDs.BASE]: "0x33128a8fC17869897dcE68Ed026d694621f6FDfD", - [CHAIN_IDs.POLYGON]: "0x1F98431c8aD98523631AE4a59f267346ea31F984", -}; -const QUOTER_CONTRACT_ADDRESS = { - [CHAIN_IDs.MAINNET]: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e", - [CHAIN_IDs.OPTIMISM]: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e", - [CHAIN_IDs.ARBITRUM]: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e", - [CHAIN_IDs.BASE]: "0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a", - [CHAIN_IDs.POLYGON]: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e", +import { getSwapAndBridgeAddress, NoSwapRouteError } from "./utils"; + +// Taken from here: https://docs.uniswap.org/contracts/v3/reference/deployments/ +export const SWAP_ROUTER_02_ADDRESS = { + [CHAIN_IDs.ARBITRUM]: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", + [CHAIN_IDs.BASE]: "0x2626664c2603336E57B271c5C0b26F421741e481", + [CHAIN_IDs.BLAST]: "0x549FEB8c9bd4c12Ad2AB27022dA12492aC452B66", + [CHAIN_IDs.MAINNET]: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", + [CHAIN_IDs.OPTIMISM]: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", + [CHAIN_IDs.POLYGON]: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", + [CHAIN_IDs.WORLD_CHAIN]: "0x091AD9e2e6e5eD44c1c66dB50e49A601F9f36cF6", + [CHAIN_IDs.ZK_SYNC]: "0x99c56385daBCE3E81d8499d0b8d0257aBC07E8A3", + [CHAIN_IDs.ZORA]: "0x7De04c96BE5159c3b5CeffC82aa176dc81281557", }; // Maps testnet chain IDs to their main counterparts. Used to get the mainnet token @@ -55,186 +39,125 @@ const TESTNET_TO_MAINNET = { [CHAIN_IDs.ARBITRUM_SEPOLIA]: CHAIN_IDs.ARBITRUM, }; -export async function getUniswapQuoteAndCalldata( - swap: AcrossSwap -): Promise { - const swapAndBridgeAddress = getSwapAndBridgeAddress( - "uniswap", - swap.swapToken.chainId - ); +export async function getUniswapQuoteForOriginSwapExactInput( + swap: Omit +): Promise { + const swapAndBridgeAddress = getSwapAndBridgeAddress("uniswap", swap.chainId); - const initialSwapToken = { ...swap.swapToken }; - const initialAcrossInputToken = { ...swap.acrossInputToken }; + const initialTokenIn = { ...swap.tokenIn }; + const initialTokenOut = { ...swap.tokenOut }; // Always use mainnet tokens for retrieving quote, so that we can get equivalent quotes // for testnet tokens. - swap.swapToken = getMainnetToken(swap.swapToken); - swap.acrossInputToken = getMainnetToken(swap.acrossInputToken); - - const poolInfo = await getPoolInfo(swap); - const tokenA = new Token( - swap.swapToken.chainId, - swap.swapToken.address, - swap.swapToken.decimals - ); - const tokenB = new Token( - swap.acrossInputToken.chainId, - swap.acrossInputToken.address, - swap.acrossInputToken.decimals - ); - const pool = new Pool( - tokenA, - tokenB, - FeeAmount.LOW, - poolInfo.sqrtPriceX96.toString(), - poolInfo.liquidity.toString(), - poolInfo.tick - ); - - const swapRoute = new Route([pool], tokenA, tokenB); - - const amountOut = await getOutputQuote(swap, swapRoute); + swap.tokenIn = getMainnetToken(swap.tokenIn); + swap.tokenOut = getMainnetToken(swap.tokenOut); - const uncheckedTrade = Trade.createUncheckedTrade({ - route: swapRoute, - inputAmount: CurrencyAmount.fromRawAmount(tokenA, swap.swapTokenAmount), - outputAmount: CurrencyAmount.fromRawAmount(tokenB, JSBI.BigInt(amountOut)), - tradeType: TradeType.EXACT_INPUT, - }); - - const options: SwapOptions = { - slippageTolerance: new Percent( - // max. slippage decimals is 2 - Number(swap.slippage.toFixed(2)) * 100, - 10_000 - ), - // 20 minutes from the current Unix time - deadline: utils.getCurrentTime() + 60 * 20, + const { route, options } = await getUniswapQuote({ + ...swap, recipient: swapAndBridgeAddress, - }; + }); - const methodParameters = SwapRouter.swapCallParameters( - [uncheckedTrade], - options - ); + if (!route.methodParameters) { + throw new NoSwapRouteError({ + dex: "uniswap", + tokenInSymbol: swap.tokenIn.symbol, + tokenOutSymbol: swap.tokenOut.symbol, + chainId: swap.chainId, + swapType: "EXACT_INPUT", + }); + } // replace mainnet token addresses with initial token addresses in calldata - methodParameters.calldata = methodParameters.calldata.replace( - swap.swapToken.address.toLowerCase().slice(2), - initialSwapToken.address.toLowerCase().slice(2) + route.methodParameters.calldata = route.methodParameters.calldata.replace( + swap.tokenIn.address.toLowerCase().slice(2), + initialTokenIn.address.toLowerCase().slice(2) ); - methodParameters.calldata = methodParameters.calldata.replace( - swap.acrossInputToken.address.toLowerCase().slice(2), - initialAcrossInputToken.address.toLowerCase().slice(2) + route.methodParameters.calldata = route.methodParameters.calldata.replace( + swap.tokenOut.address.toLowerCase().slice(2), + initialTokenOut.address.toLowerCase().slice(2) ); return { minExpectedInputTokenAmount: ethers.utils .parseUnits( - uncheckedTrade.minimumAmountOut(options.slippageTolerance).toExact(), - swap.acrossInputToken.decimals + route.trade.minimumAmountOut(options.slippageTolerance).toExact(), + swap.tokenIn.decimals ) .toString(), - routerCalldata: methodParameters.calldata, - value: ethers.BigNumber.from(methodParameters.value).toString(), + routerCalldata: route.methodParameters.calldata, + value: ethers.BigNumber.from(route.methodParameters.value).toString(), swapAndBridgeAddress, dex: "uniswap", - slippage: swap.slippage, + slippage: swap.slippageTolerance, }; } -async function getOutputQuote( - swap: AcrossSwap, - route: Route -) { - const provider = getProvider(route.chainId); +export async function getUniswapQuote(swap: Swap) { + const { router, options } = getSwapRouterAndOptions(swap); + + const amountCurrency = + swap.type === "EXACT_INPUT" ? swap.tokenIn : swap.tokenOut; + const quoteCurrency = + swap.type === "EXACT_INPUT" ? swap.tokenOut : swap.tokenIn; + + console.log("amountCurrency", amountCurrency); + console.log("quoteCurrency", quoteCurrency); - const { calldata } = SwapQuoter.quoteCallParameters( - route, + const route = await router.route( CurrencyAmount.fromRawAmount( new Token( - swap.swapToken.chainId, - swap.swapToken.address, - swap.swapToken.decimals + amountCurrency.chainId, + amountCurrency.address, + amountCurrency.decimals ), - swap.swapTokenAmount + swap.amount ), - TradeType.EXACT_INPUT, - { useQuoterV2: true } + new Token( + quoteCurrency.chainId, + quoteCurrency.address, + quoteCurrency.decimals + ), + swap.type === "EXACT_INPUT" + ? TradeType.EXACT_INPUT + : TradeType.EXACT_OUTPUT, + options ); - const quoteCallReturnData = await provider.call({ - to: QUOTER_CONTRACT_ADDRESS[swap.swapToken.chainId], - data: calldata, - }); + if (!route || !route.methodParameters) { + throw new NoSwapRouteError({ + dex: "uniswap", + tokenInSymbol: swap.tokenIn.symbol, + tokenOutSymbol: swap.tokenOut.symbol, + chainId: swap.chainId, + swapType: "EXACT_INPUT", + }); + } - return ethers.utils.defaultAbiCoder.decode(["uint256"], quoteCallReturnData); + return { route, options }; } -async function getPoolInfo({ - swapToken, - acrossInputToken, -}: AcrossSwap): Promise<{ - token0: string; - token1: string; - fee: number; - tickSpacing: number; - sqrtPriceX96: ethers.BigNumber; - liquidity: ethers.BigNumber; - tick: number; -}> { - const provider = getProvider(swapToken.chainId); - const poolContract = new ethers.Contract( - computePoolAddress({ - factoryAddress: POOL_FACTORY_CONTRACT_ADDRESS[swapToken.chainId], - tokenA: new Token( - swapToken.chainId, - swapToken.address, - swapToken.decimals - ), - tokenB: new Token( - acrossInputToken.chainId, - acrossInputToken.address, - acrossInputToken.decimals - ), - fee: FeeAmount.LOW, - }), - IUniswapV3PoolABI.abi, - provider - ); - - const poolMethods = [ - "token0", - "token1", - "fee", - "tickSpacing", - "liquidity", - "slot0", - ]; - const [token0, token1, fee, tickSpacing, liquidity, slot0] = - await callViaMulticall3( - provider, - poolMethods.map((method) => ({ - contract: poolContract, - functionName: method, - })) - ); - +function getSwapRouterAndOptions(params: { + chainId: number; + recipient: string; + slippageTolerance: number; +}) { + const provider = getProvider(params.chainId); + const router = new AlphaRouter({ + chainId: params.chainId, + provider, + }); + const options: SwapOptionsSwapRouter02 = { + recipient: params.recipient, + deadline: utils.getCurrentTime() + 30 * 60, // 30 minutes from now + type: SwapType.SWAP_ROUTER_02, + slippageTolerance: new Percent( + // max. slippage decimals is 2 + Number(params.slippageTolerance.toFixed(2)) * 100, + 10_000 + ), + }; return { - token0, - token1, - fee, - tickSpacing, - liquidity, - sqrtPriceX96: slot0[0], - tick: slot0[1], - } as unknown as { - token0: string; - token1: string; - fee: number; - tickSpacing: number; - sqrtPriceX96: ethers.BigNumber; - liquidity: ethers.BigNumber; - tick: number; + router, + options, }; } diff --git a/api/_dexes/utils.ts b/api/_dexes/utils.ts index 9edde40b9..2d04103a5 100644 --- a/api/_dexes/utils.ts +++ b/api/_dexes/utils.ts @@ -1,4 +1,4 @@ -import { ENABLED_ROUTES } from "../_utils"; +import { ENABLED_ROUTES, getTokenByAddress } from "../_utils"; export class UnsupportedDex extends Error { constructor(dex: string) { @@ -12,6 +12,20 @@ export class UnsupportedDexOnChain extends Error { } } +export class NoSwapRouteError extends Error { + constructor(args: { + dex: string; + tokenInSymbol: string; + tokenOutSymbol: string; + chainId: number; + swapType: string; + }) { + super( + `No ${args.dex} swap route found for '${args.swapType}' ${args.tokenInSymbol} to ${args.tokenOutSymbol} on chain ${args.chainId}` + ); + } +} + export const swapAndBridgeDexes = Object.keys( ENABLED_ROUTES.swapAndBridgeAddresses ); @@ -30,6 +44,16 @@ export function getSwapAndBridgeAddress(dex: string, chainId: number) { return address; } +export function getAcrossSwapType(params: { + inputToken: string; + originChainId: number; + outputToken: string; + destinationChainId: number; +}) { + // TODO: Implement this function + return "majorToMajor"; +} + function _isDexSupported( dex: string ): dex is keyof typeof ENABLED_ROUTES.swapAndBridgeAddresses { diff --git a/api/swap-quote.ts b/api/swap-quote.ts index d8d626c0d..5b9e04a71 100644 --- a/api/swap-quote.ts +++ b/api/swap-quote.ts @@ -13,8 +13,8 @@ import { validateChainAndTokenParams, isSwapRouteEnabled, } from "./_utils"; -import { getUniswapQuoteAndCalldata } from "./_dexes/uniswap"; -import { get1inchQuoteAndCalldata } from "./_dexes/1inch"; +import { getUniswapQuoteForOriginSwapExactInput } from "./_dexes/uniswap"; +import { get1inchQuoteForOriginSwapExactInput } from "./_dexes/1inch"; import { InvalidParamError } from "./_errors"; const SwapQuoteQueryParamsSchema = type({ @@ -97,18 +97,20 @@ const handler = async ( } const swap = { - swapToken, - acrossInputToken: { + chainId: originChainId, + tokenIn: swapToken, + tokenOut: { ...acrossInputToken, chainId: originChainId, }, - swapTokenAmount, - slippage: parseFloat(swapSlippage), - }; + amount: swapTokenAmount, + slippageTolerance: parseFloat(swapSlippage), + type: "EXACT_INPUT", + } as const; const quoteResults = await Promise.allSettled([ - getUniswapQuoteAndCalldata(swap), - get1inchQuoteAndCalldata(swap), + getUniswapQuoteForOriginSwapExactInput(swap), + get1inchQuoteForOriginSwapExactInput(swap), ]); const settledResults = quoteResults.flatMap((result) => diff --git a/package.json b/package.json index 0a314ad7d..783b96e90 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,9 @@ "@sentry/react": "^7.37.2", "@tanstack/react-query": "v4", "@tanstack/react-query-devtools": "v4", - "@uniswap/sdk-core": "^4.2.0", - "@uniswap/v3-sdk": "^3.11.0", + "@uniswap/sdk-core": "^5.9.0", + "@uniswap/smart-order-router": "^4.7.8", + "@uniswap/v3-sdk": "^3.18.1", "@vercel/kv": "^2.0.0", "@web3-onboard/coinbase": "^2.4.1", "@web3-onboard/core": "^2.21.2", diff --git a/scripts/generate-routes.ts b/scripts/generate-routes.ts index b6d433132..af2bf0da3 100644 --- a/scripts/generate-routes.ts +++ b/scripts/generate-routes.ts @@ -91,16 +91,16 @@ const enabledRoutes = { }, swapAndBridgeAddresses: { "1inch": { - [CHAIN_IDs.POLYGON]: "0xaBa0F11D55C5dDC52cD0Cb2cd052B621d45159d5", - [CHAIN_IDs.OPTIMISM]: "0x3E7448657409278C9d6E192b92F2b69B234FCc42", - [CHAIN_IDs.ARBITRUM]: "0xC456398D5eE3B93828252e48beDEDbc39e03368E", - [CHAIN_IDs.BASE]: "0x7CFaBF2eA327009B39f40078011B0Fb714b65926", + [CHAIN_IDs.POLYGON]: "0xF9735e425A36d22636EF4cb75c7a6c63378290CA", + [CHAIN_IDs.OPTIMISM]: "0x7631eA29479Ee265241F13FB48555A2C886d3Bf8", + [CHAIN_IDs.ARBITRUM]: "0x81C7601ac0c5825e89F967f9222B977CCD78aD77", + [CHAIN_IDs.BASE]: "0x98285D11B9F7aFec2d475805E5255f26B4490167", }, uniswap: { - [CHAIN_IDs.POLYGON]: "0x9220Fa27ae680E4e8D9733932128FA73362E0393", - [CHAIN_IDs.OPTIMISM]: "0x6f4A733c7889f038D77D4f540182Dda17423CcbF", - [CHAIN_IDs.ARBITRUM]: "0xF633b72A4C2Fb73b77A379bf72864A825aD35b6D", - // [CHAIN_IDs.BASE]: "0xbcfbCE9D92A516e3e7b0762AE218B4194adE34b4", + [CHAIN_IDs.POLYGON]: "0xa55490E20057BD4775618D0FC8D51F59f602FED0", + [CHAIN_IDs.OPTIMISM]: "0x04989eaF03547E6583f9d9e42aeD11D2b78A808b", + [CHAIN_IDs.ARBITRUM]: "0x2414A759d4EFF700Ad81e257Ab5187d07eCeEbAb", + [CHAIN_IDs.BASE]: "0xed8b9c9aE7aCEf12eb4650d26Eb876005a4752d2", }, }, routes: transformChainConfigs(enabledMainnetChainConfigs), diff --git a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json index d758c3b02..fe47510b2 100644 --- a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json +++ b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json @@ -9,15 +9,16 @@ "claimAndStakeAddress": "0x985e8A89Dd6Af8896Ef075c8dd93512433dc5829", "swapAndBridgeAddresses": { "1inch": { - "10": "0x3E7448657409278C9d6E192b92F2b69B234FCc42", - "137": "0xaBa0F11D55C5dDC52cD0Cb2cd052B621d45159d5", - "8453": "0x7CFaBF2eA327009B39f40078011B0Fb714b65926", - "42161": "0xC456398D5eE3B93828252e48beDEDbc39e03368E" + "10": "0x7631eA29479Ee265241F13FB48555A2C886d3Bf8", + "137": "0xF9735e425A36d22636EF4cb75c7a6c63378290CA", + "8453": "0x98285D11B9F7aFec2d475805E5255f26B4490167", + "42161": "0x81C7601ac0c5825e89F967f9222B977CCD78aD77" }, "uniswap": { - "10": "0x6f4A733c7889f038D77D4f540182Dda17423CcbF", - "137": "0x9220Fa27ae680E4e8D9733932128FA73362E0393", - "42161": "0xF633b72A4C2Fb73b77A379bf72864A825aD35b6D" + "10": "0x04989eaF03547E6583f9d9e42aeD11D2b78A808b", + "137": "0xa55490E20057BD4775618D0FC8D51F59f602FED0", + "8453": "0xed8b9c9aE7aCEf12eb4650d26Eb876005a4752d2", + "42161": "0x2414A759d4EFF700Ad81e257Ab5187d07eCeEbAb" } }, "routes": [ diff --git a/src/utils/bridge.ts b/src/utils/bridge.ts index 19ed75341..be207763c 100644 --- a/src/utils/bridge.ts +++ b/src/utils/bridge.ts @@ -331,20 +331,6 @@ export async function sendSwapAndBridgeTx( ); } - const [_swapTokenAddress, _acrossInputTokenAddress] = await Promise.all([ - swapAndBridge.SWAP_TOKEN(), - swapAndBridge.ACROSS_INPUT_TOKEN(), - ]); - - if ( - swapTokenAddress.toLowerCase() !== _swapTokenAddress.toLowerCase() || - inputTokenAddress.toLowerCase() !== _acrossInputTokenAddress.toLowerCase() - ) { - throw new Error( - `Mismatch between the SwapAndBridge contract's swap token and input token addresses` - ); - } - const inputAmount = BigNumber.from(swapQuote.minExpectedInputTokenAmount); const outputAmount = inputAmount.sub( inputAmount.mul(relayerFeePct).div(fixedPointAdjustment) @@ -352,6 +338,8 @@ export async function sendSwapAndBridgeTx( fillDeadline ??= await getFillDeadline(spokePool); const tx = await swapAndBridge.populateTransaction.swapAndBridge( + swapTokenAddress, + inputTokenAddress, swapQuote.routerCalldata, swapTokenAmount, swapQuote.minExpectedInputTokenAmount, diff --git a/src/utils/config.ts b/src/utils/config.ts index ba1b0449c..1952f1c0a 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -18,8 +18,8 @@ import { AcceleratingDistributor__factory, ClaimAndStake, ClaimAndStake__factory, - SwapAndBridge, - SwapAndBridge__factory, + UniversalSwapAndBridge, + UniversalSwapAndBridge__factory, } from "utils/typechain"; import { SupportedDex } from "./serverless-api/prod/swap-quote"; @@ -158,7 +158,7 @@ export class ConfigClient { chainId: constants.ChainId, dexKey: SupportedDex, signer?: Signer - ): SwapAndBridge | undefined { + ): UniversalSwapAndBridge | undefined { const address = this.getSwapAndBridgeAddress(chainId, dexKey); if (!address) { @@ -166,7 +166,7 @@ export class ConfigClient { } const provider = signer ?? providerUtils.getProvider(chainId); - return SwapAndBridge__factory.connect(address, provider); + return UniversalSwapAndBridge__factory.connect(address, provider); } getHubPoolChainId(): constants.ChainId { return this.config.hubPoolChain; diff --git a/yarn.lock b/yarn.lock index 02b5fac47..95f62d966 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,16 +16,11 @@ "@uma/common" "^2.17.0" hardhat "^2.9.3" -"@across-protocol/constants@^3.1.16": +"@across-protocol/constants@^3.1.16", "@across-protocol/constants@^3.1.9": version "3.1.16" resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.16.tgz#c126085d29d4d051fd02a04c833d804d37c3c219" integrity sha512-+U+AecGWnfY4b4sSfKBvsDj/+yXKEqpTXcZgI8GVVmUTkUhs1efA0kN4q3q10yy5TXI5TtagaG7R9yZg1zgKKg== -"@across-protocol/constants@^3.1.9": - version "3.1.13" - resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.13.tgz#b4caf494e9d9fe50290cca91b7883ea408fdb90a" - integrity sha512-EsTJgQL5p+XXs40aBxOSbMOpQr/f4ty+Iyget8Oh6MT/cncCa2+W8a78fbqYqTtSpH6Sm7E8nvT8gPuSS6ef1w== - "@across-protocol/contracts-v3.0.6@npm:@across-protocol/contracts@3.0.6": version "3.0.6" resolved "https://registry.yarnpkg.com/@across-protocol/contracts/-/contracts-3.0.6.tgz#7508fc52db50cfa670edc7c4e9e615ad25cdd56c" @@ -2299,7 +2294,7 @@ bufio "^1.0.7" chai "^4.3.4" -"@eth-optimism/core-utils@0.13.2": +"@eth-optimism/core-utils@0.13.2", "@eth-optimism/core-utils@^0.13.2": version "0.13.2" resolved "https://registry.yarnpkg.com/@eth-optimism/core-utils/-/core-utils-0.13.2.tgz#c0187c3abf6d86dad039edf04ff81299253214fe" integrity sha512-u7TOKm1RxH1V5zw7dHmfy91bOuEAZU68LT/9vJPkuWEjaTl+BgvPDRDTurjzclHzN0GbWdcpOqPZg4ftjkJGaw== @@ -2332,13 +2327,13 @@ ethers "^5.5.4" lodash "^4.17.21" -"@eth-optimism/sdk@^3.3.1": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@eth-optimism/sdk/-/sdk-3.3.1.tgz#f72b6f93b58e2a2943f10aca3be91dfc23d9839f" - integrity sha512-zf8qL+KwYWUUwvdcjF1HpBxgKSt5wsKr8oa6jwqUhdPkQHUtVK5SRKtqXqYplnAgKtxDQYwlK512GU16odEl1w== +"@eth-optimism/sdk@^3.2.2", "@eth-optimism/sdk@^3.3.1": + version "3.3.2" + resolved "https://registry.yarnpkg.com/@eth-optimism/sdk/-/sdk-3.3.2.tgz#68a5f6e77f9c85f6eeb9db8044b3b69199520eb0" + integrity sha512-+zhxT0YkBIEzHsuIayQGjr8g9NawZo6/HYfzg1NSEFsE2Yt0NyCWqVDFTuuak0T6AvIa2kNcl3r0Z8drdb2QmQ== dependencies: "@eth-optimism/contracts" "0.6.0" - "@eth-optimism/core-utils" "0.13.2" + "@eth-optimism/core-utils" "^0.13.2" lodash "^4.17.21" merkletreejs "^0.3.11" rlp "^2.2.7" @@ -2405,7 +2400,7 @@ "@ethersproject/properties" "^5.5.0" "@ethersproject/strings" "^5.5.0" -"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.0.12", "@ethersproject/abi@^5.1.2", "@ethersproject/abi@^5.4.0", "@ethersproject/abi@^5.5.0", "@ethersproject/abi@^5.6.3", "@ethersproject/abi@^5.7.0": +"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.1.2", "@ethersproject/abi@^5.4.0", "@ethersproject/abi@^5.5.0", "@ethersproject/abi@^5.6.3", "@ethersproject/abi@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449" integrity sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA== @@ -4217,11 +4212,21 @@ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.1.0.tgz#baec89a7f5f73e3d8ea582a78f1980134b605375" integrity sha512-TihZitscnaHNcZgXGj9zDLDyCqjziytB4tMCwXq0XimfWkAjBYyk5/pOsDbbwcavhlc79HhpTEpQcrMnPVa1mw== +"@openzeppelin/contracts@4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.7.0.tgz#3092d70ea60e3d1835466266b1d68ad47035a2d5" + integrity sha512-52Qb+A1DdOss8QvJrijYYPSf32GUg2pGaG/yCxtaA3cu4jduouTdg4XZSMLW9op54m1jH7J8hoajhHKOPsoJFw== + "@openzeppelin/contracts@4.9.6", "@openzeppelin/contracts@^4.2.0", "@openzeppelin/contracts@^4.7.3", "@openzeppelin/contracts@^4.8.1": version "4.9.6" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.6.tgz#2a880a24eb19b4f8b25adc2a5095f2aa27f39677" integrity sha512-xSmezSupL+y9VkHZJGDoCBpmnB2ogM13ccaYDWqJTfS3dbuHkgjuwDFUmaFauBCboQMGB/S5UqUl2y54X99BmA== +"@openzeppelin/contracts@5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.0.2.tgz#b1d03075e49290d06570b2fd42154d76c2a5d210" + integrity sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA== + "@phenomnomnominal/tsquery@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@phenomnomnominal/tsquery/-/tsquery-3.0.0.tgz#6f2f4dbf6304ff52b12cc7a5b979f20c3794a22a" @@ -6549,6 +6554,13 @@ "@types/connect" "*" "@types/node" "*" +"@types/brotli@^1.3.4": + version "1.3.4" + resolved "https://registry.yarnpkg.com/@types/brotli/-/brotli-1.3.4.tgz#3eefc5493218a99141771f351142dd640efde5d8" + integrity sha512-cKYjgaS2DMdCKF7R0F5cgx1nfBYObN2ihIuPGQ4/dlIY6RpV7OWNwe9L8V4tTVKL2eZqOkNM9FM/rgTvLf4oXw== + dependencies: + "@types/node" "*" + "@types/cacheable-request@^6.0.1", "@types/cacheable-request@^6.0.2": version "6.0.3" resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz#a430b3260466ca7b5ca5bfd735693b36e7a9d183" @@ -7351,6 +7363,11 @@ resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== +"@uniswap/default-token-list@^11.13.0": + version "11.19.0" + resolved "https://registry.yarnpkg.com/@uniswap/default-token-list/-/default-token-list-11.19.0.tgz#12d4e40f6c79f794d3e3a71e2d4d9784fb6c967b" + integrity sha512-H/YLpxeZUrzT4Ki8mi4k5UiadREiLHg7WUqCv0Qt/VkOjX2mIBhrxCj1Wh61/J7lK0XqOjksfpm6RG1+YErPoQ== + "@uniswap/lib@1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@uniswap/lib/-/lib-1.1.1.tgz#0afd29601846c16e5d082866cbb24a9e0758e6bc" @@ -7361,19 +7378,74 @@ resolved "https://registry.yarnpkg.com/@uniswap/lib/-/lib-4.0.1-alpha.tgz#2881008e55f075344675b3bca93f020b028fbd02" integrity sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA== -"@uniswap/sdk-core@^4.2.0": - version "4.2.0" - resolved "https://registry.yarnpkg.com/@uniswap/sdk-core/-/sdk-core-4.2.0.tgz#9930f133baec9c1118d891ebf8fcba7f7efc153d" - integrity sha512-yXAMLHZRYYuh6KpN2nOlLTYBjGiopmI9WUB4Z0tyNkW4ZZub54cUt22eibpGbZAhRAMxclox9IPIs6wwrM3soQ== +"@uniswap/permit2-sdk@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@uniswap/permit2-sdk/-/permit2-sdk-1.3.0.tgz#b54124e570f0adbaca9d39b2de3054fd7d3798a1" + integrity sha512-LstYQWP47dwpQrgqBJ+ysFstne9LgI5FGiKHc2ewjj91MTY8Mq1reocu6U/VDncdR5ef30TUOcZ7gPExRY8r6Q== + dependencies: + ethers "^5.7.0" + tiny-invariant "^1.1.0" + +"@uniswap/router-sdk@^1.14.0", "@uniswap/router-sdk@^1.14.2": + version "1.14.3" + resolved "https://registry.yarnpkg.com/@uniswap/router-sdk/-/router-sdk-1.14.3.tgz#99bf2cb8a90073f00d6195ccc56e9eb2b21b2123" + integrity sha512-dYD0WdPvZoezPkNWGjOrgilnshdKN88ljricaY/9Zyu0FTd9KizdOqcP9wLPLPWXLyFDsVrEQqNHjJWf3ILeJw== + dependencies: + "@ethersproject/abi" "^5.5.0" + "@uniswap/sdk-core" "^5.8.0" + "@uniswap/swap-router-contracts" "^1.3.0" + "@uniswap/v2-sdk" "^4.6.0" + "@uniswap/v3-sdk" "^3.17.0" + "@uniswap/v4-sdk" "^1.10.3" + +"@uniswap/sdk-core@^5.0.0", "@uniswap/sdk-core@^5.3.1", "@uniswap/sdk-core@^5.8.0", "@uniswap/sdk-core@^5.8.1", "@uniswap/sdk-core@^5.8.2", "@uniswap/sdk-core@^5.9.0": + version "5.9.0" + resolved "https://registry.yarnpkg.com/@uniswap/sdk-core/-/sdk-core-5.9.0.tgz#8f1edf4d0e94b314f4394fa5abe0bf5fc9c5a79a" + integrity sha512-OME7WR6+5QwQs45A2079r+/FS0zU944+JCQwUX9GyIriCxqw2pGu4F9IEqmlwD+zSIMml0+MJnJJ47pFgSyWDw== dependencies: "@ethersproject/address" "^5.0.2" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "5.7.0" + "@ethersproject/strings" "5.7.0" big.js "^5.2.2" decimal.js-light "^2.5.0" jsbi "^3.1.4" tiny-invariant "^1.1.0" toformat "^2.0.0" -"@uniswap/swap-router-contracts@^1.2.1": +"@uniswap/smart-order-router@^4.7.8": + version "4.7.8" + resolved "https://registry.yarnpkg.com/@uniswap/smart-order-router/-/smart-order-router-4.7.8.tgz#4f07f9fdb057fe3403da5d0b573f6209ffa67e45" + integrity sha512-rMVpBL7oqPU2z6RWR4V9QToT2l6pJgeGI/JRd/rowFltnjh0VOsW/cAkWrSi6wVeEnxG7pwT+8GcxkyA0qhudQ== + dependencies: + "@eth-optimism/sdk" "^3.2.2" + "@types/brotli" "^1.3.4" + "@uniswap/default-token-list" "^11.13.0" + "@uniswap/permit2-sdk" "^1.3.0" + "@uniswap/router-sdk" "^1.14.0" + "@uniswap/sdk-core" "^5.9.0" + "@uniswap/swap-router-contracts" "^1.3.1" + "@uniswap/token-lists" "^1.0.0-beta.31" + "@uniswap/universal-router" "^1.6.0" + "@uniswap/universal-router-sdk" "^4.6.1" + "@uniswap/v2-sdk" "^4.6.1" + "@uniswap/v3-sdk" "^3.17.1" + "@uniswap/v4-sdk" "^1.10.0" + async-retry "^1.3.1" + await-timeout "^1.1.1" + axios "^0.21.1" + brotli "^1.3.3" + bunyan "^1.8.15" + bunyan-blackhole "^1.1.1" + ethers "^5.7.2" + graphql "^15.5.0" + graphql-request "^3.4.0" + lodash "^4.17.21" + mnemonist "^0.38.3" + node-cache "^5.1.2" + stats-lite "^2.2.0" + +"@uniswap/swap-router-contracts@^1.3.0", "@uniswap/swap-router-contracts@^1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@uniswap/swap-router-contracts/-/swap-router-contracts-1.3.1.tgz#0ebbb93eb578625618ed9489872de381f9c66fb4" integrity sha512-mh/YNbwKb7Mut96VuEtL+Z5bRe0xVIbjjiryn+iMMrK2sFKhR4duk/86mEz0UO5gSx4pQIw9G5276P5heY/7Rg== @@ -7385,12 +7457,53 @@ dotenv "^14.2.0" hardhat-watcher "^2.1.1" +"@uniswap/token-lists@^1.0.0-beta.31": + version "1.0.0-beta.34" + resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.34.tgz#879461f5d4009327a24259bbab797e0f22db58c8" + integrity sha512-Hc3TfrFaupg0M84e/Zv7BoF+fmMWDV15mZ5s8ZQt2qZxUcNw2GQW+L6L/2k74who31G+p1m3GRYbJpAo7d1pqA== + +"@uniswap/universal-router-sdk@^4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@uniswap/universal-router-sdk/-/universal-router-sdk-4.6.1.tgz#0d4aa108b8d0cbef7878015c6c4d40a62220c6ee" + integrity sha512-HfPYd8xz3/Sjb8if/wAPFOzj0+m7173PVLy+VzF4HrbBwDreU0amYHwf4zQBjffXmys2QtOdwfu7BfZrUwx0kQ== + dependencies: + "@openzeppelin/contracts" "4.7.0" + "@uniswap/permit2-sdk" "^1.3.0" + "@uniswap/router-sdk" "^1.14.2" + "@uniswap/sdk-core" "^5.8.2" + "@uniswap/universal-router" "2.0.0-beta.2" + "@uniswap/v2-core" "^1.0.1" + "@uniswap/v2-sdk" "^4.6.0" + "@uniswap/v3-core" "1.0.0" + "@uniswap/v3-sdk" "^3.18.1" + "@uniswap/v4-sdk" "^1.10.0" + bignumber.js "^9.0.2" + ethers "^5.7.0" + +"@uniswap/universal-router@2.0.0-beta.2": + version "2.0.0-beta.2" + resolved "https://registry.yarnpkg.com/@uniswap/universal-router/-/universal-router-2.0.0-beta.2.tgz#0f891c4772733c356465d8ed2f76a43989ec9092" + integrity sha512-/USVkWZrOCjLeZluR7Yk8SpfWDUKG/MLcOyuxuwnqM1xCJj5ekguSYhct+Yfo/3t9fsZcnL8vSYgz0MKqAomGg== + dependencies: + "@openzeppelin/contracts" "5.0.2" + "@uniswap/v2-core" "1.0.1" + "@uniswap/v3-core" "1.0.0" + +"@uniswap/universal-router@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@uniswap/universal-router/-/universal-router-1.6.0.tgz#3d7372e98a0303c70587802ee6841b8b6b42fc6f" + integrity sha512-Gt0b0rtMV1vSrgXY3vz5R1RCZENB+rOkbOidY9GvcXrK1MstSrQSOAc+FCr8FSgsDhmRAdft0lk5YUxtM9i9Lg== + dependencies: + "@openzeppelin/contracts" "4.7.0" + "@uniswap/v2-core" "1.0.1" + "@uniswap/v3-core" "1.0.0" + "@uniswap/v2-core@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@uniswap/v2-core/-/v2-core-1.0.0.tgz#e0fab91a7d53e8cafb5326ae4ca18351116b0844" integrity sha512-BJiXrBGnN8mti7saW49MXwxDBRFiWemGetE58q8zgfnPPzQKq55ADltEILqOt6VFZ22kVeVKbF8gVd8aY3l7pA== -"@uniswap/v2-core@^1.0.1": +"@uniswap/v2-core@1.0.1", "@uniswap/v2-core@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@uniswap/v2-core/-/v2-core-1.0.1.tgz#af8f508bf183204779938969e2e54043e147d425" integrity sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q== @@ -7403,6 +7516,17 @@ "@uniswap/lib" "1.1.1" "@uniswap/v2-core" "1.0.0" +"@uniswap/v2-sdk@^4.6.0", "@uniswap/v2-sdk@^4.6.1": + version "4.6.2" + resolved "https://registry.yarnpkg.com/@uniswap/v2-sdk/-/v2-sdk-4.6.2.tgz#6267f66b6c2dc2e9363a89925673aa01aada9a56" + integrity sha512-6VrXfbq4XAVTUle4jH9jfKRk2ZaOOClDrL+HPB4jzIVgoKt45hhWjZLkCPxfVahr/OFTAaOTFCIt47Le41PYKw== + dependencies: + "@ethersproject/address" "^5.0.2" + "@ethersproject/solidity" "^5.0.9" + "@uniswap/sdk-core" "^5.9.0" + tiny-invariant "^1.1.0" + tiny-warning "^1.0.3" + "@uniswap/v3-core@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@uniswap/v3-core/-/v3-core-1.0.0.tgz#6c24adacc4c25dceee0ba3ca142b35adbd7e359d" @@ -7424,15 +7548,29 @@ "@uniswap/v3-core" "^1.0.0" base64-sol "1.0.1" -"@uniswap/v3-sdk@^3.11.0": - version "3.11.0" - resolved "https://registry.yarnpkg.com/@uniswap/v3-sdk/-/v3-sdk-3.11.0.tgz#328309fbafddd8c618b7b6850bb99cacf6733a79" - integrity sha512-gz6Q6SlN34AXvxhyz181F90D4OuIkxLnzBAucEzB9Fv3Z+3orHZY/SpGaD02nP1VsNQVu/DQvOsdkPUDGn1Y9Q== +"@uniswap/v3-sdk@3.12.0": + version "3.12.0" + resolved "https://registry.yarnpkg.com/@uniswap/v3-sdk/-/v3-sdk-3.12.0.tgz#2d819aa777578b747c880e0bc86a9718354140c5" + integrity sha512-mUCg9HLKl20h6W8+QtELqN/uaO47/KDSf+EOht+W3C6jt2eGuzSANqS2CY7i8MsAsnZ+MjPhmN+JTOIvf7azfA== dependencies: - "@ethersproject/abi" "^5.0.12" + "@ethersproject/abi" "^5.5.0" "@ethersproject/solidity" "^5.0.9" - "@uniswap/sdk-core" "^4.2.0" - "@uniswap/swap-router-contracts" "^1.2.1" + "@uniswap/sdk-core" "^5.0.0" + "@uniswap/swap-router-contracts" "^1.3.0" + "@uniswap/v3-periphery" "^1.1.1" + "@uniswap/v3-staker" "1.0.0" + tiny-invariant "^1.1.0" + tiny-warning "^1.0.3" + +"@uniswap/v3-sdk@^3.17.0", "@uniswap/v3-sdk@^3.17.1", "@uniswap/v3-sdk@^3.18.1": + version "3.18.1" + resolved "https://registry.yarnpkg.com/@uniswap/v3-sdk/-/v3-sdk-3.18.1.tgz#cb714b252336ba662a3298c0525f6668101b0fef" + integrity sha512-TGrKLToSWwfx6VV2d7fh4kwQMlgspXTLE49ep5zfYODVVqV6WhrRdbteHb3e0bjdjxGSj0gzoLmhsjmoJTE1/g== + dependencies: + "@ethersproject/abi" "^5.5.0" + "@ethersproject/solidity" "^5.0.9" + "@uniswap/sdk-core" "^5.8.1" + "@uniswap/swap-router-contracts" "^1.3.0" "@uniswap/v3-periphery" "^1.1.1" "@uniswap/v3-staker" "1.0.0" tiny-invariant "^1.1.0" @@ -7447,6 +7585,17 @@ "@uniswap/v3-core" "1.0.0" "@uniswap/v3-periphery" "^1.0.1" +"@uniswap/v4-sdk@^1.10.0", "@uniswap/v4-sdk@^1.10.3": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@uniswap/v4-sdk/-/v4-sdk-1.11.0.tgz#10087014f09f795aeb0372949198052f29d946e4" + integrity sha512-j7ZehPr+gKtAwD8ojtvefwYSIRatU9XCTCCWcQDHeOSEPiy63ijq4Nq7jtIvzdaq1BlvIB3qVzTI4V0mz6Wq9g== + dependencies: + "@ethersproject/solidity" "^5.0.9" + "@uniswap/sdk-core" "^5.3.1" + "@uniswap/v3-sdk" "3.12.0" + tiny-invariant "^1.1.0" + tiny-warning "^1.0.3" + "@upstash/redis@^1.31.3": version "1.34.0" resolved "https://registry.yarnpkg.com/@upstash/redis/-/redis-1.34.0.tgz#f32cd53ebeeafbba7eca10f8597a573d5a2fed0d" @@ -8744,7 +8893,7 @@ async-mutex@^0.5.0: dependencies: tslib "^2.4.0" -async-retry@^1.3.3: +async-retry@^1.3.1, async-retry@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== @@ -8763,12 +8912,7 @@ async@^2.0.1, async@^2.1.2, async@^2.4.0, async@^2.5.0: dependencies: lodash "^4.17.14" -async@^3.2.0, async@^3.2.3: - version "3.2.4" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" - integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== - -async@^3.2.5: +async@^3.2.0, async@^3.2.3, async@^3.2.5: version "3.2.5" resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== @@ -8805,6 +8949,11 @@ await-semaphore@^0.1.3: resolved "https://registry.yarnpkg.com/await-semaphore/-/await-semaphore-0.1.3.tgz#2b88018cc8c28e06167ae1cdff02504f1f9688d3" integrity sha512-d1W2aNSYcz/sxYO4pMGX9vq65qOTu0P800epMud+6cYYX0QcT7zyqcxec3VWzpgvdXo57UWmVbZpLMjX2m1I7Q== +await-timeout@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/await-timeout/-/await-timeout-1.1.1.tgz#d42062ee6bc4eb271fe4d4f851eb658dae7e3906" + integrity sha512-gsDXAS6XVc4Jt+7S92MPX6Noq69bdeXUPEaXd8dk3+yVr629LTDLxNt4j1ycBbrU+AStK2PhKIyNIM+xzWMVOQ== + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -8854,16 +9003,7 @@ axios@^0.27.2: follow-redirects "^1.14.9" form-data "^4.0.0" -axios@^1.5.1, axios@^1.6.0, axios@^1.6.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" - integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== - dependencies: - follow-redirects "^1.15.6" - form-data "^4.0.0" - proxy-from-env "^1.1.0" - -axios@^1.7.4: +axios@^1.5.1, axios@^1.6.0, axios@^1.6.2, axios@^1.7.4: version "1.7.7" resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== @@ -9577,7 +9717,7 @@ base-x@^4.0.0: resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a" integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw== -base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1: +base64-js@^1.1.2, base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -9885,6 +10025,13 @@ brorand@^1.0.1, brorand@^1.1.0: resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== +brotli@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/brotli/-/brotli-1.3.3.tgz#7365d8cc00f12cf765d2b2c898716bcf4b604d48" + integrity sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg== + dependencies: + base64-js "^1.1.2" + browser-assert@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/browser-assert/-/browser-assert-1.2.1.tgz#9aaa5a2a8c74685c2ae05bfe46efd606f068c200" @@ -10107,6 +10254,23 @@ bundle-require@^4.0.0: dependencies: load-tsconfig "^0.2.3" +bunyan-blackhole@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/bunyan-blackhole/-/bunyan-blackhole-1.1.1.tgz#b9208586dc0b4e47f4f713215b1bddd65e4f6257" + integrity sha512-UwzNPhbbSqbzeJhCbygqjlAY7p0ZUdv1ADXPQvDh3CA7VW3C/rCc1gaQO/8j9QL4vsKQCQZQSQIEwX+lxioPAQ== + dependencies: + stream-blackhole "^1.0.3" + +bunyan@^1.8.15: + version "1.8.15" + resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.8.15.tgz#8ce34ca908a17d0776576ca1b2f6cbd916e93b46" + integrity sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig== + optionalDependencies: + dtrace-provider "~0.8" + moment "^2.19.3" + mv "~2" + safe-json-stringify "~1" + busboy@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" @@ -10794,16 +10958,16 @@ clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" +clone@2.x, clone@^2.0.0, clone@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== + clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== -clone@^2.0.0, clone@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" - integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== - clsx@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" @@ -11849,6 +12013,13 @@ drbg.js@^1.0.1: create-hash "^1.1.2" create-hmac "^1.1.4" +dtrace-provider@~0.8: + version "0.8.8" + resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.8.tgz#2996d5490c37e1347be263b423ed7b297fb0d97e" + integrity sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg== + dependencies: + nan "^2.14.0" + duplexer2@~0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" @@ -13142,7 +13313,7 @@ ethers@5.5.3: "@ethersproject/web" "5.5.1" "@ethersproject/wordlists" "5.5.0" -ethers@5.7.2, ethers@^5.0.13, ethers@^5.4.2, ethers@^5.5.4, ethers@^5.6.8, ethers@^5.7.1, ethers@^5.7.2: +ethers@5.7.2, ethers@^5.0.13, ethers@^5.4.2, ethers@^5.5.4, ethers@^5.6.8, ethers@^5.7.0, ethers@^5.7.1, ethers@^5.7.2: version "5.7.2" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== @@ -14351,6 +14522,17 @@ glob@^5.0.15: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^6.0.1: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + integrity sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A== + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^7.0.0, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.2.0: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -14601,7 +14783,7 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== -graphql-request@^3.3.0, graphql-request@^3.5.0: +graphql-request@^3.3.0, graphql-request@^3.4.0, graphql-request@^3.5.0: version "3.7.0" resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-3.7.0.tgz#c7406e537084f8b9788541e3e6704340ca13055b" integrity sha512-dw5PxHCgBneN2DDNqpWu8QkbbJ07oOziy8z+bK/TAXufsOLaETuVO4GkXrbs0WjhdKhBMN3BkpN/RIvUHkmNUQ== @@ -14617,10 +14799,10 @@ graphql-tag@^2.11.0: dependencies: tslib "^2.1.0" -graphql@^15.4.0, graphql@^15.6.1: - version "15.8.0" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38" - integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw== +graphql@^15.4.0, graphql@^15.5.0, graphql@^15.6.1: + version "15.9.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.9.0.tgz#4e8ca830cfd30b03d44d3edd9cac2b0690304b53" + integrity sha512-GCOQdvm7XxV1S4U4CGrsdlEN37245eC8P9zaYCMr6K1BG0IPGy5lUwmJsEOGyl1GD6HXjOtl2keCP9asRBwNvA== growl@1.10.5: version "1.10.5" @@ -16046,6 +16228,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +isnumber@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isnumber/-/isnumber-1.0.0.tgz#0e3f9759b581d99dd85086f0ec2a74909cfadd01" + integrity sha512-JLiSz/zsZcGFXPrB4I/AGBvtStkt+8QmksyZBZnVXnnK9XdTEyz0tX8CRYljtwYDuIuZzih6DpHQdi+3Q6zHPw== + iso-constants@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/iso-constants/-/iso-constants-0.1.2.tgz#3d2456ed5aeaa55d18564f285ba02a47a0d885b4" @@ -17052,16 +17239,7 @@ jws@^4.0.0: jwa "^2.0.0" safe-buffer "^5.0.1" -keccak@^3.0.0, keccak@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.3.tgz#4bc35ad917be1ef54ff246f904c2bbbf9ac61276" - integrity sha512-JZrLIAJWuZxKbCilMpNz5Vj7Vtb4scDG3dMXLOsbzBmQGyjwE61BbW7bJkfKKCShXiQZt3T6sBgALRtmd+nZaQ== - dependencies: - node-addon-api "^2.0.0" - node-gyp-build "^4.2.0" - readable-stream "^3.6.0" - -keccak@^3.0.3: +keccak@^3.0.0, keccak@^3.0.2, keccak@^3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.4.tgz#edc09b89e633c0549da444432ecf062ffadee86d" integrity sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q== @@ -18172,7 +18350,7 @@ mkdirp@*: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== -mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@^0.5.5: +mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@^0.5.5, mkdirp@~0.5.1: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== @@ -18184,7 +18362,7 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mnemonist@^0.38.0: +mnemonist@^0.38.0, mnemonist@^0.38.3: version "0.38.5" resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.38.5.tgz#4adc7f4200491237fe0fa689ac0b86539685cade" integrity sha512-bZTFT5rrPKtPJxj8KSV0WkPyNxl72vQepqqVUAW2ARUpUSF2qXMB6jZj7hW5/k7C1rtpzqbD/IIbJwLXUjCHeg== @@ -18253,7 +18431,7 @@ mock-fs@^4.1.0: resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.14.0.tgz#ce5124d2c601421255985e6e94da80a7357b1b18" integrity sha512-qYvlv/exQ4+svI3UOvPUpLDF0OMX5euvUH0Ny4N5QyRyhNdgAgUrVH3iUINSzEPLvx0kbo/Bp28GJKIqvE7URw== -moment@^2.22.1, moment@^2.24.0: +moment@^2.19.3, moment@^2.22.1, moment@^2.24.0: version "2.30.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== @@ -18456,6 +18634,15 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +mv@~2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/mv/-/mv-2.1.1.tgz#ae6ce0d6f6d5e0a4f7d893798d03c1ea9559b6a2" + integrity sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg== + dependencies: + mkdirp "~0.5.1" + ncp "~2.0.0" + rimraf "~2.4.0" + mz@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" @@ -18537,6 +18724,11 @@ natural-orderby@^2.0.1: resolved "https://registry.yarnpkg.com/natural-orderby/-/natural-orderby-2.0.3.tgz#8623bc518ba162f8ff1cdb8941d74deb0fdcc016" integrity sha512-p7KTHxU0CUrcOXe62Zfrb5Z13nLvPhSWR/so3kFulUQU0sgUll2Z0LwpsLN351eOOD+hRGu/F1g+6xDfPeD++Q== +ncp@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" + integrity sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA== + negotiator@0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" @@ -18574,6 +18766,13 @@ node-addon-api@^5.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== +node-cache@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d" + integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg== + dependencies: + clone "2.x" + node-dir@^0.1.17: version "0.1.17" resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5" @@ -21005,6 +21204,13 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +rimraf@~2.4.0: + version "2.4.5" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da" + integrity sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ== + dependencies: + glob "^6.0.1" + rimraf@~2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" @@ -21176,6 +21382,11 @@ safe-event-emitter@^1.0.1: dependencies: events "^3.0.0" +safe-json-stringify@~1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd" + integrity sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg== + safe-json-utils@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/safe-json-utils/-/safe-json-utils-1.1.1.tgz#0e883874467d95ab914c3f511096b89bfb3e63b1" @@ -21951,6 +22162,13 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" +stats-lite@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/stats-lite/-/stats-lite-2.2.0.tgz#278a5571fa1d2e8b1691295dccc0235282393bbf" + integrity sha512-/Kz55rgUIv2KP2MKphwYT/NCuSfAlbbMRv2ZWw7wyXayu230zdtzhxxuXXcvsc6EmmhS8bSJl3uS1wmMHFumbA== + dependencies: + isnumber "~1.0.0" + statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" @@ -21980,6 +22198,11 @@ storybook@^7.5.3: dependencies: "@storybook/cli" "7.5.3" +stream-blackhole@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/stream-blackhole/-/stream-blackhole-1.0.3.tgz#6fc2e2c2e9d9fde6be8c68d3db88de09802e4d63" + integrity sha512-7NWl3dkmCd12mPkEwTbBPGxwvxj7L4O9DTjJudn02Fmk9K+RuPaDF8zeGo3kmjbsffU5E1aGpZ1dTR9AaRg6AQ== + stream-buffers@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-3.0.2.tgz#5249005a8d5c2d00b3a32e6e0a6ea209dc4f3521" From 1e369330e34a1434e48be0a228accb1795434b7e Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Thu, 14 Nov 2024 12:24:23 +0700 Subject: [PATCH 03/47] feat: uniswap min output cross swap quotes resolver (#1271) * feat: cross swap quotes resolve uniswap min output * add test script * refine min output and leftover tokens handling --- api/_dexes/cross-swap.ts | 142 +++++++++++ api/_dexes/types.ts | 49 +++- api/_dexes/uniswap.ts | 481 +++++++++++++++++++++++++++++++++++--- api/_dexes/utils.ts | 19 +- api/_multicall-handler.ts | 48 ++++ api/_utils.ts | 173 ++++++++++++++ api/swap-quote.ts | 3 +- api/swap.ts | 107 +++++++-- scripts/tests/swap.ts | 64 +++++ 9 files changed, 1024 insertions(+), 62 deletions(-) create mode 100644 api/_dexes/cross-swap.ts create mode 100644 api/_multicall-handler.ts create mode 100644 scripts/tests/swap.ts diff --git a/api/_dexes/cross-swap.ts b/api/_dexes/cross-swap.ts new file mode 100644 index 000000000..c435a20c3 --- /dev/null +++ b/api/_dexes/cross-swap.ts @@ -0,0 +1,142 @@ +import { + isRouteEnabled, + isInputTokenBridgeable, + isOutputTokenBridgeable, + getBridgeQuoteForMinOutput, +} from "../_utils"; +import { + getUniswapCrossSwapQuotesForMinOutputB2A, + getUniswapCrossSwapQuotesForMinOutputA2B, + getBestUniswapCrossSwapQuotesForMinOutputA2A, +} from "./uniswap"; +import { CrossSwap, CrossSwapQuotes } from "./types"; + +export type CrossSwapType = + (typeof CROSS_SWAP_TYPE)[keyof typeof CROSS_SWAP_TYPE]; + +export type AmountType = (typeof AMOUNT_TYPE)[keyof typeof AMOUNT_TYPE]; + +export const AMOUNT_TYPE = { + EXACT_INPUT: "exactInput", + MIN_OUTPUT: "minOutput", +} as const; + +export const CROSS_SWAP_TYPE = { + BRIDGEABLE_TO_BRIDGEABLE: "bridgeableToBridgeable", + BRIDGEABLE_TO_ANY: "bridgeableToAny", + ANY_TO_BRIDGEABLE: "anyToBridgeable", + ANY_TO_ANY: "anyToAny", +} as const; + +export const PREFERRED_BRIDGE_TOKENS = ["WETH", "USDC"]; + +export async function getCrossSwapQuotes( + crossSwap: CrossSwap +): Promise { + if (crossSwap.type === AMOUNT_TYPE.EXACT_INPUT) { + // @TODO: Add support for exact input amount + throw new Error("Not implemented yet"); + } + + if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT) { + return getCrossSwapQuotesForMinOutput(crossSwap); + } + + throw new Error("Invalid amount type"); +} + +export async function getCrossSwapQuotesForMinOutput(crossSwap: CrossSwap) { + const crossSwapType = getCrossSwapType({ + inputToken: crossSwap.inputToken.address, + originChainId: crossSwap.inputToken.chainId, + outputToken: crossSwap.outputToken.address, + destinationChainId: crossSwap.outputToken.chainId, + }); + + if (crossSwapType === CROSS_SWAP_TYPE.BRIDGEABLE_TO_BRIDGEABLE) { + return getCrossSwapQuotesForMinOutputB2B(crossSwap); + } + + if (crossSwapType === CROSS_SWAP_TYPE.BRIDGEABLE_TO_ANY) { + return getCrossSwapQuotesForMinOutputB2A(crossSwap); + } + + if (crossSwapType === CROSS_SWAP_TYPE.ANY_TO_BRIDGEABLE) { + return getCrossSwapQuotesForMinOutputA2B(crossSwap); + } + + if (crossSwapType === CROSS_SWAP_TYPE.ANY_TO_ANY) { + return getCrossSwapQuotesForMinOutputA2A(crossSwap); + } + + throw new Error("Invalid cross swap type"); +} + +// @TODO: Implement the following function +export async function getCrossSwapQuotesForExactInput(crossSwap: CrossSwap) { + throw new Error("Not implemented yet"); +} + +export async function getCrossSwapQuotesForMinOutputB2B(crossSwap: CrossSwap) { + const bridgeQuote = await getBridgeQuoteForMinOutput({ + inputToken: crossSwap.inputToken, + outputToken: crossSwap.outputToken, + minOutputAmount: crossSwap.amount, + // @TODO: handle ETH/WETH message generation + message: "0x", + }); + return { + crossSwap, + destinationSwapQuote: undefined, + bridgeQuote, + originSwapQuote: undefined, + }; +} + +export async function getCrossSwapQuotesForMinOutputB2A(crossSwap: CrossSwap) { + // @TODO: Add support for other DEXes / aggregators + return getUniswapCrossSwapQuotesForMinOutputB2A(crossSwap); +} + +export async function getCrossSwapQuotesForMinOutputA2B(crossSwap: CrossSwap) { + // @TODO: Add support for other DEXes / aggregators + return getUniswapCrossSwapQuotesForMinOutputA2B(crossSwap); +} + +export async function getCrossSwapQuotesForMinOutputA2A(crossSwap: CrossSwap) { + // @TODO: Add support for other DEXes / aggregators + return getBestUniswapCrossSwapQuotesForMinOutputA2A(crossSwap, { + preferredBridgeTokens: PREFERRED_BRIDGE_TOKENS, + bridgeRoutesLimit: 2, + }); +} + +export function getCrossSwapType(params: { + inputToken: string; + originChainId: number; + outputToken: string; + destinationChainId: number; +}): CrossSwapType { + if ( + isRouteEnabled( + params.originChainId, + params.destinationChainId, + params.inputToken, + params.outputToken + ) + ) { + return CROSS_SWAP_TYPE.BRIDGEABLE_TO_BRIDGEABLE; + } + + if (isInputTokenBridgeable(params.inputToken, params.originChainId)) { + return CROSS_SWAP_TYPE.BRIDGEABLE_TO_ANY; + } + + if (isOutputTokenBridgeable(params.outputToken, params.destinationChainId)) { + return CROSS_SWAP_TYPE.ANY_TO_BRIDGEABLE; + } + + return CROSS_SWAP_TYPE.ANY_TO_ANY; +} + +export function calcFees() {} diff --git a/api/_dexes/types.ts b/api/_dexes/types.ts index 51782b50d..da5e448c8 100644 --- a/api/_dexes/types.ts +++ b/api/_dexes/types.ts @@ -1,3 +1,9 @@ +import { BigNumber } from "ethers"; +import { getSuggestedFees } from "../_utils"; +import { AmountType, CrossSwapType } from "./cross-swap"; + +export type { AmountType, CrossSwapType }; + export type Token = { address: string; decimals: number; @@ -12,7 +18,18 @@ export type Swap = { amount: string; recipient: string; slippageTolerance: number; - type: "EXACT_INPUT" | "MIN_OUTPUT"; + type: AmountType; +}; + +export type CrossSwap = { + amount: BigNumber; + inputToken: Token; + outputToken: Token; + recipient: string; + slippageTolerance: number; + type: AmountType; + refundOnOrigin: boolean; + refundAddress?: string; }; export type SupportedDex = "1inch" | "uniswap"; @@ -25,3 +42,33 @@ export type OriginSwapQuoteAndCalldata = { dex: SupportedDex; slippage: number; }; + +export type SwapQuote = { + maximumAmountIn: BigNumber; + minAmountOut: BigNumber; + expectedAmountOut: BigNumber; + expectedAmountIn: BigNumber; + slippageTolerance: number; + swapTx: { + to: string; + data: string; + value: string; + }; + tokenIn: Token; + tokenOut: Token; +}; + +export type CrossSwapQuotes = { + crossSwap: CrossSwap; + bridgeQuote: { + message?: string; + inputToken: Token; + outputToken: Token; + inputAmount: BigNumber; + outputAmount: BigNumber; + minOutputAmount: BigNumber; + suggestedFees: Awaited>; + }; + destinationSwapQuote?: SwapQuote; + originSwapQuote?: SwapQuote; +}; diff --git a/api/_dexes/uniswap.ts b/api/_dexes/uniswap.ts index 0e0100c69..cf2fc2c53 100644 --- a/api/_dexes/uniswap.ts +++ b/api/_dexes/uniswap.ts @@ -1,4 +1,4 @@ -import { ethers } from "ethers"; +import { BigNumber, ethers } from "ethers"; import { CurrencyAmount, Percent, Token, TradeType } from "@uniswap/sdk-core"; import { AlphaRouter, @@ -8,14 +8,29 @@ import { import { CHAIN_IDs } from "@across-protocol/constants"; import { utils } from "@across-protocol/sdk"; -import { getProvider } from "../_utils"; +import { + getProvider, + getRouteByInputTokenAndDestinationChain, + getTokenByAddress, + getBridgeQuoteForMinOutput, + getRoutesByChainIds, + getRouteByOutputTokenAndOriginChain, +} from "../_utils"; import { TOKEN_SYMBOLS_MAP } from "../_constants"; +import { + buildMulticallHandlerMessage, + getMultiCallHandlerAddress, +} from "../_multicall-handler"; import { OriginSwapQuoteAndCalldata, Token as AcrossToken, Swap, + CrossSwap, + SwapQuote, + CrossSwapQuotes, } from "./types"; import { getSwapAndBridgeAddress, NoSwapRouteError } from "./utils"; +import { AMOUNT_TYPE } from "./cross-swap"; // Taken from here: https://docs.uniswap.org/contracts/v3/reference/deployments/ export const SWAP_ROUTER_02_ADDRESS = { @@ -39,6 +54,10 @@ const TESTNET_TO_MAINNET = { [CHAIN_IDs.ARBITRUM_SEPOLIA]: CHAIN_IDs.ARBITRUM, }; +/** + * Returns Uniswap v3 quote for a swap with exact input amount. + * @param swap + */ export async function getUniswapQuoteForOriginSwapExactInput( swap: Omit ): Promise { @@ -51,56 +70,402 @@ export async function getUniswapQuoteForOriginSwapExactInput( swap.tokenIn = getMainnetToken(swap.tokenIn); swap.tokenOut = getMainnetToken(swap.tokenOut); - const { route, options } = await getUniswapQuote({ + const { swapTx, minAmountOut } = await getUniswapQuote({ ...swap, recipient: swapAndBridgeAddress, }); - if (!route.methodParameters) { - throw new NoSwapRouteError({ - dex: "uniswap", - tokenInSymbol: swap.tokenIn.symbol, - tokenOutSymbol: swap.tokenOut.symbol, - chainId: swap.chainId, - swapType: "EXACT_INPUT", - }); - } - // replace mainnet token addresses with initial token addresses in calldata - route.methodParameters.calldata = route.methodParameters.calldata.replace( + swapTx.data = swapTx.data.replace( swap.tokenIn.address.toLowerCase().slice(2), initialTokenIn.address.toLowerCase().slice(2) ); - route.methodParameters.calldata = route.methodParameters.calldata.replace( + swapTx.data = swapTx.data.replace( swap.tokenOut.address.toLowerCase().slice(2), initialTokenOut.address.toLowerCase().slice(2) ); return { - minExpectedInputTokenAmount: ethers.utils - .parseUnits( - route.trade.minimumAmountOut(options.slippageTolerance).toExact(), - swap.tokenIn.decimals - ) - .toString(), - routerCalldata: route.methodParameters.calldata, - value: ethers.BigNumber.from(route.methodParameters.value).toString(), + minExpectedInputTokenAmount: minAmountOut.toString(), + routerCalldata: swapTx.data, + value: BigNumber.from(swapTx.value).toString(), swapAndBridgeAddress, dex: "uniswap", slippage: swap.slippageTolerance, }; } -export async function getUniswapQuote(swap: Swap) { +/** + * Returns Uniswap v3 quote for a swap with min. output amount for route + * BRIDGEABLE input token -> ANY output token, e.g. USDC -> ARB. Required steps: + * 1. Get destination swap quote for bridgeable output token -> any token + * 2. Get bridge quote for bridgeable input token -> bridgeable output token + */ +export async function getUniswapCrossSwapQuotesForMinOutputB2A( + crossSwap: CrossSwap +) { + const destinationSwapChainId = crossSwap.outputToken.chainId; + const bridgeRoute = getRouteByInputTokenAndDestinationChain( + crossSwap.inputToken.address, + destinationSwapChainId + ); + + if (!bridgeRoute) { + throw new Error( + `No bridge route found for input token ${crossSwap.inputToken.symbol} ` + + `${crossSwap.inputToken.chainId} -> ${destinationSwapChainId}` + ); + } + + const _bridgeableOutputToken = getTokenByAddress( + bridgeRoute.toTokenAddress, + bridgeRoute.toChain + ); + + if (!_bridgeableOutputToken) { + throw new Error( + `No bridgeable output token found for ${bridgeRoute.toTokenAddress} on chain ${bridgeRoute.toChain}` + ); + } + + const bridgeableOutputToken = { + address: bridgeRoute.toTokenAddress, + decimals: _bridgeableOutputToken.decimals, + symbol: _bridgeableOutputToken.symbol, + chainId: destinationSwapChainId, + }; + + // 1. Get destination swap quote for bridgeable output token -> any token + const destinationSwapQuote = await getUniswapQuote({ + chainId: destinationSwapChainId, + tokenIn: bridgeableOutputToken, + tokenOut: crossSwap.outputToken, + amount: crossSwap.amount.toString(), + recipient: crossSwap.recipient, + slippageTolerance: crossSwap.slippageTolerance, + type: AMOUNT_TYPE.MIN_OUTPUT, + }); + + // 2. Get bridge quote for bridgeable input token -> bridgeable output token + const bridgeQuote = await getBridgeQuoteForMinOutput({ + inputToken: crossSwap.inputToken, + outputToken: bridgeableOutputToken, + minOutputAmount: destinationSwapQuote.maximumAmountIn, + recipient: getMultiCallHandlerAddress(destinationSwapChainId), + message: buildMulticallHandlerMessage({ + // @TODO: handle fallback recipient for params `refundOnOrigin` and `refundAddress` + fallbackRecipient: crossSwap.recipient, + actions: [ + { + target: destinationSwapQuote.swapTx.to, + callData: destinationSwapQuote.swapTx.data, + value: destinationSwapQuote.swapTx.value, + }, + ], + }), + }); + + // 3. Re-fetch destination swap quote with updated input amount and EXACT_INPUT type. + // This prevents leftover tokens in the MulticallHandler contract. + const updatedDestinationSwapQuote = await getUniswapQuote({ + chainId: destinationSwapChainId, + tokenIn: bridgeableOutputToken, + tokenOut: crossSwap.outputToken, + amount: bridgeQuote.outputAmount.toString(), + recipient: crossSwap.recipient, + slippageTolerance: crossSwap.slippageTolerance, + type: AMOUNT_TYPE.EXACT_INPUT, + }); + + // 4. Rebuild message + bridgeQuote.message = buildMulticallHandlerMessage({ + // @TODO: handle fallback recipient for params `refundOnOrigin` and `refundAddress` + fallbackRecipient: crossSwap.recipient, + actions: [ + { + target: updatedDestinationSwapQuote.swapTx.to, + callData: updatedDestinationSwapQuote.swapTx.data, + value: updatedDestinationSwapQuote.swapTx.value, + }, + ], + }); + + return { + crossSwap, + bridgeQuote, + destinationSwapQuote: updatedDestinationSwapQuote, + originSwapQuote: undefined, + }; +} + +/** + * Returns Uniswap v3 quote for a swap with min. output amount for route + * ANY input token -> BRIDGEABLE output token, e.g. ARB -> USDC. Required steps: + * 1. Get bridge quote for bridgeable input token -> bridgeable output token + * 2. Get origin swap quote for any input token -> bridgeable input token + */ +export async function getUniswapCrossSwapQuotesForMinOutputA2B( + crossSwap: CrossSwap +) { + const originSwapChainId = crossSwap.inputToken.chainId; + const bridgeRoute = getRouteByOutputTokenAndOriginChain( + crossSwap.outputToken.address, + originSwapChainId + ); + + if (!bridgeRoute) { + throw new Error( + `No bridge route found for output token ${crossSwap.outputToken.symbol} ` + + `${originSwapChainId} -> ${crossSwap.outputToken.chainId}` + ); + } + + const _bridgeableInputToken = getTokenByAddress( + bridgeRoute.fromTokenAddress, + bridgeRoute.fromChain + ); + + if (!_bridgeableInputToken) { + throw new Error( + `No bridgeable input token found for ${bridgeRoute.toTokenAddress} on chain ${bridgeRoute.toChain}` + ); + } + + const bridgeableInputToken = { + address: bridgeRoute.fromTokenAddress, + decimals: _bridgeableInputToken.decimals, + symbol: _bridgeableInputToken.symbol, + chainId: bridgeRoute.fromChain, + }; + + // 1. Get bridge quote for bridgeable input token -> bridgeable output token + const bridgeQuote = await getBridgeQuoteForMinOutput({ + inputToken: bridgeableInputToken, + outputToken: crossSwap.outputToken, + minOutputAmount: crossSwap.amount, + // @TODO: handle ETH/WETH message generation + }); + + // 2. Get origin swap quote for any input token -> bridgeable input token + const originSwapQuote = await getUniswapQuote({ + chainId: originSwapChainId, + tokenIn: crossSwap.inputToken, + tokenOut: bridgeableInputToken, + amount: bridgeQuote.inputAmount.toString(), + recipient: getSwapAndBridgeAddress("uniswap", originSwapChainId), + slippageTolerance: crossSwap.slippageTolerance, + type: AMOUNT_TYPE.MIN_OUTPUT, + }); + + return { + crossSwap, + bridgeQuote, + destinationSwapQuote: undefined, + originSwapQuote, + }; +} + +/** + * Returns Uniswap v3 quote for a swap with min. output amount for route + * ANY input token -> ANY output token, e.g. ARB -> OP. We compare quotes from + * different bridge routes and return the best one. In this iteration, we only + * consider a hardcoded list of high-liquid bridge routes. + * @param crossSwap + * @param opts + */ +export async function getBestUniswapCrossSwapQuotesForMinOutputA2A( + crossSwap: CrossSwap, + opts: { + preferredBridgeTokens: string[]; + bridgeRoutesLimit: number; + } +) { + const originSwapChainId = crossSwap.inputToken.chainId; + const destinationSwapChainId = crossSwap.outputToken.chainId; + const allBridgeRoutes = getRoutesByChainIds( + originSwapChainId, + destinationSwapChainId + ); + + if (allBridgeRoutes.length === 0) { + throw new Error( + `No bridge routes found for ${originSwapChainId} -> ${destinationSwapChainId}` + ); + } + + const preferredBridgeRoutes = allBridgeRoutes.filter(({ fromTokenSymbol }) => + opts.preferredBridgeTokens.includes(fromTokenSymbol) + ); + const bridgeRoutesToCompare = ( + preferredBridgeRoutes.length > 0 ? preferredBridgeRoutes : allBridgeRoutes + ).slice(0, opts.bridgeRoutesLimit); + + if (bridgeRoutesToCompare.length === 0) { + throw new Error( + `No bridge routes to compare for ${originSwapChainId} -> ${destinationSwapChainId}` + ); + } + + const crossSwapQuotesSettledResults = await Promise.allSettled( + bridgeRoutesToCompare.map((bridgeRoute) => + getUniswapCrossSwapQuotesForMinOutputA2A(crossSwap, bridgeRoute) + ) + ); + const crossSwapQuotes = crossSwapQuotesSettledResults + .filter((res) => res.status === "fulfilled") + .map((res) => (res as PromiseFulfilledResult).value); + + if (crossSwapQuotes.length === 0) { + console.log("crossSwapQuotesSettledResults", crossSwapQuotesSettledResults); + throw new Error( + `No successful bridge quotes found for ${originSwapChainId} -> ${destinationSwapChainId}` + ); + } + + // Compare quotes by lowest input amount + const bestCrossSwapQuote = crossSwapQuotes.reduce((prev, curr) => + prev.originSwapQuote!.maximumAmountIn.lt( + curr.originSwapQuote!.maximumAmountIn + ) + ? prev + : curr + ); + return bestCrossSwapQuote; +} + +/** + * Returns Uniswap v3 quote for a swap with min. output amount for route + * ANY input token -> ANY output token, e.g. ARB -> OP, using a specific bridge route. + * @param crossSwap + * @param bridgeRoute + */ +export async function getUniswapCrossSwapQuotesForMinOutputA2A( + crossSwap: CrossSwap, + bridgeRoute: { + fromTokenAddress: string; + fromChain: number; + toTokenAddress: string; + toChain: number; + } +) { + const originSwapChainId = crossSwap.inputToken.chainId; + const destinationSwapChainId = crossSwap.outputToken.chainId; + + const _bridgeableInputToken = getTokenByAddress( + bridgeRoute.fromTokenAddress, + bridgeRoute.fromChain + ); + const _bridgeableOutputToken = getTokenByAddress( + bridgeRoute.toTokenAddress, + bridgeRoute.toChain + ); + + if (!_bridgeableInputToken) { + throw new Error( + `No bridgeable input token found for ${bridgeRoute.fromTokenAddress} on chain ${bridgeRoute.fromChain}` + ); + } + + if (!_bridgeableOutputToken) { + throw new Error( + `No bridgeable output token found for ${bridgeRoute.toTokenAddress} on chain ${bridgeRoute.toChain}` + ); + } + + const bridgeableInputToken = { + address: bridgeRoute.fromTokenAddress, + decimals: _bridgeableInputToken.decimals, + symbol: _bridgeableInputToken.symbol, + chainId: bridgeRoute.fromChain, + }; + const bridgeableOutputToken = { + address: bridgeRoute.toTokenAddress, + decimals: _bridgeableOutputToken.decimals, + symbol: _bridgeableOutputToken.symbol, + chainId: bridgeRoute.toChain, + }; + + // 1. Get destination swap quote for bridgeable output token -> any token + const destinationSwapQuote = await getUniswapQuote({ + chainId: destinationSwapChainId, + tokenIn: bridgeableOutputToken, + tokenOut: crossSwap.outputToken, + amount: crossSwap.amount.toString(), + recipient: crossSwap.recipient, + slippageTolerance: crossSwap.slippageTolerance, + type: AMOUNT_TYPE.MIN_OUTPUT, + }); + + // 2. Get bridge quote for bridgeable input token -> bridgeable output token + const bridgeQuote = await getBridgeQuoteForMinOutput({ + inputToken: bridgeableInputToken, + outputToken: bridgeableOutputToken, + minOutputAmount: destinationSwapQuote.maximumAmountIn, + recipient: getMultiCallHandlerAddress(destinationSwapChainId), + message: buildMulticallHandlerMessage({ + // @TODO: handle fallback recipient for params `refundOnOrigin` and `refundAddress` + fallbackRecipient: crossSwap.recipient, + actions: [ + { + target: destinationSwapQuote.swapTx.to, + callData: destinationSwapQuote.swapTx.data, + value: destinationSwapQuote.swapTx.value, + }, + ], + }), + }); + + // 3. Re-fetch destination swap quote with updated input amount and EXACT_INPUT type. + // This prevents leftover tokens in the MulticallHandler contract. + const updatedDestinationSwapQuote = await getUniswapQuote({ + chainId: destinationSwapChainId, + tokenIn: bridgeableOutputToken, + tokenOut: crossSwap.outputToken, + amount: bridgeQuote.outputAmount.toString(), + recipient: crossSwap.recipient, + slippageTolerance: crossSwap.slippageTolerance, + type: AMOUNT_TYPE.EXACT_INPUT, + }); + + // 4. Rebuild message + bridgeQuote.message = buildMulticallHandlerMessage({ + // @TODO: handle fallback recipient for params `refundOnOrigin` and `refundAddress` + fallbackRecipient: crossSwap.recipient, + actions: [ + { + target: updatedDestinationSwapQuote.swapTx.to, + callData: updatedDestinationSwapQuote.swapTx.data, + value: updatedDestinationSwapQuote.swapTx.value, + }, + ], + }); + + // 3. Get origin swap quote for any input token -> bridgeable input token + const originSwapQuote = await getUniswapQuote({ + chainId: originSwapChainId, + tokenIn: crossSwap.inputToken, + tokenOut: bridgeableInputToken, + amount: bridgeQuote.inputAmount.toString(), + recipient: getSwapAndBridgeAddress("uniswap", originSwapChainId), + slippageTolerance: crossSwap.slippageTolerance, + type: AMOUNT_TYPE.MIN_OUTPUT, + }); + + return { + crossSwap, + destinationSwapQuote: updatedDestinationSwapQuote, + bridgeQuote, + originSwapQuote, + }; +} + +export async function getUniswapQuote(swap: Swap): Promise { const { router, options } = getSwapRouterAndOptions(swap); const amountCurrency = - swap.type === "EXACT_INPUT" ? swap.tokenIn : swap.tokenOut; + swap.type === AMOUNT_TYPE.EXACT_INPUT ? swap.tokenIn : swap.tokenOut; const quoteCurrency = - swap.type === "EXACT_INPUT" ? swap.tokenOut : swap.tokenIn; - - console.log("amountCurrency", amountCurrency); - console.log("quoteCurrency", quoteCurrency); + swap.type === AMOUNT_TYPE.EXACT_INPUT ? swap.tokenOut : swap.tokenIn; const route = await router.route( CurrencyAmount.fromRawAmount( @@ -116,7 +481,7 @@ export async function getUniswapQuote(swap: Swap) { quoteCurrency.address, quoteCurrency.decimals ), - swap.type === "EXACT_INPUT" + swap.type === AMOUNT_TYPE.EXACT_INPUT ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT, options @@ -128,11 +493,49 @@ export async function getUniswapQuote(swap: Swap) { tokenInSymbol: swap.tokenIn.symbol, tokenOutSymbol: swap.tokenOut.symbol, chainId: swap.chainId, - swapType: "EXACT_INPUT", + swapType: swap.type, }); } - return { route, options }; + const swapQuote = { + tokenIn: swap.tokenIn, + tokenOut: swap.tokenOut, + maximumAmountIn: ethers.utils.parseUnits( + route.trade.maximumAmountIn(options.slippageTolerance).toExact(), + swap.tokenIn.decimals + ), + minAmountOut: ethers.utils.parseUnits( + route.trade.minimumAmountOut(options.slippageTolerance).toExact(), + swap.tokenOut.decimals + ), + expectedAmountOut: ethers.utils.parseUnits( + route.trade.outputAmount.toExact(), + swap.tokenOut.decimals + ), + expectedAmountIn: ethers.utils.parseUnits( + route.trade.inputAmount.toExact(), + swap.tokenIn.decimals + ), + slippageTolerance: swap.slippageTolerance, + swapTx: { + to: route.methodParameters.to, + data: route.methodParameters.calldata, + value: route.methodParameters.value, + }, + }; + + console.log("swapQuote", { + type: swap.type, + tokenIn: swapQuote.tokenIn.symbol, + tokenOut: swapQuote.tokenOut.symbol, + chainId: swap.chainId, + maximumAmountIn: swapQuote.maximumAmountIn.toString(), + minAmountOut: swapQuote.minAmountOut.toString(), + expectedAmountOut: swapQuote.expectedAmountOut.toString(), + expectedAmountIn: swapQuote.expectedAmountIn.toString(), + }); + + return swapQuote; } function getSwapRouterAndOptions(params: { @@ -149,11 +552,7 @@ function getSwapRouterAndOptions(params: { recipient: params.recipient, deadline: utils.getCurrentTime() + 30 * 60, // 30 minutes from now type: SwapType.SWAP_ROUTER_02, - slippageTolerance: new Percent( - // max. slippage decimals is 2 - Number(params.slippageTolerance.toFixed(2)) * 100, - 10_000 - ), + slippageTolerance: floatToPercent(params.slippageTolerance), }; return { router, @@ -161,6 +560,14 @@ function getSwapRouterAndOptions(params: { }; } +function floatToPercent(value: number) { + return new Percent( + // max. slippage decimals is 2 + Number(value.toFixed(2)) * 100, + 10_000 + ); +} + function getMainnetToken(token: AcrossToken) { const mainnetChainId = TESTNET_TO_MAINNET[token.chainId] || token.chainId; diff --git a/api/_dexes/utils.ts b/api/_dexes/utils.ts index 2d04103a5..06cb97ece 100644 --- a/api/_dexes/utils.ts +++ b/api/_dexes/utils.ts @@ -1,4 +1,6 @@ -import { ENABLED_ROUTES, getTokenByAddress } from "../_utils"; +import { UniversalSwapAndBridge__factory } from "@across-protocol/contracts/dist/typechain"; + +import { ENABLED_ROUTES, getProvider } from "../_utils"; export class UnsupportedDex extends Error { constructor(dex: string) { @@ -44,14 +46,13 @@ export function getSwapAndBridgeAddress(dex: string, chainId: number) { return address; } -export function getAcrossSwapType(params: { - inputToken: string; - originChainId: number; - outputToken: string; - destinationChainId: number; -}) { - // TODO: Implement this function - return "majorToMajor"; +export function getSwapAndBridge(dex: string, chainId: number) { + const swapAndBridgeAddress = getSwapAndBridgeAddress(dex, chainId); + + return UniversalSwapAndBridge__factory.connect( + swapAndBridgeAddress, + getProvider(chainId) + ); } function _isDexSupported( diff --git a/api/_multicall-handler.ts b/api/_multicall-handler.ts new file mode 100644 index 000000000..9bcb86a7b --- /dev/null +++ b/api/_multicall-handler.ts @@ -0,0 +1,48 @@ +import { BigNumber, ethers } from "ethers"; + +export function getMultiCallHandlerAddress(chainId: number) { + // @todo: use API to source addresses? + const defaultAddress = "0x924a9f036260DdD5808007E1AA95f08eD08aA569"; + switch (chainId) { + case 324: + return "0x863859ef502F0Ee9676626ED5B418037252eFeb2"; + case 59144: + return "0x1015c58894961F4F7Dd7D68ba033e28Ed3ee1cDB"; + default: + return defaultAddress; + } +} + +export function buildMulticallHandlerMessage(params: { + actions: Array<{ + target: string; + callData: string; + value: string; + }>; + fallbackRecipient: string; +}) { + const abiCoder = ethers.utils.defaultAbiCoder; + + return abiCoder.encode( + [ + "tuple(" + + "tuple(" + + "address target," + + "bytes callData," + + "uint256 value" + + ")[]," + + "address fallbackRecipient" + + ")", + ], + [ + [ + params.actions.map(({ target, callData, value }) => ({ + target, + callData, + value: BigNumber.from(value), + })), + params.fallbackRecipient, + ], + ] + ); +} diff --git a/api/_utils.ts b/api/_utils.ts index 208eab704..0b50770f8 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -65,6 +65,7 @@ import { InvalidParamError, RouteNotEnabledError, } from "./_errors"; +import { Token } from "./_dexes/types"; export { InputError, handleErrorCondition } from "./_errors"; @@ -806,6 +807,124 @@ export const getCachedLimits = async ( ).data; }; +export async function getSuggestedFees(params: { + inputToken: string; + outputToken: string; + originChainId: number; + destinationChainId: number; + amount: string; + skipAmountLimit?: boolean; + message?: string; + depositMethod?: string; + recipient?: string; +}): Promise<{ + estimatedFillTimeSec: number; + timestamp: number; + isAmountTooLow: boolean; + quoteBlock: string; + exclusiveRelayer: string; + exclusivityDeadline: number; + spokePoolAddress: string; + destinationSpokePoolAddress: string; + totalRelayFee: { + pct: string; + total: string; + }; + relayerCapitalFee: { + pct: string; + total: string; + }; + relayerGasFee: { + pct: string; + total: string; + }; + lpFee: { + pct: string; + total: string; + }; + limits: { + minDeposit: string; + maxDeposit: string; + maxDepositInstant: string; + maxDepositShortDelay: string; + recommendedDepositInstant: string; + }; +}> { + return ( + await axios(`${resolveVercelEndpoint()}/api/suggested-fees`, { + params, + }) + ).data; +} + +export async function getBridgeQuoteForMinOutput(params: { + inputToken: Token; + outputToken: Token; + minOutputAmount: BigNumber; + recipient?: string; + message?: string; +}) { + const baseParams = { + inputToken: params.inputToken.address, + outputToken: params.outputToken.address, + originChainId: params.inputToken.chainId, + destinationChainId: params.outputToken.chainId, + skipAmountLimit: false, + recipient: params.recipient, + message: params.message, + }; + + // 1. Use the suggested fees to get an indicative quote with + // input amount equal to minOutputAmount + let tries = 0; + let adjustedInputAmount = params.minOutputAmount; + let indicativeQuote = await getSuggestedFees({ + ...baseParams, + amount: adjustedInputAmount.toString(), + }); + let adjustmentPct = indicativeQuote.totalRelayFee.pct; + let finalQuote: Awaited> | undefined = + undefined; + + // 2. Adjust input amount to meet minOutputAmount + while (tries < 3) { + adjustedInputAmount = adjustedInputAmount + .mul(utils.parseEther("1").add(adjustmentPct)) + .div(sdk.utils.fixedPointAdjustment); + const adjustedQuote = await getSuggestedFees({ + ...baseParams, + amount: adjustedInputAmount.toString(), + }); + const outputAmount = adjustedInputAmount.sub( + adjustedInputAmount + .mul(adjustedQuote.totalRelayFee.pct) + .div(sdk.utils.fixedPointAdjustment) + ); + + if (outputAmount.gte(params.minOutputAmount)) { + finalQuote = adjustedQuote; + break; + } else { + adjustmentPct = adjustedQuote.totalRelayFee.pct; + tries++; + } + } + + if (!finalQuote) { + throw new Error("Failed to adjust input amount to meet minOutputAmount"); + } + + return { + inputAmount: adjustedInputAmount, + outputAmount: adjustedInputAmount.sub(finalQuote.totalRelayFee.total), + minOutputAmount: params.minOutputAmount, + suggestedFees: finalQuote, + message: params.message, + inputToken: params.inputToken, + outputToken: params.outputToken, + }; +} + export const providerCache: Record = {}; /** @@ -942,6 +1061,60 @@ export const isRouteEnabled = ( return !!enabled; }; +export function isInputTokenBridgeable( + inputTokenAddress: string, + originChainId: number +) { + return ENABLED_ROUTES.routes.some( + ({ fromTokenAddress, fromChain }) => + originChainId === fromChain && + inputTokenAddress.toLowerCase() === fromTokenAddress.toLowerCase() + ); +} + +export function isOutputTokenBridgeable( + outputTokenAddress: string, + destinationChainId: number +) { + return ENABLED_ROUTES.routes.some( + ({ toTokenAddress, toChain }) => + destinationChainId === toChain && + toTokenAddress.toLowerCase() === outputTokenAddress.toLowerCase() + ); +} + +export function getRouteByInputTokenAndDestinationChain( + inputTokenAddress: string, + destinationChainId: number +) { + return ENABLED_ROUTES.routes.find( + ({ fromTokenAddress, toChain }) => + destinationChainId === toChain && + fromTokenAddress.toLowerCase() === inputTokenAddress.toLowerCase() + ); +} + +export function getRouteByOutputTokenAndOriginChain( + outputTokenAddress: string, + originChainId: number +) { + return ENABLED_ROUTES.routes.find( + ({ toTokenAddress, fromChain }) => + originChainId === fromChain && + outputTokenAddress.toLowerCase() === toTokenAddress.toLowerCase() + ); +} + +export function getRoutesByChainIds( + originChainId: number, + destinationChainId: number +) { + return ENABLED_ROUTES.routes.filter( + ({ toChain, fromChain }) => + originChainId === fromChain && destinationChainId === toChain + ); +} + /** * Resolves the balance of a given ERC20 token at a provided address. If no token is provided, the balance of the * native currency will be returned. diff --git a/api/swap-quote.ts b/api/swap-quote.ts index 5b9e04a71..56c54d472 100644 --- a/api/swap-quote.ts +++ b/api/swap-quote.ts @@ -16,6 +16,7 @@ import { import { getUniswapQuoteForOriginSwapExactInput } from "./_dexes/uniswap"; import { get1inchQuoteForOriginSwapExactInput } from "./_dexes/1inch"; import { InvalidParamError } from "./_errors"; +import { AMOUNT_TYPE } from "./_dexes/cross-swap"; const SwapQuoteQueryParamsSchema = type({ swapToken: validAddress(), @@ -105,7 +106,7 @@ const handler = async ( }, amount: swapTokenAmount, slippageTolerance: parseFloat(swapSlippage), - type: "EXACT_INPUT", + type: AMOUNT_TYPE.EXACT_INPUT, } as const; const quoteResults = await Promise.allSettled([ diff --git a/api/swap.ts b/api/swap.ts index 7112a138d..2dc0cc8a1 100644 --- a/api/swap.ts +++ b/api/swap.ts @@ -1,5 +1,6 @@ import { VercelResponse } from "@vercel/node"; import { assert, Infer, type, string, optional } from "superstruct"; +import { BigNumber } from "ethers"; import { TypedVercelRequest } from "./_types"; import { @@ -9,10 +10,15 @@ import { positiveIntStr, validAddress, boolStr, + getTokenByAddress, } from "./_utils"; +import { AMOUNT_TYPE, getCrossSwapQuotes } from "./_dexes/cross-swap"; +import { Token } from "./_dexes/types"; +import { InvalidParamError, MissingParamError } from "./_errors"; const SwapQueryParamsSchema = type({ - minOutputAmount: positiveIntStr(), + minOutputAmount: optional(positiveIntStr()), + exactInputAmount: optional(positiveIntStr()), inputToken: validAddress(), outputToken: validAddress(), originChainId: positiveIntStr(), @@ -39,36 +45,109 @@ const handler = async ( try { assert(query, SwapQueryParamsSchema); - let { - inputToken, - outputToken, - minOutputAmount, - originChainId, - destinationChainId, + const { + inputToken: _inputTokenAddress, + outputToken: _outputTokenAddress, + exactInputAmount: _exactInputAmount, + minOutputAmount: _minOutputAmount, + originChainId: _originChainId, + destinationChainId: _destinationChainId, recipient, integratorId, refundAddress, - refundOnOrigin, + refundOnOrigin: _refundOnOrigin = "true", slippageTolerance = "0.5", // Default to 0.5% slippage } = query; + const originChainId = Number(_originChainId); + const destinationChainId = Number(_destinationChainId); + const refundOnOrigin = _refundOnOrigin === "true"; + + if (!_minOutputAmount && !_exactInputAmount) { + throw new MissingParamError({ + param: "minOutputAmount, exactInputAmount", + message: "One of 'minOutputAmount' or 'exactInputAmount' is required", + }); + } + + if (_minOutputAmount && _exactInputAmount) { + throw new InvalidParamError({ + param: "minOutputAmount, exactInputAmount", + message: + "Only one of 'minOutputAmount' or 'exactInputAmount' is allowed", + }); + } + + const amountType = _minOutputAmount + ? AMOUNT_TYPE.MIN_OUTPUT + : AMOUNT_TYPE.EXACT_INPUT; + const amount = BigNumber.from( + amountType === AMOUNT_TYPE.EXACT_INPUT + ? _exactInputAmount + : _minOutputAmount + ); + // 1. Get auxiliary data - // - Swap type: major to major (same), major to major (different), major to any, any to major, any to any // - Token details // - Token prices + const knownInputToken = getTokenByAddress( + _inputTokenAddress, + originChainId + ); + const inputToken: Token = knownInputToken + ? { + address: knownInputToken.addresses[originChainId]!, + decimals: knownInputToken.decimals, + symbol: knownInputToken.symbol, + chainId: originChainId, + } + : // @FIXME: fetch dynamic token details. using hardcoded values for now + { + address: _inputTokenAddress, + decimals: 18, + symbol: "UNKNOWN", + chainId: originChainId, + }; + const knownOutputToken = getTokenByAddress( + _outputTokenAddress, + destinationChainId + ); + const outputToken: Token = knownOutputToken + ? { + address: knownOutputToken.addresses[destinationChainId]!, + decimals: knownOutputToken.decimals, + symbol: knownOutputToken.symbol, + chainId: destinationChainId, + } + : // @FIXME: fetch dynamic token details. using hardcoded values for now + { + address: _outputTokenAddress, + decimals: 18, + symbol: "UNKNOWN", + chainId: destinationChainId, + }; // 2. Get swap quotes and calldata based on the swap type + const crossSwapQuotes = await getCrossSwapQuotes({ + amount, + inputToken, + outputToken, + recipient, + slippageTolerance: Number(slippageTolerance), + type: amountType, + refundOnOrigin, + refundAddress, + }); - // 3. Get suggested fees with message and input amount - - // 4. Build tx and return + // 3. Build tx and return + // @TODO logger.debug({ at: "Swap", message: "Response data", - responseJson: {}, + responseJson: crossSwapQuotes, }); - response.status(200).json({}); + response.status(200).json(crossSwapQuotes); } catch (error: unknown) { return handleErrorCondition("swap", response, logger, error); } diff --git a/scripts/tests/swap.ts b/scripts/tests/swap.ts new file mode 100644 index 000000000..c2d185abb --- /dev/null +++ b/scripts/tests/swap.ts @@ -0,0 +1,64 @@ +import axios from "axios"; +import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants"; +import { ethers } from "ethers"; + +/** + * Manual test script for the swap API. Should be converted to a proper test suite. + */ + +const depositor = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D"; +const MIN_OUTPUT_CASES = [ + // B2B + { + minOutputAmount: ethers.utils.parseUnits("100", 6).toString(), + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], + originChainId: CHAIN_IDs.BASE, + outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], + destinationChainId: CHAIN_IDs.ARBITRUM, + recipient: depositor, + integratorId: "test", + }, + // B2A + { + minOutputAmount: ethers.utils.parseUnits("0.001", 18).toString(), + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], + originChainId: CHAIN_IDs.BASE, + outputToken: TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.ARBITRUM], + destinationChainId: CHAIN_IDs.ARBITRUM, + recipient: depositor, + integratorId: "test", + }, + // A2B + { + minOutputAmount: ethers.utils.parseUnits("10", 6).toString(), + inputToken: TOKEN_SYMBOLS_MAP.USDbC.addresses[CHAIN_IDs.BASE], + originChainId: CHAIN_IDs.BASE, + outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], + destinationChainId: CHAIN_IDs.ARBITRUM, + recipient: depositor, + integratorId: "test", + }, + // A2A + { + minOutputAmount: ethers.utils.parseUnits("1", 18).toString(), + inputToken: TOKEN_SYMBOLS_MAP.USDbC.addresses[CHAIN_IDs.BASE], + originChainId: CHAIN_IDs.BASE, + outputToken: "0x74885b4D524d497261259B38900f54e6dbAd2210", // APE Coin + destinationChainId: CHAIN_IDs.ARBITRUM, + recipient: depositor, + integratorId: "test", + }, +]; + +async function swap() { + for (const testCase of MIN_OUTPUT_CASES) { + const response = await axios.get(`http://localhost:3000/api/swap`, { + params: testCase, + }); + console.log(response.data); + } +} + +swap() + .then(() => console.log("Done")) + .catch(console.error); From 19c03896f69c109c4ba4da55c4d3474bd23f5f99 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp <51655063+gsteenkamp89@users.noreply.github.com> Date: Fri, 15 Nov 2024 12:24:06 +0200 Subject: [PATCH 04/47] get token details for any address in any chainId (#1272) * resolve arbitrary token details * fixup * use wrapped addresses to resolve native tokens * create error class, refactor using multicall * Update api/_utils.ts Co-authored-by: Dong-Ha Kim * validate inputs, add cause to error --------- Co-authored-by: Dong-Ha Kim --- api/_errors.ts | 9 +++++ api/_utils.ts | 106 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/api/_errors.ts b/api/_errors.ts index b48ac2651..93db80b38 100644 --- a/api/_errors.ts +++ b/api/_errors.ts @@ -74,6 +74,15 @@ export class AcrossApiError extends Error { } } +export class TokenNotFoundError extends AcrossApiError { + constructor(args: { address: string; chainId: number; opts?: ErrorOptions }) { + super({ + message: `Unable to find tokenDetails for address: ${args.address}, on chain with id: ${args.chainId}`, + status: HttpErrorToStatusCode.NOT_FOUND, + }); + } +} + export class UnauthorizedError extends AcrossApiError { constructor(args?: { message: string }, opts?: ErrorOptions) { super( diff --git a/api/_utils.ts b/api/_utils.ts index 0b50770f8..a5c216e15 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -54,7 +54,7 @@ import { maxRelayFeePct, relayerFeeCapitalCostConfig, } from "./_constants"; -import { PoolStateOfUser, PoolStateResult } from "./_types"; +import { PoolStateOfUser, PoolStateResult, TokenInfo } from "./_types"; import { buildInternalCacheKey, getCachedValue, @@ -64,6 +64,7 @@ import { MissingParamError, InvalidParamError, RouteNotEnabledError, + TokenNotFoundError, } from "./_errors"; import { Token } from "./_dexes/types"; @@ -2165,3 +2166,106 @@ export function paramToArray( if (!param) return; return Array.isArray(param) ? param : [param]; } + +export type TokenOptions = { + chainId: number; + address: string; +}; + +const TTL_TOKEN_INFO = 30 * 24 * 60 * 60; // 30 days + +function tokenInfoCache(params: TokenOptions) { + return makeCacheGetterAndSetter( + buildInternalCacheKey("tokenInfo", params.address, params.chainId), + TTL_TOKEN_INFO, + () => getTokenInfo(params), + (tokenDetails) => tokenDetails + ); +} + +export async function getCachedTokenInfo(params: TokenOptions) { + return tokenInfoCache(params).get(); +} + +// find decimals and symbol for any token address on any chain we support +export async function getTokenInfo({ + chainId, + address, +}: TokenOptions): Promise< + Pick +> { + try { + if (!ethers.utils.isAddress(address)) { + throw new InvalidParamError({ + param: "address", + message: '"Address" must be a valid ethereum address', + }); + } + + if (!Number.isSafeInteger(chainId) || chainId < 0) { + throw new InvalidParamError({ + param: "chainId", + message: '"chainId" must be a positive integer', + }); + } + + // ERC20 resolved statically + const token = Object.values(TOKEN_SYMBOLS_MAP).find((token) => + Boolean( + token.addresses?.[chainId]?.toLowerCase() === address.toLowerCase() + ) + ); + + if (token) { + return { + decimals: token.decimals, + symbol: token.symbol, + address: token.addresses[chainId], + name: token.name, + }; + } + + // ERC20 resolved dynamically + const provider = getProvider(chainId); + + const erc20 = ERC20__factory.connect( + ethers.utils.getAddress(address), + provider + ); + + const calls = [ + { + contract: erc20, + functionName: "decimals", + }, + { + contract: erc20, + functionName: "symbol", + }, + { + contract: erc20, + functionName: "name", + }, + ]; + + const [[decimals], [symbol], [name]] = await callViaMulticall3( + provider, + calls + ); + + return { + address, + decimals, + symbol, + name, + }; + } catch (error) { + throw new TokenNotFoundError({ + chainId, + address, + opts: { + cause: error, + }, + }); + } +} From b24582780e270fbd7d8281647e2978592e526eee Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Wed, 20 Nov 2024 20:10:23 +0700 Subject: [PATCH 05/47] feat: build cross swap tx (#1274) * feat: build cross swap tx * fixup * fixup * refactor * fix * fixup * fixup * feat: add leftover handling type * fixup * fixup * review requests --- api/_dexes/cross-swap.ts | 105 ++++++++++- api/_dexes/types.ts | 5 +- api/_dexes/uniswap.ts | 370 ++++++++++++++++++++++++--------------- api/_integrator-id.ts | 29 +++ api/_utils.ts | 129 +++++++++----- api/swap.ts | 98 +++++------ scripts/tests/swap.ts | 51 ++++-- 7 files changed, 533 insertions(+), 254 deletions(-) create mode 100644 api/_integrator-id.ts diff --git a/api/_dexes/cross-swap.ts b/api/_dexes/cross-swap.ts index c435a20c3..8599baa3d 100644 --- a/api/_dexes/cross-swap.ts +++ b/api/_dexes/cross-swap.ts @@ -1,8 +1,12 @@ +import { SpokePool } from "@across-protocol/contracts/dist/typechain"; + import { isRouteEnabled, isInputTokenBridgeable, isOutputTokenBridgeable, getBridgeQuoteForMinOutput, + getSpokePool, + latestGasPriceCache, } from "../_utils"; import { getUniswapCrossSwapQuotesForMinOutputB2A, @@ -10,12 +14,18 @@ import { getBestUniswapCrossSwapQuotesForMinOutputA2A, } from "./uniswap"; import { CrossSwap, CrossSwapQuotes } from "./types"; +import { getSwapAndBridge } from "./utils"; +import { tagIntegratorId } from "../_integrator-id"; +import { PopulatedTransaction } from "ethers"; +import { getMultiCallHandlerAddress } from "../_multicall-handler"; export type CrossSwapType = (typeof CROSS_SWAP_TYPE)[keyof typeof CROSS_SWAP_TYPE]; export type AmountType = (typeof AMOUNT_TYPE)[keyof typeof AMOUNT_TYPE]; +export type LeftoverType = (typeof LEFTOVER_TYPE)[keyof typeof LEFTOVER_TYPE]; + export const AMOUNT_TYPE = { EXACT_INPUT: "exactInput", MIN_OUTPUT: "minOutput", @@ -28,6 +38,11 @@ export const CROSS_SWAP_TYPE = { ANY_TO_ANY: "anyToAny", } as const; +export const LEFTOVER_TYPE = { + OUTPUT_TOKEN: "outputToken", + BRIDGEABLE_TOKEN: "bridgeableToken", +} as const; + export const PREFERRED_BRIDGE_TOKENS = ["WETH", "USDC"]; export async function getCrossSwapQuotes( @@ -139,4 +154,92 @@ export function getCrossSwapType(params: { return CROSS_SWAP_TYPE.ANY_TO_ANY; } -export function calcFees() {} +export async function buildCrossSwapTx( + crossSwapQuotes: CrossSwapQuotes, + integratorId?: string +) { + const originChainId = crossSwapQuotes.crossSwap.inputToken.chainId; + const destinationChainId = crossSwapQuotes.crossSwap.outputToken.chainId; + const spokePool = getSpokePool(originChainId); + const deposit = { + depositor: crossSwapQuotes.crossSwap.depositor, + recipient: crossSwapQuotes.destinationSwapQuote + ? getMultiCallHandlerAddress(destinationChainId) + : crossSwapQuotes.crossSwap.recipient, + inputToken: crossSwapQuotes.bridgeQuote.inputToken.address, + outputToken: crossSwapQuotes.bridgeQuote.outputToken.address, + inputAmount: crossSwapQuotes.bridgeQuote.inputAmount, + outputAmount: crossSwapQuotes.bridgeQuote.outputAmount, + destinationChainid: crossSwapQuotes.bridgeQuote.outputToken.chainId, + exclusiveRelayer: + crossSwapQuotes.bridgeQuote.suggestedFees.exclusiveRelayer, + quoteTimestamp: crossSwapQuotes.bridgeQuote.suggestedFees.timestamp, + fillDeadline: await getFillDeadline(spokePool), + exclusivityDeadline: + crossSwapQuotes.bridgeQuote.suggestedFees.exclusivityDeadline, + message: crossSwapQuotes.bridgeQuote.message || "0x", + }; + + let tx: PopulatedTransaction; + let toAddress: string; + + if (crossSwapQuotes.originSwapQuote) { + const swapAndBridge = getSwapAndBridge("uniswap", originChainId); + tx = await swapAndBridge.populateTransaction.swapAndBridge( + crossSwapQuotes.originSwapQuote.tokenIn.address, + crossSwapQuotes.originSwapQuote.tokenOut.address, + crossSwapQuotes.originSwapQuote.swapTx.data, + crossSwapQuotes.originSwapQuote.maximumAmountIn, + crossSwapQuotes.originSwapQuote.minAmountOut, + deposit + ); + toAddress = swapAndBridge.address; + } else { + const spokePool = getSpokePool( + crossSwapQuotes.crossSwap.inputToken.chainId + ); + tx = await spokePool.populateTransaction.depositV3( + deposit.depositor, + deposit.recipient, + deposit.inputToken, + deposit.outputToken, + deposit.inputAmount, + deposit.outputAmount, + deposit.destinationChainid, + deposit.exclusiveRelayer, + deposit.quoteTimestamp, + deposit.fillDeadline, + deposit.exclusivityDeadline, + deposit.message + ); + toAddress = spokePool.address; + } + + const [gas, gasPrice] = await Promise.all([ + spokePool.provider.estimateGas({ + from: crossSwapQuotes.crossSwap.depositor, + ...tx, + }), + latestGasPriceCache(originChainId).get(), + ]); + + return { + from: crossSwapQuotes.crossSwap.depositor, + to: toAddress, + data: integratorId ? tagIntegratorId(integratorId, tx.data!) : tx.data, + gas, + gasPrice, + value: tx.value, + }; +} + +async function getFillDeadline(spokePool: SpokePool): Promise { + const calls = [ + spokePool.interface.encodeFunctionData("getCurrentTime"), + spokePool.interface.encodeFunctionData("fillDeadlineBuffer"), + ]; + + const [currentTime, fillDeadlineBuffer] = + await spokePool.callStatic.multicall(calls); + return Number(currentTime) + Number(fillDeadlineBuffer); +} diff --git a/api/_dexes/types.ts b/api/_dexes/types.ts index da5e448c8..6e00f04ce 100644 --- a/api/_dexes/types.ts +++ b/api/_dexes/types.ts @@ -1,6 +1,6 @@ import { BigNumber } from "ethers"; import { getSuggestedFees } from "../_utils"; -import { AmountType, CrossSwapType } from "./cross-swap"; +import { AmountType, CrossSwapType, LeftoverType } from "./cross-swap"; export type { AmountType, CrossSwapType }; @@ -19,15 +19,18 @@ export type Swap = { recipient: string; slippageTolerance: number; type: AmountType; + leftoverType?: LeftoverType; }; export type CrossSwap = { amount: BigNumber; inputToken: Token; outputToken: Token; + depositor: string; recipient: string; slippageTolerance: number; type: AmountType; + leftoverType?: LeftoverType; refundOnOrigin: boolean; refundAddress?: string; }; diff --git a/api/_dexes/uniswap.ts b/api/_dexes/uniswap.ts index cf2fc2c53..b658edfc8 100644 --- a/api/_dexes/uniswap.ts +++ b/api/_dexes/uniswap.ts @@ -27,10 +27,9 @@ import { Swap, CrossSwap, SwapQuote, - CrossSwapQuotes, } from "./types"; import { getSwapAndBridgeAddress, NoSwapRouteError } from "./utils"; -import { AMOUNT_TYPE } from "./cross-swap"; +import { LEFTOVER_TYPE } from "./cross-swap"; // Taken from here: https://docs.uniswap.org/contracts/v3/reference/deployments/ export const SWAP_ROUTER_02_ADDRESS = { @@ -45,9 +44,9 @@ export const SWAP_ROUTER_02_ADDRESS = { [CHAIN_IDs.ZORA]: "0x7De04c96BE5159c3b5CeffC82aa176dc81281557", }; -// Maps testnet chain IDs to their main counterparts. Used to get the mainnet token +// Maps testnet chain IDs to their prod counterparts. Used to get the prod token // info for testnet tokens. -const TESTNET_TO_MAINNET = { +const TESTNET_TO_PROD = { [CHAIN_IDs.SEPOLIA]: CHAIN_IDs.MAINNET, [CHAIN_IDs.BASE_SEPOLIA]: CHAIN_IDs.BASE, [CHAIN_IDs.OPTIMISM_SEPOLIA]: CHAIN_IDs.OPTIMISM, @@ -67,13 +66,16 @@ export async function getUniswapQuoteForOriginSwapExactInput( const initialTokenOut = { ...swap.tokenOut }; // Always use mainnet tokens for retrieving quote, so that we can get equivalent quotes // for testnet tokens. - swap.tokenIn = getMainnetToken(swap.tokenIn); - swap.tokenOut = getMainnetToken(swap.tokenOut); + swap.tokenIn = getProdToken(swap.tokenIn); + swap.tokenOut = getProdToken(swap.tokenOut); - const { swapTx, minAmountOut } = await getUniswapQuote({ - ...swap, - recipient: swapAndBridgeAddress, - }); + const { swapTx, minAmountOut } = await getUniswapQuote( + { + ...swap, + recipient: swapAndBridgeAddress, + }, + TradeType.EXACT_INPUT + ); // replace mainnet token addresses with initial token addresses in calldata swapTx.data = swapTx.data.replace( @@ -135,16 +137,37 @@ export async function getUniswapCrossSwapQuotesForMinOutputB2A( chainId: destinationSwapChainId, }; - // 1. Get destination swap quote for bridgeable output token -> any token - const destinationSwapQuote = await getUniswapQuote({ + const destinationSwap = { chainId: destinationSwapChainId, tokenIn: bridgeableOutputToken, tokenOut: crossSwap.outputToken, - amount: crossSwap.amount.toString(), recipient: crossSwap.recipient, slippageTolerance: crossSwap.slippageTolerance, - type: AMOUNT_TYPE.MIN_OUTPUT, - }); + }; + // 1.1. Get destination swap quote for bridgeable output token -> any token + // with exact output amount. + let destinationSwapQuote = await getUniswapQuote( + { + ...destinationSwap, + amount: crossSwap.amount.toString(), + }, + TradeType.EXACT_OUTPUT + ); + // 1.2. Re-fetch destination swap quote with exact input amount if leftover tokens + // should be sent as output tokens instead of bridgeable output tokens. + if (crossSwap.leftoverType === LEFTOVER_TYPE.OUTPUT_TOKEN) { + destinationSwapQuote = await getUniswapQuote( + { + ...destinationSwap, + amount: addSlippageToAmount( + destinationSwapQuote.maximumAmountIn, + crossSwap.slippageTolerance.toString() + ), + }, + TradeType.EXACT_INPUT + ); + assertMinOutputAmount(destinationSwapQuote.minAmountOut, crossSwap.amount); + } // 2. Get bridge quote for bridgeable input token -> bridgeable output token const bridgeQuote = await getBridgeQuoteForMinOutput({ @@ -152,48 +175,17 @@ export async function getUniswapCrossSwapQuotesForMinOutputB2A( outputToken: bridgeableOutputToken, minOutputAmount: destinationSwapQuote.maximumAmountIn, recipient: getMultiCallHandlerAddress(destinationSwapChainId), - message: buildMulticallHandlerMessage({ - // @TODO: handle fallback recipient for params `refundOnOrigin` and `refundAddress` - fallbackRecipient: crossSwap.recipient, - actions: [ - { - target: destinationSwapQuote.swapTx.to, - callData: destinationSwapQuote.swapTx.data, - value: destinationSwapQuote.swapTx.value, - }, - ], + message: buildDestinationSwapCrossChainMessage({ + crossSwap, + destinationSwapQuote, + bridgeableOutputToken, }), }); - // 3. Re-fetch destination swap quote with updated input amount and EXACT_INPUT type. - // This prevents leftover tokens in the MulticallHandler contract. - const updatedDestinationSwapQuote = await getUniswapQuote({ - chainId: destinationSwapChainId, - tokenIn: bridgeableOutputToken, - tokenOut: crossSwap.outputToken, - amount: bridgeQuote.outputAmount.toString(), - recipient: crossSwap.recipient, - slippageTolerance: crossSwap.slippageTolerance, - type: AMOUNT_TYPE.EXACT_INPUT, - }); - - // 4. Rebuild message - bridgeQuote.message = buildMulticallHandlerMessage({ - // @TODO: handle fallback recipient for params `refundOnOrigin` and `refundAddress` - fallbackRecipient: crossSwap.recipient, - actions: [ - { - target: updatedDestinationSwapQuote.swapTx.to, - callData: updatedDestinationSwapQuote.swapTx.data, - value: updatedDestinationSwapQuote.swapTx.value, - }, - ], - }); - return { crossSwap, bridgeQuote, - destinationSwapQuote: updatedDestinationSwapQuote, + destinationSwapQuote, originSwapQuote: undefined, }; } @@ -246,22 +238,43 @@ export async function getUniswapCrossSwapQuotesForMinOutputA2B( // @TODO: handle ETH/WETH message generation }); - // 2. Get origin swap quote for any input token -> bridgeable input token - const originSwapQuote = await getUniswapQuote({ + const originSwap = { chainId: originSwapChainId, tokenIn: crossSwap.inputToken, tokenOut: bridgeableInputToken, - amount: bridgeQuote.inputAmount.toString(), recipient: getSwapAndBridgeAddress("uniswap", originSwapChainId), slippageTolerance: crossSwap.slippageTolerance, - type: AMOUNT_TYPE.MIN_OUTPUT, - }); + }; + // 2.1. Get origin swap quote for any input token -> bridgeable input token + const originSwapQuote = await getUniswapQuote( + { + ...originSwap, + amount: bridgeQuote.inputAmount.toString(), + }, + TradeType.EXACT_OUTPUT + ); + // 2.2. Re-fetch origin swap quote with updated input amount and EXACT_INPUT type. + // This prevents leftover tokens in the SwapAndBridge contract. + const adjOriginSwapQuote = await getUniswapQuote( + { + ...originSwap, + amount: originSwapQuote.maximumAmountIn.toString(), + }, + TradeType.EXACT_INPUT + ); + + if (adjOriginSwapQuote.minAmountOut.lt(bridgeQuote.inputAmount)) { + throw new Error( + `Origin swap quote min. output amount ${adjOriginSwapQuote.minAmountOut.toString()} ` + + `is less than required bridge input amount ${bridgeQuote.inputAmount.toString()}` + ); + } return { crossSwap, bridgeQuote, destinationSwapQuote: undefined, - originSwapQuote, + originSwapQuote: adjOriginSwapQuote, }; } @@ -293,8 +306,8 @@ export async function getBestUniswapCrossSwapQuotesForMinOutputA2A( ); } - const preferredBridgeRoutes = allBridgeRoutes.filter(({ fromTokenSymbol }) => - opts.preferredBridgeTokens.includes(fromTokenSymbol) + const preferredBridgeRoutes = allBridgeRoutes.filter(({ toTokenSymbol }) => + opts.preferredBridgeTokens.includes(toTokenSymbol) ); const bridgeRoutesToCompare = ( preferredBridgeRoutes.length > 0 ? preferredBridgeRoutes : allBridgeRoutes @@ -306,21 +319,11 @@ export async function getBestUniswapCrossSwapQuotesForMinOutputA2A( ); } - const crossSwapQuotesSettledResults = await Promise.allSettled( + const crossSwapQuotes = await Promise.all( bridgeRoutesToCompare.map((bridgeRoute) => getUniswapCrossSwapQuotesForMinOutputA2A(crossSwap, bridgeRoute) ) ); - const crossSwapQuotes = crossSwapQuotesSettledResults - .filter((res) => res.status === "fulfilled") - .map((res) => (res as PromiseFulfilledResult).value); - - if (crossSwapQuotes.length === 0) { - console.log("crossSwapQuotesSettledResults", crossSwapQuotesSettledResults); - throw new Error( - `No successful bridge quotes found for ${originSwapChainId} -> ${destinationSwapChainId}` - ); - } // Compare quotes by lowest input amount const bestCrossSwapQuote = crossSwapQuotes.reduce((prev, curr) => @@ -384,88 +387,95 @@ export async function getUniswapCrossSwapQuotesForMinOutputA2A( symbol: _bridgeableOutputToken.symbol, chainId: bridgeRoute.toChain, }; - - // 1. Get destination swap quote for bridgeable output token -> any token - const destinationSwapQuote = await getUniswapQuote({ + const originSwap = { + chainId: originSwapChainId, + tokenIn: crossSwap.inputToken, + tokenOut: bridgeableInputToken, + recipient: getSwapAndBridgeAddress("uniswap", originSwapChainId), + slippageTolerance: crossSwap.slippageTolerance, + }; + const destinationSwap = { chainId: destinationSwapChainId, tokenIn: bridgeableOutputToken, tokenOut: crossSwap.outputToken, - amount: crossSwap.amount.toString(), recipient: crossSwap.recipient, slippageTolerance: crossSwap.slippageTolerance, - type: AMOUNT_TYPE.MIN_OUTPUT, - }); + }; + + // 1.1. Get destination swap quote for bridgeable output token -> any token + // with exact output amount + let destinationSwapQuote = await getUniswapQuote( + { + ...destinationSwap, + amount: crossSwap.amount.toString(), + }, + TradeType.EXACT_OUTPUT + ); + // 1.2. Re-fetch destination swap quote with exact input amount if leftover tokens + // should be sent as output tokens instead of bridgeable output tokens. + if (crossSwap.leftoverType === LEFTOVER_TYPE.OUTPUT_TOKEN) { + destinationSwapQuote = await getUniswapQuote( + { + ...destinationSwap, + amount: addSlippageToAmount( + destinationSwapQuote.maximumAmountIn, + crossSwap.slippageTolerance.toString() + ), + }, + TradeType.EXACT_INPUT + ); + assertMinOutputAmount(destinationSwapQuote.minAmountOut, crossSwap.amount); + } // 2. Get bridge quote for bridgeable input token -> bridgeable output token const bridgeQuote = await getBridgeQuoteForMinOutput({ inputToken: bridgeableInputToken, outputToken: bridgeableOutputToken, - minOutputAmount: destinationSwapQuote.maximumAmountIn, + minOutputAmount: destinationSwapQuote.expectedAmountIn, recipient: getMultiCallHandlerAddress(destinationSwapChainId), - message: buildMulticallHandlerMessage({ - // @TODO: handle fallback recipient for params `refundOnOrigin` and `refundAddress` - fallbackRecipient: crossSwap.recipient, - actions: [ - { - target: destinationSwapQuote.swapTx.to, - callData: destinationSwapQuote.swapTx.data, - value: destinationSwapQuote.swapTx.value, - }, - ], + message: buildDestinationSwapCrossChainMessage({ + crossSwap, + destinationSwapQuote, + bridgeableOutputToken, }), }); - // 3. Re-fetch destination swap quote with updated input amount and EXACT_INPUT type. - // This prevents leftover tokens in the MulticallHandler contract. - const updatedDestinationSwapQuote = await getUniswapQuote({ - chainId: destinationSwapChainId, - tokenIn: bridgeableOutputToken, - tokenOut: crossSwap.outputToken, - amount: bridgeQuote.outputAmount.toString(), - recipient: crossSwap.recipient, - slippageTolerance: crossSwap.slippageTolerance, - type: AMOUNT_TYPE.EXACT_INPUT, - }); - - // 4. Rebuild message - bridgeQuote.message = buildMulticallHandlerMessage({ - // @TODO: handle fallback recipient for params `refundOnOrigin` and `refundAddress` - fallbackRecipient: crossSwap.recipient, - actions: [ - { - target: updatedDestinationSwapQuote.swapTx.to, - callData: updatedDestinationSwapQuote.swapTx.data, - value: updatedDestinationSwapQuote.swapTx.value, - }, - ], - }); - - // 3. Get origin swap quote for any input token -> bridgeable input token - const originSwapQuote = await getUniswapQuote({ - chainId: originSwapChainId, - tokenIn: crossSwap.inputToken, - tokenOut: bridgeableInputToken, - amount: bridgeQuote.inputAmount.toString(), - recipient: getSwapAndBridgeAddress("uniswap", originSwapChainId), - slippageTolerance: crossSwap.slippageTolerance, - type: AMOUNT_TYPE.MIN_OUTPUT, - }); + // 3.1. Get origin swap quote for any input token -> bridgeable input token + const originSwapQuote = await getUniswapQuote( + { + ...originSwap, + amount: bridgeQuote.inputAmount.toString(), + }, + TradeType.EXACT_OUTPUT + ); + // 3.2. Re-fetch origin swap quote with updated input amount and EXACT_INPUT type. + // This prevents leftover tokens in the SwapAndBridge contract. + const adjOriginSwapQuote = await getUniswapQuote( + { + ...originSwap, + amount: originSwapQuote.maximumAmountIn.toString(), + }, + TradeType.EXACT_INPUT + ); return { crossSwap, - destinationSwapQuote: updatedDestinationSwapQuote, + destinationSwapQuote, bridgeQuote, - originSwapQuote, + originSwapQuote: adjOriginSwapQuote, }; } -export async function getUniswapQuote(swap: Swap): Promise { +export async function getUniswapQuote( + swap: Omit, + tradeType: TradeType +): Promise { const { router, options } = getSwapRouterAndOptions(swap); const amountCurrency = - swap.type === AMOUNT_TYPE.EXACT_INPUT ? swap.tokenIn : swap.tokenOut; + tradeType === TradeType.EXACT_INPUT ? swap.tokenIn : swap.tokenOut; const quoteCurrency = - swap.type === AMOUNT_TYPE.EXACT_INPUT ? swap.tokenOut : swap.tokenIn; + tradeType === TradeType.EXACT_INPUT ? swap.tokenOut : swap.tokenIn; const route = await router.route( CurrencyAmount.fromRawAmount( @@ -481,9 +491,7 @@ export async function getUniswapQuote(swap: Swap): Promise { quoteCurrency.address, quoteCurrency.decimals ), - swap.type === AMOUNT_TYPE.EXACT_INPUT - ? TradeType.EXACT_INPUT - : TradeType.EXACT_OUTPUT, + tradeType, options ); @@ -493,7 +501,8 @@ export async function getUniswapQuote(swap: Swap): Promise { tokenInSymbol: swap.tokenIn.symbol, tokenOutSymbol: swap.tokenOut.symbol, chainId: swap.chainId, - swapType: swap.type, + swapType: + tradeType === TradeType.EXACT_INPUT ? "EXACT_INPUT" : "EXACT_OUTPUT", }); } @@ -525,7 +534,7 @@ export async function getUniswapQuote(swap: Swap): Promise { }; console.log("swapQuote", { - type: swap.type, + type: tradeType === TradeType.EXACT_INPUT ? "EXACT_INPUT" : "EXACT_OUTPUT", tokenIn: swapQuote.tokenIn.symbol, tokenOut: swapQuote.tokenOut.symbol, chainId: swap.chainId, @@ -568,22 +577,101 @@ function floatToPercent(value: number) { ); } -function getMainnetToken(token: AcrossToken) { - const mainnetChainId = TESTNET_TO_MAINNET[token.chainId] || token.chainId; +function getProdToken(token: AcrossToken) { + const prodChainId = TESTNET_TO_PROD[token.chainId] || token.chainId; - const mainnetToken = + const prodToken = TOKEN_SYMBOLS_MAP[token.symbol as keyof typeof TOKEN_SYMBOLS_MAP]; - const mainnetTokenAddress = mainnetToken?.addresses[mainnetChainId]; + const prodTokenAddress = prodToken?.addresses[prodChainId]; - if (!mainnetToken || !mainnetTokenAddress) { + if (!prodToken || !prodTokenAddress) { throw new Error( - `Mainnet token not found for ${token.symbol} on chain ${token.chainId}` + `Prod token not found for ${token.symbol} on chain ${token.chainId}` ); } return { - ...mainnetToken, - chainId: mainnetChainId, - address: mainnetTokenAddress, + ...prodToken, + chainId: prodChainId, + address: prodTokenAddress, }; } + +function buildDestinationSwapCrossChainMessage({ + crossSwap, + destinationSwapQuote, + bridgeableOutputToken, +}: { + crossSwap: CrossSwap; + bridgeableOutputToken: AcrossToken; + destinationSwapQuote: SwapQuote; +}) { + const destinationSwapChainId = destinationSwapQuote.tokenOut.chainId; + return buildMulticallHandlerMessage({ + // @TODO: handle fallback recipient for params `refundOnOrigin` and `refundAddress` + fallbackRecipient: crossSwap.depositor, + actions: [ + // approve bridgeable output token + { + target: bridgeableOutputToken.address, + callData: encodeApproveCalldata( + SWAP_ROUTER_02_ADDRESS[destinationSwapChainId], + destinationSwapQuote.maximumAmountIn + ), + value: "0", + }, + // swap bridgeable output token -> cross swap output token + { + target: destinationSwapQuote.swapTx.to, + callData: destinationSwapQuote.swapTx.data, + value: destinationSwapQuote.swapTx.value, + }, + // drain remaining bridgeable output tokens from MultiCallHandler contract + { + target: getMultiCallHandlerAddress(destinationSwapChainId), + callData: encodeDrainCalldata( + bridgeableOutputToken.address, + crossSwap.depositor + ), + value: "0", + }, + ], + }); +} + +function encodeApproveCalldata(spender: string, value: ethers.BigNumber) { + const approveFunction = "function approve(address spender, uint256 value)"; + const erc20Interface = new ethers.utils.Interface([approveFunction]); + return erc20Interface.encodeFunctionData("approve", [spender, value]); +} + +function encodeDrainCalldata(token: string, destination: string) { + const drainFunction = + "function drainLeftoverTokens(address token, address payable destination)"; + const multicallHandlerInterface = new ethers.utils.Interface([drainFunction]); + return multicallHandlerInterface.encodeFunctionData("drainLeftoverTokens", [ + token, + destination, + ]); +} + +function assertMinOutputAmount( + amountOut: BigNumber, + expectedMinAmountOut: BigNumber +) { + if (amountOut.lt(expectedMinAmountOut)) { + throw new Error( + `Swap quote output amount ${amountOut.toString()} ` + + `is less than required min. output amount ${expectedMinAmountOut.toString()}` + ); + } +} + +function addSlippageToAmount(amount: BigNumber, slippageTolerance: string) { + return amount + .mul( + ethers.utils.parseEther((1 + Number(slippageTolerance) / 100).toString()) + ) + .div(utils.fixedPointAdjustment) + .toString(); +} diff --git a/api/_integrator-id.ts b/api/_integrator-id.ts new file mode 100644 index 000000000..982971eeb --- /dev/null +++ b/api/_integrator-id.ts @@ -0,0 +1,29 @@ +import { utils } from "ethers"; + +export const DOMAIN_CALLDATA_DELIMITER = "0x1dc0de"; + +export function isValidIntegratorId(integratorId: string) { + return ( + utils.isHexString(integratorId) && + // "0x" + 2 bytes = 6 hex characters + integratorId.length === 6 + ); +} + +export function assertValidIntegratorId(integratorId: string) { + if (!isValidIntegratorId(integratorId)) { + throw new Error( + `Invalid integrator ID: ${integratorId}. Needs to be 2 bytes hex string.` + ); + } + + return true; +} + +export function tagIntegratorId(integratorId: string, txData: string) { + assertValidIntegratorId(integratorId); + + return utils.hexlify( + utils.concat([txData, DOMAIN_CALLDATA_DELIMITER, integratorId]) + ); +} diff --git a/api/_utils.ts b/api/_utils.ts index a5c216e15..7b43e9b08 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -14,7 +14,7 @@ import { BalancerNetworkConfig, Multicall3, } from "@balancer-labs/sdk"; -import axios from "axios"; +import axios, { AxiosError } from "axios"; import { BigNumber, BigNumberish, @@ -64,6 +64,9 @@ import { MissingParamError, InvalidParamError, RouteNotEnabledError, + AcrossApiError, + HttpErrorToStatusCode, + AcrossErrorCode, TokenNotFoundError, } from "./_errors"; import { Token } from "./_dexes/types"; @@ -875,55 +878,84 @@ export async function getBridgeQuoteForMinOutput(params: { message: params.message, }; - // 1. Use the suggested fees to get an indicative quote with - // input amount equal to minOutputAmount - let tries = 0; - let adjustedInputAmount = params.minOutputAmount; - let indicativeQuote = await getSuggestedFees({ - ...baseParams, - amount: adjustedInputAmount.toString(), - }); - let adjustmentPct = indicativeQuote.totalRelayFee.pct; - let finalQuote: Awaited> | undefined = - undefined; - - // 2. Adjust input amount to meet minOutputAmount - while (tries < 3) { - adjustedInputAmount = adjustedInputAmount - .mul(utils.parseEther("1").add(adjustmentPct)) - .div(sdk.utils.fixedPointAdjustment); - const adjustedQuote = await getSuggestedFees({ + try { + // 1. Use the suggested fees to get an indicative quote with + // input amount equal to minOutputAmount + let tries = 0; + let adjustedInputAmount = params.minOutputAmount; + let indicativeQuote = await getSuggestedFees({ ...baseParams, amount: adjustedInputAmount.toString(), }); - const outputAmount = adjustedInputAmount.sub( - adjustedInputAmount - .mul(adjustedQuote.totalRelayFee.pct) - .div(sdk.utils.fixedPointAdjustment) - ); + let adjustmentPct = indicativeQuote.totalRelayFee.pct; + let finalQuote: Awaited> | undefined = + undefined; + + // 2. Adjust input amount to meet minOutputAmount + while (tries < 3) { + adjustedInputAmount = adjustedInputAmount + .mul(utils.parseEther("1").add(adjustmentPct)) + .div(sdk.utils.fixedPointAdjustment); + const adjustedQuote = await getSuggestedFees({ + ...baseParams, + amount: adjustedInputAmount.toString(), + }); + const outputAmount = adjustedInputAmount.sub( + adjustedInputAmount + .mul(adjustedQuote.totalRelayFee.pct) + .div(sdk.utils.fixedPointAdjustment) + ); - if (outputAmount.gte(params.minOutputAmount)) { - finalQuote = adjustedQuote; - break; - } else { - adjustmentPct = adjustedQuote.totalRelayFee.pct; - tries++; + if (outputAmount.gte(params.minOutputAmount)) { + finalQuote = adjustedQuote; + break; + } else { + adjustmentPct = adjustedQuote.totalRelayFee.pct; + tries++; + } } - } - if (!finalQuote) { - throw new Error("Failed to adjust input amount to meet minOutputAmount"); - } + if (!finalQuote) { + throw new Error("Failed to adjust input amount to meet minOutputAmount"); + } - return { - inputAmount: adjustedInputAmount, - outputAmount: adjustedInputAmount.sub(finalQuote.totalRelayFee.total), - minOutputAmount: params.minOutputAmount, - suggestedFees: finalQuote, - message: params.message, - inputToken: params.inputToken, - outputToken: params.outputToken, - }; + return { + inputAmount: adjustedInputAmount, + outputAmount: adjustedInputAmount.sub(finalQuote.totalRelayFee.total), + minOutputAmount: params.minOutputAmount, + suggestedFees: finalQuote, + message: params.message, + inputToken: params.inputToken, + outputToken: params.outputToken, + }; + } catch (err) { + if (err instanceof AxiosError) { + const { response = { data: {} } } = err; + // If upstream error is an AcrossApiError, we just return it + if (response?.data?.type === "AcrossApiError") { + throw new AcrossApiError( + { + message: response.data.message, + status: response.data.status, + code: response.data.code, + param: response.data.param, + }, + { cause: err } + ); + } else { + const message = `Upstream http request to ${err.request?.url} failed with ${err.status} ${err.message}`; + throw new AcrossApiError( + { + message, + status: HttpErrorToStatusCode.BAD_GATEWAY, + code: AcrossErrorCode.UPSTREAM_HTTP_ERROR, + }, + { cause: err } + ); + } + } + throw err; + } } export const providerCache: Record = {}; @@ -2188,11 +2220,10 @@ export async function getCachedTokenInfo(params: TokenOptions) { } // find decimals and symbol for any token address on any chain we support -export async function getTokenInfo({ - chainId, - address, -}: TokenOptions): Promise< - Pick +export async function getTokenInfo({ chainId, address }: TokenOptions): Promise< + Pick & { + chainId: number; + } > { try { if (!ethers.utils.isAddress(address)) { @@ -2222,6 +2253,7 @@ export async function getTokenInfo({ symbol: token.symbol, address: token.addresses[chainId], name: token.name, + chainId, }; } @@ -2258,6 +2290,7 @@ export async function getTokenInfo({ decimals, symbol, name, + chainId, }; } catch (error) { throw new TokenNotFoundError({ diff --git a/api/swap.ts b/api/swap.ts index 2dc0cc8a1..93b98e949 100644 --- a/api/swap.ts +++ b/api/swap.ts @@ -10,11 +10,15 @@ import { positiveIntStr, validAddress, boolStr, - getTokenByAddress, + getCachedTokenInfo, } from "./_utils"; -import { AMOUNT_TYPE, getCrossSwapQuotes } from "./_dexes/cross-swap"; -import { Token } from "./_dexes/types"; +import { + AMOUNT_TYPE, + buildCrossSwapTx, + getCrossSwapQuotes, +} from "./_dexes/cross-swap"; import { InvalidParamError, MissingParamError } from "./_errors"; +import { isValidIntegratorId } from "./_integrator-id"; const SwapQueryParamsSchema = type({ minOutputAmount: optional(positiveIntStr()), @@ -23,8 +27,9 @@ const SwapQueryParamsSchema = type({ outputToken: validAddress(), originChainId: positiveIntStr(), destinationChainId: positiveIntStr(), - recipient: validAddress(), - integratorId: string(), + depositor: validAddress(), + recipient: optional(validAddress()), + integratorId: optional(string()), refundAddress: optional(validAddress()), refundOnOrigin: optional(boolStr()), slippageTolerance: optional(positiveFloatStr(50)), // max. 50% slippage @@ -53,10 +58,11 @@ const handler = async ( originChainId: _originChainId, destinationChainId: _destinationChainId, recipient, + depositor, integratorId, refundAddress, refundOnOrigin: _refundOnOrigin = "true", - slippageTolerance = "0.5", // Default to 0.5% slippage + slippageTolerance = "1", // Default to 1% slippage } = query; const originChainId = Number(_originChainId); @@ -78,6 +84,13 @@ const handler = async ( }); } + if (integratorId && !isValidIntegratorId(integratorId)) { + throw new InvalidParamError({ + param: "integratorId", + message: "Invalid integrator ID. Needs to be 2 bytes hex string.", + }); + } + const amountType = _minOutputAmount ? AMOUNT_TYPE.MIN_OUTPUT : AMOUNT_TYPE.EXACT_INPUT; @@ -87,67 +100,52 @@ const handler = async ( : _minOutputAmount ); - // 1. Get auxiliary data - // - Token details - // - Token prices - const knownInputToken = getTokenByAddress( - _inputTokenAddress, - originChainId - ); - const inputToken: Token = knownInputToken - ? { - address: knownInputToken.addresses[originChainId]!, - decimals: knownInputToken.decimals, - symbol: knownInputToken.symbol, - chainId: originChainId, - } - : // @FIXME: fetch dynamic token details. using hardcoded values for now - { - address: _inputTokenAddress, - decimals: 18, - symbol: "UNKNOWN", - chainId: originChainId, - }; - const knownOutputToken = getTokenByAddress( - _outputTokenAddress, - destinationChainId - ); - const outputToken: Token = knownOutputToken - ? { - address: knownOutputToken.addresses[destinationChainId]!, - decimals: knownOutputToken.decimals, - symbol: knownOutputToken.symbol, - chainId: destinationChainId, - } - : // @FIXME: fetch dynamic token details. using hardcoded values for now - { - address: _outputTokenAddress, - decimals: 18, - symbol: "UNKNOWN", - chainId: destinationChainId, - }; + // 1. Get token details + const [inputToken, outputToken] = await Promise.all([ + getCachedTokenInfo({ + address: _inputTokenAddress, + chainId: originChainId, + }), + getCachedTokenInfo({ + address: _outputTokenAddress, + chainId: destinationChainId, + }), + ]); // 2. Get swap quotes and calldata based on the swap type const crossSwapQuotes = await getCrossSwapQuotes({ amount, inputToken, outputToken, - recipient, + depositor, + recipient: recipient || depositor, slippageTolerance: Number(slippageTolerance), type: amountType, refundOnOrigin, refundAddress, + // @TODO: Make this configurable via env var or query param + leftoverType: "bridgeableToken", }); - // 3. Build tx and return - // @TODO + // 3. Build cross swap tx + const crossSwapTx = await buildCrossSwapTx(crossSwapQuotes, integratorId); + + const responseJson = { + tx: { + to: crossSwapTx.to, + data: crossSwapTx.data, + value: crossSwapTx.value?.toString(), + gas: crossSwapTx.gas?.toString(), + gasPrice: crossSwapTx.gasPrice?.toString(), + }, + }; logger.debug({ at: "Swap", message: "Response data", - responseJson: crossSwapQuotes, + responseJson, }); - response.status(200).json(crossSwapQuotes); + response.status(200).json(responseJson); } catch (error: unknown) { return handleErrorCondition("swap", response, logger, error); } diff --git a/scripts/tests/swap.ts b/scripts/tests/swap.ts index c2d185abb..e3f40fd34 100644 --- a/scripts/tests/swap.ts +++ b/scripts/tests/swap.ts @@ -1,6 +1,9 @@ import axios from "axios"; import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants"; -import { ethers } from "ethers"; +import { ethers, Wallet } from "ethers"; +import dotenv from "dotenv"; +import { getProvider } from "../../api/_utils"; +dotenv.config(); /** * Manual test script for the swap API. Should be converted to a proper test suite. @@ -10,52 +13,74 @@ const depositor = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D"; const MIN_OUTPUT_CASES = [ // B2B { - minOutputAmount: ethers.utils.parseUnits("100", 6).toString(), + label: "B2B", + minOutputAmount: ethers.utils.parseUnits("1", 6).toString(), inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], originChainId: CHAIN_IDs.BASE, outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], destinationChainId: CHAIN_IDs.ARBITRUM, - recipient: depositor, - integratorId: "test", + depositor, }, // B2A { - minOutputAmount: ethers.utils.parseUnits("0.001", 18).toString(), + label: "B2A", + minOutputAmount: ethers.utils.parseUnits("1", 18).toString(), inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], originChainId: CHAIN_IDs.BASE, - outputToken: TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.ARBITRUM], + outputToken: "0x74885b4D524d497261259B38900f54e6dbAd2210", // APE Coin destinationChainId: CHAIN_IDs.ARBITRUM, - recipient: depositor, - integratorId: "test", + depositor, }, // A2B { - minOutputAmount: ethers.utils.parseUnits("10", 6).toString(), + label: "A2B", + minOutputAmount: ethers.utils.parseUnits("1", 6).toString(), inputToken: TOKEN_SYMBOLS_MAP.USDbC.addresses[CHAIN_IDs.BASE], originChainId: CHAIN_IDs.BASE, outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], destinationChainId: CHAIN_IDs.ARBITRUM, - recipient: depositor, - integratorId: "test", + depositor, }, // A2A { + label: "A2A", minOutputAmount: ethers.utils.parseUnits("1", 18).toString(), inputToken: TOKEN_SYMBOLS_MAP.USDbC.addresses[CHAIN_IDs.BASE], originChainId: CHAIN_IDs.BASE, outputToken: "0x74885b4D524d497261259B38900f54e6dbAd2210", // APE Coin destinationChainId: CHAIN_IDs.ARBITRUM, - recipient: depositor, - integratorId: "test", + depositor, + slippageTolerance: 1, }, ]; async function swap() { for (const testCase of MIN_OUTPUT_CASES) { + console.log("\nTest case:", testCase.label); const response = await axios.get(`http://localhost:3000/api/swap`, { params: testCase, }); console.log(response.data); + + if (process.env.DEV_WALLET_PK) { + const wallet = new Wallet(process.env.DEV_WALLET_PK!).connect( + getProvider(testCase.originChainId) + ); + try { + const tx = await wallet.sendTransaction({ + to: response.data.tx.to, + data: response.data.tx.data, + value: response.data.tx.value, + gasLimit: response.data.tx.gas, + gasPrice: response.data.tx.gasPrice, + }); + console.log("Tx hash: ", tx.hash); + await tx.wait(); + console.log("Tx mined"); + } catch (e) { + console.error("Tx reverted", e); + } + } } } From b5859729b3878666eb37d020d6e97563a291026c Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Thu, 21 Nov 2024 19:16:18 +0700 Subject: [PATCH 06/47] feat: support native eth as input/output (#1279) * feat: build cross swap tx * fixup * fixup * refactor * fix * fixup * fixup * feat: add leftover handling type * fixup * fixup * feat: support native eth as input/output * review requests * refactor leftover type into exact_output and min_output for clarity * add temp swap and bridge typechain binding and use for testing * fixup --- api/_dexes/cross-swap.ts | 79 +- api/_dexes/types.ts | 5 +- api/_dexes/uniswap.ts | 103 ++- api/_dexes/utils.ts | 104 ++- api/_multicall-handler.ts | 31 + .../SwapAndBridge.sol/SwapAndBridge.ts | 345 ++++++++ .../SwapAndBridge.sol/SwapAndBridgeBase.ts | 211 +++++ .../UniversalSwapAndBridge.ts | 671 +++++++++++++++ api/_typechain/SwapAndBridge.sol/index.ts | 6 + .../SwapAndBridgeBase__factory.ts | 153 ++++ .../SwapAndBridge__factory.ts | 367 +++++++++ .../UniversalSwapAndBridge__factory.ts | 777 ++++++++++++++++++ .../factories/SwapAndBridge.sol/index.ts | 6 + api/_utils.ts | 8 + api/swap.ts | 52 +- scripts/generate-routes.ts | 8 +- scripts/tests/swap.ts | 178 +++- ...6fA914353c44b2E33eBE05f21846F1048bEda.json | 8 +- 18 files changed, 2995 insertions(+), 117 deletions(-) create mode 100644 api/_typechain/SwapAndBridge.sol/SwapAndBridge.ts create mode 100644 api/_typechain/SwapAndBridge.sol/SwapAndBridgeBase.ts create mode 100644 api/_typechain/SwapAndBridge.sol/UniversalSwapAndBridge.ts create mode 100644 api/_typechain/SwapAndBridge.sol/index.ts create mode 100644 api/_typechain/factories/SwapAndBridge.sol/SwapAndBridgeBase__factory.ts create mode 100644 api/_typechain/factories/SwapAndBridge.sol/SwapAndBridge__factory.ts create mode 100644 api/_typechain/factories/SwapAndBridge.sol/UniversalSwapAndBridge__factory.ts create mode 100644 api/_typechain/factories/SwapAndBridge.sol/index.ts diff --git a/api/_dexes/cross-swap.ts b/api/_dexes/cross-swap.ts index 8599baa3d..800c0a70d 100644 --- a/api/_dexes/cross-swap.ts +++ b/api/_dexes/cross-swap.ts @@ -1,4 +1,5 @@ import { SpokePool } from "@across-protocol/contracts/dist/typechain"; +import { PopulatedTransaction } from "ethers"; import { isRouteEnabled, @@ -9,14 +10,17 @@ import { latestGasPriceCache, } from "../_utils"; import { - getUniswapCrossSwapQuotesForMinOutputB2A, - getUniswapCrossSwapQuotesForMinOutputA2B, - getBestUniswapCrossSwapQuotesForMinOutputA2A, + getUniswapCrossSwapQuotesForOutputB2A, + getUniswapCrossSwapQuotesForOutputA2B, + getBestUniswapCrossSwapQuotesForOutputA2A, } from "./uniswap"; import { CrossSwap, CrossSwapQuotes } from "./types"; -import { getSwapAndBridge } from "./utils"; +import { + buildExactOutputBridgeTokenMessage, + buildMinOutputBridgeTokenMessage, + getSwapAndBridge, +} from "./utils"; import { tagIntegratorId } from "../_integrator-id"; -import { PopulatedTransaction } from "ethers"; import { getMultiCallHandlerAddress } from "../_multicall-handler"; export type CrossSwapType = @@ -28,6 +32,7 @@ export type LeftoverType = (typeof LEFTOVER_TYPE)[keyof typeof LEFTOVER_TYPE]; export const AMOUNT_TYPE = { EXACT_INPUT: "exactInput", + EXACT_OUTPUT: "exactOutput", MIN_OUTPUT: "minOutput", } as const; @@ -53,14 +58,17 @@ export async function getCrossSwapQuotes( throw new Error("Not implemented yet"); } - if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT) { - return getCrossSwapQuotesForMinOutput(crossSwap); + if ( + crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT || + crossSwap.type === AMOUNT_TYPE.EXACT_OUTPUT + ) { + return getCrossSwapQuotesForOutput(crossSwap); } throw new Error("Invalid amount type"); } -export async function getCrossSwapQuotesForMinOutput(crossSwap: CrossSwap) { +export async function getCrossSwapQuotesForOutput(crossSwap: CrossSwap) { const crossSwapType = getCrossSwapType({ inputToken: crossSwap.inputToken.address, originChainId: crossSwap.inputToken.chainId, @@ -69,19 +77,19 @@ export async function getCrossSwapQuotesForMinOutput(crossSwap: CrossSwap) { }); if (crossSwapType === CROSS_SWAP_TYPE.BRIDGEABLE_TO_BRIDGEABLE) { - return getCrossSwapQuotesForMinOutputB2B(crossSwap); + return getCrossSwapQuotesForOutputB2B(crossSwap); } if (crossSwapType === CROSS_SWAP_TYPE.BRIDGEABLE_TO_ANY) { - return getCrossSwapQuotesForMinOutputB2A(crossSwap); + return getCrossSwapQuotesForOutputB2A(crossSwap); } if (crossSwapType === CROSS_SWAP_TYPE.ANY_TO_BRIDGEABLE) { - return getCrossSwapQuotesForMinOutputA2B(crossSwap); + return getCrossSwapQuotesForOutputA2B(crossSwap); } if (crossSwapType === CROSS_SWAP_TYPE.ANY_TO_ANY) { - return getCrossSwapQuotesForMinOutputA2A(crossSwap); + return getCrossSwapQuotesForOutputA2A(crossSwap); } throw new Error("Invalid cross swap type"); @@ -92,14 +100,25 @@ export async function getCrossSwapQuotesForExactInput(crossSwap: CrossSwap) { throw new Error("Not implemented yet"); } -export async function getCrossSwapQuotesForMinOutputB2B(crossSwap: CrossSwap) { +export async function getCrossSwapQuotesForOutputB2B(crossSwap: CrossSwap) { const bridgeQuote = await getBridgeQuoteForMinOutput({ inputToken: crossSwap.inputToken, outputToken: crossSwap.outputToken, minOutputAmount: crossSwap.amount, - // @TODO: handle ETH/WETH message generation - message: "0x", + recipient: getMultiCallHandlerAddress(crossSwap.outputToken.chainId), + message: + crossSwap.type === AMOUNT_TYPE.EXACT_OUTPUT + ? buildExactOutputBridgeTokenMessage(crossSwap) + : buildMinOutputBridgeTokenMessage(crossSwap), }); + + if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT) { + bridgeQuote.message = buildMinOutputBridgeTokenMessage( + crossSwap, + bridgeQuote.outputAmount + ); + } + return { crossSwap, destinationSwapQuote: undefined, @@ -108,19 +127,19 @@ export async function getCrossSwapQuotesForMinOutputB2B(crossSwap: CrossSwap) { }; } -export async function getCrossSwapQuotesForMinOutputB2A(crossSwap: CrossSwap) { +export async function getCrossSwapQuotesForOutputB2A(crossSwap: CrossSwap) { // @TODO: Add support for other DEXes / aggregators - return getUniswapCrossSwapQuotesForMinOutputB2A(crossSwap); + return getUniswapCrossSwapQuotesForOutputB2A(crossSwap); } -export async function getCrossSwapQuotesForMinOutputA2B(crossSwap: CrossSwap) { +export async function getCrossSwapQuotesForOutputA2B(crossSwap: CrossSwap) { // @TODO: Add support for other DEXes / aggregators - return getUniswapCrossSwapQuotesForMinOutputA2B(crossSwap); + return getUniswapCrossSwapQuotesForOutputA2B(crossSwap); } -export async function getCrossSwapQuotesForMinOutputA2A(crossSwap: CrossSwap) { +export async function getCrossSwapQuotesForOutputA2A(crossSwap: CrossSwap) { // @TODO: Add support for other DEXes / aggregators - return getBestUniswapCrossSwapQuotesForMinOutputA2A(crossSwap, { + return getBestUniswapCrossSwapQuotesForOutputA2A(crossSwap, { preferredBridgeTokens: PREFERRED_BRIDGE_TOKENS, bridgeRoutesLimit: 2, }); @@ -163,9 +182,7 @@ export async function buildCrossSwapTx( const spokePool = getSpokePool(originChainId); const deposit = { depositor: crossSwapQuotes.crossSwap.depositor, - recipient: crossSwapQuotes.destinationSwapQuote - ? getMultiCallHandlerAddress(destinationChainId) - : crossSwapQuotes.crossSwap.recipient, + recipient: getMultiCallHandlerAddress(destinationChainId), inputToken: crossSwapQuotes.bridgeQuote.inputToken.address, outputToken: crossSwapQuotes.bridgeQuote.outputToken.address, inputAmount: crossSwapQuotes.bridgeQuote.inputAmount, @@ -191,7 +208,12 @@ export async function buildCrossSwapTx( crossSwapQuotes.originSwapQuote.swapTx.data, crossSwapQuotes.originSwapQuote.maximumAmountIn, crossSwapQuotes.originSwapQuote.minAmountOut, - deposit + deposit, + { + value: crossSwapQuotes.crossSwap.isInputNative + ? deposit.inputAmount + : 0, + } ); toAddress = swapAndBridge.address; } else { @@ -210,7 +232,12 @@ export async function buildCrossSwapTx( deposit.quoteTimestamp, deposit.fillDeadline, deposit.exclusivityDeadline, - deposit.message + deposit.message, + { + value: crossSwapQuotes.crossSwap.isInputNative + ? deposit.inputAmount + : 0, + } ); toAddress = spokePool.address; } diff --git a/api/_dexes/types.ts b/api/_dexes/types.ts index 6e00f04ce..d8205279a 100644 --- a/api/_dexes/types.ts +++ b/api/_dexes/types.ts @@ -19,7 +19,8 @@ export type Swap = { recipient: string; slippageTolerance: number; type: AmountType; - leftoverType?: LeftoverType; + isInputNative?: boolean; + isOutputNative?: boolean; }; export type CrossSwap = { @@ -33,6 +34,8 @@ export type CrossSwap = { leftoverType?: LeftoverType; refundOnOrigin: boolean; refundAddress?: string; + isInputNative?: boolean; + isOutputNative?: boolean; }; export type SupportedDex = "1inch" | "uniswap"; diff --git a/api/_dexes/uniswap.ts b/api/_dexes/uniswap.ts index b658edfc8..02f46cdc4 100644 --- a/api/_dexes/uniswap.ts +++ b/api/_dexes/uniswap.ts @@ -19,6 +19,10 @@ import { import { TOKEN_SYMBOLS_MAP } from "../_constants"; import { buildMulticallHandlerMessage, + encodeApproveCalldata, + encodeDrainCalldata, + encodeTransferCalldata, + encodeWethWithdrawCalldata, getMultiCallHandlerAddress, } from "../_multicall-handler"; import { @@ -28,8 +32,13 @@ import { CrossSwap, SwapQuote, } from "./types"; -import { getSwapAndBridgeAddress, NoSwapRouteError } from "./utils"; -import { LEFTOVER_TYPE } from "./cross-swap"; +import { + buildExactOutputBridgeTokenMessage, + buildMinOutputBridgeTokenMessage, + getSwapAndBridgeAddress, + NoSwapRouteError, +} from "./utils"; +import { AMOUNT_TYPE } from "./cross-swap"; // Taken from here: https://docs.uniswap.org/contracts/v3/reference/deployments/ export const SWAP_ROUTER_02_ADDRESS = { @@ -103,7 +112,7 @@ export async function getUniswapQuoteForOriginSwapExactInput( * 1. Get destination swap quote for bridgeable output token -> any token * 2. Get bridge quote for bridgeable input token -> bridgeable output token */ -export async function getUniswapCrossSwapQuotesForMinOutputB2A( +export async function getUniswapCrossSwapQuotesForOutputB2A( crossSwap: CrossSwap ) { const destinationSwapChainId = crossSwap.outputToken.chainId; @@ -141,7 +150,7 @@ export async function getUniswapCrossSwapQuotesForMinOutputB2A( chainId: destinationSwapChainId, tokenIn: bridgeableOutputToken, tokenOut: crossSwap.outputToken, - recipient: crossSwap.recipient, + recipient: getMultiCallHandlerAddress(destinationSwapChainId), slippageTolerance: crossSwap.slippageTolerance, }; // 1.1. Get destination swap quote for bridgeable output token -> any token @@ -154,8 +163,8 @@ export async function getUniswapCrossSwapQuotesForMinOutputB2A( TradeType.EXACT_OUTPUT ); // 1.2. Re-fetch destination swap quote with exact input amount if leftover tokens - // should be sent as output tokens instead of bridgeable output tokens. - if (crossSwap.leftoverType === LEFTOVER_TYPE.OUTPUT_TOKEN) { + // should be sent to receiver. + if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT) { destinationSwapQuote = await getUniswapQuote( { ...destinationSwap, @@ -196,10 +205,11 @@ export async function getUniswapCrossSwapQuotesForMinOutputB2A( * 1. Get bridge quote for bridgeable input token -> bridgeable output token * 2. Get origin swap quote for any input token -> bridgeable input token */ -export async function getUniswapCrossSwapQuotesForMinOutputA2B( +export async function getUniswapCrossSwapQuotesForOutputA2B( crossSwap: CrossSwap ) { const originSwapChainId = crossSwap.inputToken.chainId; + const destinationChainId = crossSwap.outputToken.chainId; const bridgeRoute = getRouteByOutputTokenAndOriginChain( crossSwap.outputToken.address, originSwapChainId @@ -235,8 +245,16 @@ export async function getUniswapCrossSwapQuotesForMinOutputA2B( inputToken: bridgeableInputToken, outputToken: crossSwap.outputToken, minOutputAmount: crossSwap.amount, - // @TODO: handle ETH/WETH message generation + recipient: getMultiCallHandlerAddress(destinationChainId), + message: buildExactOutputBridgeTokenMessage(crossSwap), }); + // 1.1. Update bridge quote message for min. output amount + if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT && crossSwap.isOutputNative) { + bridgeQuote.message = buildMinOutputBridgeTokenMessage( + crossSwap, + bridgeQuote.outputAmount + ); + } const originSwap = { chainId: originSwapChainId, @@ -286,7 +304,7 @@ export async function getUniswapCrossSwapQuotesForMinOutputA2B( * @param crossSwap * @param opts */ -export async function getBestUniswapCrossSwapQuotesForMinOutputA2A( +export async function getBestUniswapCrossSwapQuotesForOutputA2A( crossSwap: CrossSwap, opts: { preferredBridgeTokens: string[]; @@ -321,7 +339,7 @@ export async function getBestUniswapCrossSwapQuotesForMinOutputA2A( const crossSwapQuotes = await Promise.all( bridgeRoutesToCompare.map((bridgeRoute) => - getUniswapCrossSwapQuotesForMinOutputA2A(crossSwap, bridgeRoute) + getUniswapCrossSwapQuotesForOutputA2A(crossSwap, bridgeRoute) ) ); @@ -342,7 +360,7 @@ export async function getBestUniswapCrossSwapQuotesForMinOutputA2A( * @param crossSwap * @param bridgeRoute */ -export async function getUniswapCrossSwapQuotesForMinOutputA2A( +export async function getUniswapCrossSwapQuotesForOutputA2A( crossSwap: CrossSwap, bridgeRoute: { fromTokenAddress: string; @@ -398,7 +416,7 @@ export async function getUniswapCrossSwapQuotesForMinOutputA2A( chainId: destinationSwapChainId, tokenIn: bridgeableOutputToken, tokenOut: crossSwap.outputToken, - recipient: crossSwap.recipient, + recipient: getMultiCallHandlerAddress(destinationSwapChainId), slippageTolerance: crossSwap.slippageTolerance, }; @@ -412,8 +430,8 @@ export async function getUniswapCrossSwapQuotesForMinOutputA2A( TradeType.EXACT_OUTPUT ); // 1.2. Re-fetch destination swap quote with exact input amount if leftover tokens - // should be sent as output tokens instead of bridgeable output tokens. - if (crossSwap.leftoverType === LEFTOVER_TYPE.OUTPUT_TOKEN) { + // should be sent to receiver. + if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT) { destinationSwapQuote = await getUniswapQuote( { ...destinationSwap, @@ -607,6 +625,41 @@ function buildDestinationSwapCrossChainMessage({ destinationSwapQuote: SwapQuote; }) { const destinationSwapChainId = destinationSwapQuote.tokenOut.chainId; + const transferActions = crossSwap.isOutputNative + ? // If output token is native, we need to unwrap WETH before sending it to the + // recipient. This is because we only handle WETH in the destination swap. + [ + { + target: crossSwap.outputToken.address, + callData: encodeWethWithdrawCalldata(crossSwap.amount), + value: "0", + }, + { + target: crossSwap.recipient, + callData: "0x", + value: crossSwap.amount.toString(), + }, + ] + : [ + { + target: crossSwap.outputToken.address, + callData: encodeTransferCalldata( + crossSwap.recipient, + crossSwap.amount + ), + value: "0", + }, + { + target: getMultiCallHandlerAddress(destinationSwapChainId), + callData: encodeDrainCalldata( + crossSwap.outputToken.address, + crossSwap.type === AMOUNT_TYPE.EXACT_OUTPUT + ? crossSwap.depositor + : crossSwap.recipient + ), + value: "0", + }, + ]; return buildMulticallHandlerMessage({ // @TODO: handle fallback recipient for params `refundOnOrigin` and `refundAddress` fallbackRecipient: crossSwap.depositor, @@ -626,12 +679,16 @@ function buildDestinationSwapCrossChainMessage({ callData: destinationSwapQuote.swapTx.data, value: destinationSwapQuote.swapTx.value, }, + // transfer output tokens to recipient + ...transferActions, // drain remaining bridgeable output tokens from MultiCallHandler contract { target: getMultiCallHandlerAddress(destinationSwapChainId), callData: encodeDrainCalldata( bridgeableOutputToken.address, - crossSwap.depositor + crossSwap.type === AMOUNT_TYPE.EXACT_OUTPUT + ? crossSwap.depositor + : crossSwap.recipient ), value: "0", }, @@ -639,22 +696,6 @@ function buildDestinationSwapCrossChainMessage({ }); } -function encodeApproveCalldata(spender: string, value: ethers.BigNumber) { - const approveFunction = "function approve(address spender, uint256 value)"; - const erc20Interface = new ethers.utils.Interface([approveFunction]); - return erc20Interface.encodeFunctionData("approve", [spender, value]); -} - -function encodeDrainCalldata(token: string, destination: string) { - const drainFunction = - "function drainLeftoverTokens(address token, address payable destination)"; - const multicallHandlerInterface = new ethers.utils.Interface([drainFunction]); - return multicallHandlerInterface.encodeFunctionData("drainLeftoverTokens", [ - token, - destination, - ]); -} - function assertMinOutputAmount( amountOut: BigNumber, expectedMinAmountOut: BigNumber diff --git a/api/_dexes/utils.ts b/api/_dexes/utils.ts index 06cb97ece..3702417dd 100644 --- a/api/_dexes/utils.ts +++ b/api/_dexes/utils.ts @@ -1,6 +1,15 @@ -import { UniversalSwapAndBridge__factory } from "@across-protocol/contracts/dist/typechain"; +import { UniversalSwapAndBridge__factory } from "../_typechain/factories/SwapAndBridge.sol"; +import { BigNumber } from "ethers"; import { ENABLED_ROUTES, getProvider } from "../_utils"; +import { + buildMulticallHandlerMessage, + encodeDrainCalldata, + encodeTransferCalldata, + encodeWethWithdrawCalldata, + getMultiCallHandlerAddress, +} from "../_multicall-handler"; +import { CrossSwap } from "./types"; export class UnsupportedDex extends Error { constructor(dex: string) { @@ -60,3 +69,96 @@ function _isDexSupported( ): dex is keyof typeof ENABLED_ROUTES.swapAndBridgeAddresses { return swapAndBridgeDexes.includes(dex); } + +/** + * This builds a cross-chain message for a (any/bridgeable)-to-bridgeable cross swap + * with a specific amount of output tokens that the recipient will receive. Excess + * tokens are refunded to the depositor. + */ +export function buildExactOutputBridgeTokenMessage(crossSwap: CrossSwap) { + const transferActions = crossSwap.isOutputNative + ? // WETH unwrap to ETH + [ + { + target: crossSwap.outputToken.address, + callData: encodeWethWithdrawCalldata(crossSwap.amount), + value: "0", + }, + { + target: crossSwap.recipient, + callData: "0x", + value: crossSwap.amount.toString(), + }, + ] + : // ERC-20 token transfer + [ + { + target: crossSwap.outputToken.address, + callData: encodeTransferCalldata( + crossSwap.recipient, + crossSwap.amount + ), + value: "0", + }, + ]; + return buildMulticallHandlerMessage({ + // @TODO: handle fallback recipient for params `refundOnOrigin` and `refundAddress` + fallbackRecipient: crossSwap.depositor, + actions: [ + ...transferActions, + // drain remaining bridgeable output tokens from MultiCallHandler contract + { + target: getMultiCallHandlerAddress(crossSwap.outputToken.chainId), + callData: encodeDrainCalldata( + crossSwap.outputToken.address, + crossSwap.depositor + ), + value: "0", + }, + ], + }); +} + +/** + * This builds a cross-chain message for a (any/bridgeable)-to-bridgeable cross swap + * with a min. amount of output tokens that the recipient will receive. + */ +export function buildMinOutputBridgeTokenMessage( + crossSwap: CrossSwap, + unwrapAmount?: BigNumber +) { + const transferActions = crossSwap.isOutputNative + ? // WETH unwrap to ETH + [ + { + target: crossSwap.outputToken.address, + callData: encodeWethWithdrawCalldata( + unwrapAmount || crossSwap.amount + ), + value: "0", + }, + { + target: crossSwap.recipient, + callData: "0x", + value: crossSwap.amount.toString(), + }, + ] + : // ERC-20 token transfer + []; + return buildMulticallHandlerMessage({ + // @TODO: handle fallback recipient for params `refundOnOrigin` and `refundAddress` + fallbackRecipient: crossSwap.depositor, + actions: [ + ...transferActions, + // drain remaining bridgeable output tokens from MultiCallHandler contract + { + target: getMultiCallHandlerAddress(crossSwap.outputToken.chainId), + callData: encodeDrainCalldata( + crossSwap.outputToken.address, + crossSwap.recipient + ), + value: "0", + }, + ], + }); +} diff --git a/api/_multicall-handler.ts b/api/_multicall-handler.ts index 9bcb86a7b..a4c8ef6c1 100644 --- a/api/_multicall-handler.ts +++ b/api/_multicall-handler.ts @@ -46,3 +46,34 @@ export function buildMulticallHandlerMessage(params: { ] ); } + +export function encodeWethWithdrawCalldata(value: ethers.BigNumber) { + const withdrawFunction = "function withdraw(uint256 wad)"; + const wethInterface = new ethers.utils.Interface([withdrawFunction]); + return wethInterface.encodeFunctionData("withdraw", [value]); +} + +export function encodeApproveCalldata( + spender: string, + value: ethers.BigNumber +) { + const approveFunction = "function approve(address spender, uint256 value)"; + const erc20Interface = new ethers.utils.Interface([approveFunction]); + return erc20Interface.encodeFunctionData("approve", [spender, value]); +} + +export function encodeTransferCalldata(to: string, value: ethers.BigNumber) { + const approveFunction = "function transfer(address to, uint256 value)"; + const erc20Interface = new ethers.utils.Interface([approveFunction]); + return erc20Interface.encodeFunctionData("transfer", [to, value]); +} + +export function encodeDrainCalldata(token: string, destination: string) { + const drainFunction = + "function drainLeftoverTokens(address token, address payable destination)"; + const multicallHandlerInterface = new ethers.utils.Interface([drainFunction]); + return multicallHandlerInterface.encodeFunctionData("drainLeftoverTokens", [ + token, + destination, + ]); +} diff --git a/api/_typechain/SwapAndBridge.sol/SwapAndBridge.ts b/api/_typechain/SwapAndBridge.sol/SwapAndBridge.ts new file mode 100644 index 000000000..8909d98ba --- /dev/null +++ b/api/_typechain/SwapAndBridge.sol/SwapAndBridge.ts @@ -0,0 +1,345 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +import type { + BaseContract, + BigNumber, + BigNumberish, + BytesLike, + CallOverrides, + ContractTransaction, + Overrides, + PopulatedTransaction, + Signer, + utils, +} from "ethers"; +import type { + FunctionFragment, + Result, + EventFragment, +} from "@ethersproject/abi"; +import type { Listener, Provider } from "@ethersproject/providers"; +import type { + TypedEventFilter, + TypedEvent, + TypedListener, + OnEvent, +} from "@across-protocol/contracts/dist/typechain/common"; + +export declare namespace SwapAndBridgeBase { + export type DepositDataStruct = { + outputToken: string; + outputAmount: BigNumberish; + depositor: string; + recipient: string; + destinationChainid: BigNumberish; + exclusiveRelayer: string; + quoteTimestamp: BigNumberish; + fillDeadline: BigNumberish; + exclusivityDeadline: BigNumberish; + message: BytesLike; + }; + + export type DepositDataStructOutput = [ + string, + BigNumber, + string, + string, + BigNumber, + string, + number, + number, + number, + string, + ] & { + outputToken: string; + outputAmount: BigNumber; + depositor: string; + recipient: string; + destinationChainid: BigNumber; + exclusiveRelayer: string; + quoteTimestamp: number; + fillDeadline: number; + exclusivityDeadline: number; + message: string; + }; +} + +export interface SwapAndBridgeInterface extends utils.Interface { + functions: { + "ACROSS_INPUT_TOKEN()": FunctionFragment; + "EXCHANGE()": FunctionFragment; + "SPOKE_POOL()": FunctionFragment; + "SWAP_TOKEN()": FunctionFragment; + "allowedSelectors(bytes4)": FunctionFragment; + "multicall(bytes[])": FunctionFragment; + "swapAndBridge(bytes,uint256,uint256,(address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes))": FunctionFragment; + }; + + getFunction( + nameOrSignatureOrTopic: + | "ACROSS_INPUT_TOKEN" + | "EXCHANGE" + | "SPOKE_POOL" + | "SWAP_TOKEN" + | "allowedSelectors" + | "multicall" + | "swapAndBridge" + ): FunctionFragment; + + encodeFunctionData( + functionFragment: "ACROSS_INPUT_TOKEN", + values?: undefined + ): string; + encodeFunctionData(functionFragment: "EXCHANGE", values?: undefined): string; + encodeFunctionData( + functionFragment: "SPOKE_POOL", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "SWAP_TOKEN", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "allowedSelectors", + values: [BytesLike] + ): string; + encodeFunctionData( + functionFragment: "multicall", + values: [BytesLike[]] + ): string; + encodeFunctionData( + functionFragment: "swapAndBridge", + values: [ + BytesLike, + BigNumberish, + BigNumberish, + SwapAndBridgeBase.DepositDataStruct, + ] + ): string; + + decodeFunctionResult( + functionFragment: "ACROSS_INPUT_TOKEN", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "EXCHANGE", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "SPOKE_POOL", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "SWAP_TOKEN", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "allowedSelectors", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "multicall", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "swapAndBridge", + data: BytesLike + ): Result; + + events: { + "SwapBeforeBridge(address,address,address,uint256,uint256,address,uint256)": EventFragment; + }; + + getEvent(nameOrSignatureOrTopic: "SwapBeforeBridge"): EventFragment; +} + +export interface SwapBeforeBridgeEventObject { + exchange: string; + swapToken: string; + acrossInputToken: string; + swapTokenAmount: BigNumber; + acrossInputAmount: BigNumber; + acrossOutputToken: string; + acrossOutputAmount: BigNumber; +} +export type SwapBeforeBridgeEvent = TypedEvent< + [string, string, string, BigNumber, BigNumber, string, BigNumber], + SwapBeforeBridgeEventObject +>; + +export type SwapBeforeBridgeEventFilter = + TypedEventFilter; + +export interface SwapAndBridge extends BaseContract { + connect(signerOrProvider: Signer | Provider | string): this; + attach(addressOrName: string): this; + deployed(): Promise; + + interface: SwapAndBridgeInterface; + + queryFilter( + event: TypedEventFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise>; + + listeners( + eventFilter?: TypedEventFilter + ): Array>; + listeners(eventName?: string): Array; + removeAllListeners( + eventFilter: TypedEventFilter + ): this; + removeAllListeners(eventName?: string): this; + off: OnEvent; + on: OnEvent; + once: OnEvent; + removeListener: OnEvent; + + functions: { + ACROSS_INPUT_TOKEN(overrides?: CallOverrides): Promise<[string]>; + + EXCHANGE(overrides?: CallOverrides): Promise<[string]>; + + SPOKE_POOL(overrides?: CallOverrides): Promise<[string]>; + + SWAP_TOKEN(overrides?: CallOverrides): Promise<[string]>; + + allowedSelectors( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise<[boolean]>; + + multicall( + data: BytesLike[], + overrides?: Overrides & { from?: string } + ): Promise; + + swapAndBridge( + routerCalldata: BytesLike, + swapTokenAmount: BigNumberish, + minExpectedInputTokenAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + overrides?: Overrides & { from?: string } + ): Promise; + }; + + ACROSS_INPUT_TOKEN(overrides?: CallOverrides): Promise; + + EXCHANGE(overrides?: CallOverrides): Promise; + + SPOKE_POOL(overrides?: CallOverrides): Promise; + + SWAP_TOKEN(overrides?: CallOverrides): Promise; + + allowedSelectors( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise; + + multicall( + data: BytesLike[], + overrides?: Overrides & { from?: string } + ): Promise; + + swapAndBridge( + routerCalldata: BytesLike, + swapTokenAmount: BigNumberish, + minExpectedInputTokenAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + overrides?: Overrides & { from?: string } + ): Promise; + + callStatic: { + ACROSS_INPUT_TOKEN(overrides?: CallOverrides): Promise; + + EXCHANGE(overrides?: CallOverrides): Promise; + + SPOKE_POOL(overrides?: CallOverrides): Promise; + + SWAP_TOKEN(overrides?: CallOverrides): Promise; + + allowedSelectors( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise; + + multicall(data: BytesLike[], overrides?: CallOverrides): Promise; + + swapAndBridge( + routerCalldata: BytesLike, + swapTokenAmount: BigNumberish, + minExpectedInputTokenAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + overrides?: CallOverrides + ): Promise; + }; + + filters: { + "SwapBeforeBridge(address,address,address,uint256,uint256,address,uint256)"( + exchange?: null, + swapToken?: string | null, + acrossInputToken?: string | null, + swapTokenAmount?: null, + acrossInputAmount?: null, + acrossOutputToken?: string | null, + acrossOutputAmount?: null + ): SwapBeforeBridgeEventFilter; + SwapBeforeBridge( + exchange?: null, + swapToken?: string | null, + acrossInputToken?: string | null, + swapTokenAmount?: null, + acrossInputAmount?: null, + acrossOutputToken?: string | null, + acrossOutputAmount?: null + ): SwapBeforeBridgeEventFilter; + }; + + estimateGas: { + ACROSS_INPUT_TOKEN(overrides?: CallOverrides): Promise; + + EXCHANGE(overrides?: CallOverrides): Promise; + + SPOKE_POOL(overrides?: CallOverrides): Promise; + + SWAP_TOKEN(overrides?: CallOverrides): Promise; + + allowedSelectors( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise; + + multicall( + data: BytesLike[], + overrides?: Overrides & { from?: string } + ): Promise; + + swapAndBridge( + routerCalldata: BytesLike, + swapTokenAmount: BigNumberish, + minExpectedInputTokenAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + overrides?: Overrides & { from?: string } + ): Promise; + }; + + populateTransaction: { + ACROSS_INPUT_TOKEN( + overrides?: CallOverrides + ): Promise; + + EXCHANGE(overrides?: CallOverrides): Promise; + + SPOKE_POOL(overrides?: CallOverrides): Promise; + + SWAP_TOKEN(overrides?: CallOverrides): Promise; + + allowedSelectors( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise; + + multicall( + data: BytesLike[], + overrides?: Overrides & { from?: string } + ): Promise; + + swapAndBridge( + routerCalldata: BytesLike, + swapTokenAmount: BigNumberish, + minExpectedInputTokenAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + overrides?: Overrides & { from?: string } + ): Promise; + }; +} diff --git a/api/_typechain/SwapAndBridge.sol/SwapAndBridgeBase.ts b/api/_typechain/SwapAndBridge.sol/SwapAndBridgeBase.ts new file mode 100644 index 000000000..bbbdf9bdb --- /dev/null +++ b/api/_typechain/SwapAndBridge.sol/SwapAndBridgeBase.ts @@ -0,0 +1,211 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ + +import type { + BaseContract, + BigNumber, + BytesLike, + CallOverrides, + ContractTransaction, + Overrides, + PopulatedTransaction, + Signer, + utils, +} from "ethers"; +import type { + FunctionFragment, + Result, + EventFragment, +} from "@ethersproject/abi"; +import type { Listener, Provider } from "@ethersproject/providers"; +import type { + TypedEventFilter, + TypedEvent, + TypedListener, + OnEvent, +} from "@across-protocol/contracts/dist/typechain/common"; + +export interface SwapAndBridgeBaseInterface extends utils.Interface { + functions: { + "EXCHANGE()": FunctionFragment; + "SPOKE_POOL()": FunctionFragment; + "allowedSelectors(bytes4)": FunctionFragment; + "multicall(bytes[])": FunctionFragment; + }; + + getFunction( + nameOrSignatureOrTopic: + | "EXCHANGE" + | "SPOKE_POOL" + | "allowedSelectors" + | "multicall" + ): FunctionFragment; + + encodeFunctionData(functionFragment: "EXCHANGE", values?: undefined): string; + encodeFunctionData( + functionFragment: "SPOKE_POOL", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "allowedSelectors", + values: [BytesLike] + ): string; + encodeFunctionData( + functionFragment: "multicall", + values: [BytesLike[]] + ): string; + + decodeFunctionResult(functionFragment: "EXCHANGE", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "SPOKE_POOL", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "allowedSelectors", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "multicall", data: BytesLike): Result; + + events: { + "SwapBeforeBridge(address,address,address,uint256,uint256,address,uint256)": EventFragment; + }; + + getEvent(nameOrSignatureOrTopic: "SwapBeforeBridge"): EventFragment; +} + +export interface SwapBeforeBridgeEventObject { + exchange: string; + swapToken: string; + acrossInputToken: string; + swapTokenAmount: BigNumber; + acrossInputAmount: BigNumber; + acrossOutputToken: string; + acrossOutputAmount: BigNumber; +} +export type SwapBeforeBridgeEvent = TypedEvent< + [string, string, string, BigNumber, BigNumber, string, BigNumber], + SwapBeforeBridgeEventObject +>; + +export type SwapBeforeBridgeEventFilter = + TypedEventFilter; + +export interface SwapAndBridgeBase extends BaseContract { + connect(signerOrProvider: Signer | Provider | string): this; + attach(addressOrName: string): this; + deployed(): Promise; + + interface: SwapAndBridgeBaseInterface; + + queryFilter( + event: TypedEventFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise>; + + listeners( + eventFilter?: TypedEventFilter + ): Array>; + listeners(eventName?: string): Array; + removeAllListeners( + eventFilter: TypedEventFilter + ): this; + removeAllListeners(eventName?: string): this; + off: OnEvent; + on: OnEvent; + once: OnEvent; + removeListener: OnEvent; + + functions: { + EXCHANGE(overrides?: CallOverrides): Promise<[string]>; + + SPOKE_POOL(overrides?: CallOverrides): Promise<[string]>; + + allowedSelectors( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise<[boolean]>; + + multicall( + data: BytesLike[], + overrides?: Overrides & { from?: string } + ): Promise; + }; + + EXCHANGE(overrides?: CallOverrides): Promise; + + SPOKE_POOL(overrides?: CallOverrides): Promise; + + allowedSelectors( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise; + + multicall( + data: BytesLike[], + overrides?: Overrides & { from?: string } + ): Promise; + + callStatic: { + EXCHANGE(overrides?: CallOverrides): Promise; + + SPOKE_POOL(overrides?: CallOverrides): Promise; + + allowedSelectors( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise; + + multicall(data: BytesLike[], overrides?: CallOverrides): Promise; + }; + + filters: { + "SwapBeforeBridge(address,address,address,uint256,uint256,address,uint256)"( + exchange?: null, + swapToken?: string | null, + acrossInputToken?: string | null, + swapTokenAmount?: null, + acrossInputAmount?: null, + acrossOutputToken?: string | null, + acrossOutputAmount?: null + ): SwapBeforeBridgeEventFilter; + SwapBeforeBridge( + exchange?: null, + swapToken?: string | null, + acrossInputToken?: string | null, + swapTokenAmount?: null, + acrossInputAmount?: null, + acrossOutputToken?: string | null, + acrossOutputAmount?: null + ): SwapBeforeBridgeEventFilter; + }; + + estimateGas: { + EXCHANGE(overrides?: CallOverrides): Promise; + + SPOKE_POOL(overrides?: CallOverrides): Promise; + + allowedSelectors( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise; + + multicall( + data: BytesLike[], + overrides?: Overrides & { from?: string } + ): Promise; + }; + + populateTransaction: { + EXCHANGE(overrides?: CallOverrides): Promise; + + SPOKE_POOL(overrides?: CallOverrides): Promise; + + allowedSelectors( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise; + + multicall( + data: BytesLike[], + overrides?: Overrides & { from?: string } + ): Promise; + }; +} diff --git a/api/_typechain/SwapAndBridge.sol/UniversalSwapAndBridge.ts b/api/_typechain/SwapAndBridge.sol/UniversalSwapAndBridge.ts new file mode 100644 index 000000000..1612a08c3 --- /dev/null +++ b/api/_typechain/SwapAndBridge.sol/UniversalSwapAndBridge.ts @@ -0,0 +1,671 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +import type { + BaseContract, + BigNumber, + BigNumberish, + BytesLike, + CallOverrides, + ContractTransaction, + Overrides, + PayableOverrides, + PopulatedTransaction, + Signer, + utils, +} from "ethers"; +import type { + FunctionFragment, + Result, + EventFragment, +} from "@ethersproject/abi"; +import type { Listener, Provider } from "@ethersproject/providers"; +import type { + TypedEventFilter, + TypedEvent, + TypedListener, + OnEvent, +} from "@across-protocol/contracts/dist/typechain/common"; + +export declare namespace SwapAndBridgeBase { + export type DepositDataStruct = { + outputToken: string; + outputAmount: BigNumberish; + depositor: string; + recipient: string; + destinationChainid: BigNumberish; + exclusiveRelayer: string; + quoteTimestamp: BigNumberish; + fillDeadline: BigNumberish; + exclusivityDeadline: BigNumberish; + message: BytesLike; + }; + + export type DepositDataStructOutput = [ + string, + BigNumber, + string, + string, + BigNumber, + string, + number, + number, + number, + string, + ] & { + outputToken: string; + outputAmount: BigNumber; + depositor: string; + recipient: string; + destinationChainid: BigNumber; + exclusiveRelayer: string; + quoteTimestamp: number; + fillDeadline: number; + exclusivityDeadline: number; + message: string; + }; +} + +export interface UniversalSwapAndBridgeInterface extends utils.Interface { + functions: { + "EXCHANGE()": FunctionFragment; + "SPOKE_POOL()": FunctionFragment; + "allowedSelectors(bytes4)": FunctionFragment; + "depositWithAuthorization(address,uint256,(address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes),uint256,uint256,bytes32,uint8,bytes32,bytes32)": FunctionFragment; + "depositWithPermit(address,uint256,(address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes),uint256,uint8,bytes32,bytes32)": FunctionFragment; + "multicall(bytes[])": FunctionFragment; + "swapAndBridge(address,address,bytes,uint256,uint256,(address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes))": FunctionFragment; + "swapAndBridgeWithAuthorization(address,address,bytes,uint256,uint256,(address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes),uint256,uint256,bytes32,uint8,bytes32,bytes32)": FunctionFragment; + "swapAndBridgeWithPermit(address,address,bytes,uint256,uint256,(address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes),uint256,uint8,bytes32,bytes32)": FunctionFragment; + }; + + getFunction( + nameOrSignatureOrTopic: + | "EXCHANGE" + | "SPOKE_POOL" + | "allowedSelectors" + | "depositWithAuthorization" + | "depositWithPermit" + | "multicall" + | "swapAndBridge" + | "swapAndBridgeWithAuthorization" + | "swapAndBridgeWithPermit" + ): FunctionFragment; + + encodeFunctionData(functionFragment: "EXCHANGE", values?: undefined): string; + encodeFunctionData( + functionFragment: "SPOKE_POOL", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "allowedSelectors", + values: [BytesLike] + ): string; + encodeFunctionData( + functionFragment: "depositWithAuthorization", + values: [ + string, + BigNumberish, + SwapAndBridgeBase.DepositDataStruct, + BigNumberish, + BigNumberish, + BytesLike, + BigNumberish, + BytesLike, + BytesLike, + ] + ): string; + encodeFunctionData( + functionFragment: "depositWithPermit", + values: [ + string, + BigNumberish, + SwapAndBridgeBase.DepositDataStruct, + BigNumberish, + BigNumberish, + BytesLike, + BytesLike, + ] + ): string; + encodeFunctionData( + functionFragment: "multicall", + values: [BytesLike[]] + ): string; + encodeFunctionData( + functionFragment: "swapAndBridge", + values: [ + string, + string, + BytesLike, + BigNumberish, + BigNumberish, + SwapAndBridgeBase.DepositDataStruct, + ] + ): string; + encodeFunctionData( + functionFragment: "swapAndBridgeWithAuthorization", + values: [ + string, + string, + BytesLike, + BigNumberish, + BigNumberish, + SwapAndBridgeBase.DepositDataStruct, + BigNumberish, + BigNumberish, + BytesLike, + BigNumberish, + BytesLike, + BytesLike, + ] + ): string; + encodeFunctionData( + functionFragment: "swapAndBridgeWithPermit", + values: [ + string, + string, + BytesLike, + BigNumberish, + BigNumberish, + SwapAndBridgeBase.DepositDataStruct, + BigNumberish, + BigNumberish, + BytesLike, + BytesLike, + ] + ): string; + + decodeFunctionResult(functionFragment: "EXCHANGE", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "SPOKE_POOL", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "allowedSelectors", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "depositWithAuthorization", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "depositWithPermit", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "multicall", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "swapAndBridge", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "swapAndBridgeWithAuthorization", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "swapAndBridgeWithPermit", + data: BytesLike + ): Result; + + events: { + "SwapBeforeBridge(address,address,address,uint256,uint256,address,uint256)": EventFragment; + }; + + getEvent(nameOrSignatureOrTopic: "SwapBeforeBridge"): EventFragment; +} + +export interface SwapBeforeBridgeEventObject { + exchange: string; + swapToken: string; + acrossInputToken: string; + swapTokenAmount: BigNumber; + acrossInputAmount: BigNumber; + acrossOutputToken: string; + acrossOutputAmount: BigNumber; +} +export type SwapBeforeBridgeEvent = TypedEvent< + [string, string, string, BigNumber, BigNumber, string, BigNumber], + SwapBeforeBridgeEventObject +>; + +export type SwapBeforeBridgeEventFilter = + TypedEventFilter; + +export interface UniversalSwapAndBridge extends BaseContract { + connect(signerOrProvider: Signer | Provider | string): this; + attach(addressOrName: string): this; + deployed(): Promise; + + interface: UniversalSwapAndBridgeInterface; + + queryFilter( + event: TypedEventFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise>; + + listeners( + eventFilter?: TypedEventFilter + ): Array>; + listeners(eventName?: string): Array; + removeAllListeners( + eventFilter: TypedEventFilter + ): this; + removeAllListeners(eventName?: string): this; + off: OnEvent; + on: OnEvent; + once: OnEvent; + removeListener: OnEvent; + + functions: { + EXCHANGE(overrides?: CallOverrides): Promise<[string]>; + + SPOKE_POOL(overrides?: CallOverrides): Promise<[string]>; + + allowedSelectors( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise<[boolean]>; + + depositWithAuthorization( + acrossInputToken: string, + acrossInputAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + validAfter: BigNumberish, + validBefore: BigNumberish, + nonce: BytesLike, + v: BigNumberish, + r: BytesLike, + s: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + depositWithPermit( + acrossInputToken: string, + acrossInputAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + deadline: BigNumberish, + v: BigNumberish, + r: BytesLike, + s: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + multicall( + data: BytesLike[], + overrides?: Overrides & { from?: string } + ): Promise; + + swapAndBridge( + swapToken: string, + acrossInputToken: string, + routerCalldata: BytesLike, + swapTokenAmount: BigNumberish, + minExpectedInputTokenAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + overrides?: PayableOverrides & { from?: string } + ): Promise; + + swapAndBridgeWithAuthorization( + swapToken: string, + acrossInputToken: string, + routerCalldata: BytesLike, + swapTokenAmount: BigNumberish, + minExpectedInputTokenAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + validAfter: BigNumberish, + validBefore: BigNumberish, + nonce: BytesLike, + v: BigNumberish, + r: BytesLike, + s: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + swapAndBridgeWithPermit( + swapToken: string, + acrossInputToken: string, + routerCalldata: BytesLike, + swapTokenAmount: BigNumberish, + minExpectedInputTokenAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + deadline: BigNumberish, + v: BigNumberish, + r: BytesLike, + s: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + }; + + EXCHANGE(overrides?: CallOverrides): Promise; + + SPOKE_POOL(overrides?: CallOverrides): Promise; + + allowedSelectors( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise; + + depositWithAuthorization( + acrossInputToken: string, + acrossInputAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + validAfter: BigNumberish, + validBefore: BigNumberish, + nonce: BytesLike, + v: BigNumberish, + r: BytesLike, + s: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + depositWithPermit( + acrossInputToken: string, + acrossInputAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + deadline: BigNumberish, + v: BigNumberish, + r: BytesLike, + s: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + multicall( + data: BytesLike[], + overrides?: Overrides & { from?: string } + ): Promise; + + swapAndBridge( + swapToken: string, + acrossInputToken: string, + routerCalldata: BytesLike, + swapTokenAmount: BigNumberish, + minExpectedInputTokenAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + overrides?: PayableOverrides & { from?: string } + ): Promise; + + swapAndBridgeWithAuthorization( + swapToken: string, + acrossInputToken: string, + routerCalldata: BytesLike, + swapTokenAmount: BigNumberish, + minExpectedInputTokenAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + validAfter: BigNumberish, + validBefore: BigNumberish, + nonce: BytesLike, + v: BigNumberish, + r: BytesLike, + s: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + swapAndBridgeWithPermit( + swapToken: string, + acrossInputToken: string, + routerCalldata: BytesLike, + swapTokenAmount: BigNumberish, + minExpectedInputTokenAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + deadline: BigNumberish, + v: BigNumberish, + r: BytesLike, + s: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + callStatic: { + EXCHANGE(overrides?: CallOverrides): Promise; + + SPOKE_POOL(overrides?: CallOverrides): Promise; + + allowedSelectors( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise; + + depositWithAuthorization( + acrossInputToken: string, + acrossInputAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + validAfter: BigNumberish, + validBefore: BigNumberish, + nonce: BytesLike, + v: BigNumberish, + r: BytesLike, + s: BytesLike, + overrides?: CallOverrides + ): Promise; + + depositWithPermit( + acrossInputToken: string, + acrossInputAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + deadline: BigNumberish, + v: BigNumberish, + r: BytesLike, + s: BytesLike, + overrides?: CallOverrides + ): Promise; + + multicall(data: BytesLike[], overrides?: CallOverrides): Promise; + + swapAndBridge( + swapToken: string, + acrossInputToken: string, + routerCalldata: BytesLike, + swapTokenAmount: BigNumberish, + minExpectedInputTokenAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + overrides?: CallOverrides + ): Promise; + + swapAndBridgeWithAuthorization( + swapToken: string, + acrossInputToken: string, + routerCalldata: BytesLike, + swapTokenAmount: BigNumberish, + minExpectedInputTokenAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + validAfter: BigNumberish, + validBefore: BigNumberish, + nonce: BytesLike, + v: BigNumberish, + r: BytesLike, + s: BytesLike, + overrides?: CallOverrides + ): Promise; + + swapAndBridgeWithPermit( + swapToken: string, + acrossInputToken: string, + routerCalldata: BytesLike, + swapTokenAmount: BigNumberish, + minExpectedInputTokenAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + deadline: BigNumberish, + v: BigNumberish, + r: BytesLike, + s: BytesLike, + overrides?: CallOverrides + ): Promise; + }; + + filters: { + "SwapBeforeBridge(address,address,address,uint256,uint256,address,uint256)"( + exchange?: null, + swapToken?: string | null, + acrossInputToken?: string | null, + swapTokenAmount?: null, + acrossInputAmount?: null, + acrossOutputToken?: string | null, + acrossOutputAmount?: null + ): SwapBeforeBridgeEventFilter; + SwapBeforeBridge( + exchange?: null, + swapToken?: string | null, + acrossInputToken?: string | null, + swapTokenAmount?: null, + acrossInputAmount?: null, + acrossOutputToken?: string | null, + acrossOutputAmount?: null + ): SwapBeforeBridgeEventFilter; + }; + + estimateGas: { + EXCHANGE(overrides?: CallOverrides): Promise; + + SPOKE_POOL(overrides?: CallOverrides): Promise; + + allowedSelectors( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise; + + depositWithAuthorization( + acrossInputToken: string, + acrossInputAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + validAfter: BigNumberish, + validBefore: BigNumberish, + nonce: BytesLike, + v: BigNumberish, + r: BytesLike, + s: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + depositWithPermit( + acrossInputToken: string, + acrossInputAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + deadline: BigNumberish, + v: BigNumberish, + r: BytesLike, + s: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + multicall( + data: BytesLike[], + overrides?: Overrides & { from?: string } + ): Promise; + + swapAndBridge( + swapToken: string, + acrossInputToken: string, + routerCalldata: BytesLike, + swapTokenAmount: BigNumberish, + minExpectedInputTokenAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + overrides?: PayableOverrides & { from?: string } + ): Promise; + + swapAndBridgeWithAuthorization( + swapToken: string, + acrossInputToken: string, + routerCalldata: BytesLike, + swapTokenAmount: BigNumberish, + minExpectedInputTokenAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + validAfter: BigNumberish, + validBefore: BigNumberish, + nonce: BytesLike, + v: BigNumberish, + r: BytesLike, + s: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + swapAndBridgeWithPermit( + swapToken: string, + acrossInputToken: string, + routerCalldata: BytesLike, + swapTokenAmount: BigNumberish, + minExpectedInputTokenAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + deadline: BigNumberish, + v: BigNumberish, + r: BytesLike, + s: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + }; + + populateTransaction: { + EXCHANGE(overrides?: CallOverrides): Promise; + + SPOKE_POOL(overrides?: CallOverrides): Promise; + + allowedSelectors( + arg0: BytesLike, + overrides?: CallOverrides + ): Promise; + + depositWithAuthorization( + acrossInputToken: string, + acrossInputAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + validAfter: BigNumberish, + validBefore: BigNumberish, + nonce: BytesLike, + v: BigNumberish, + r: BytesLike, + s: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + depositWithPermit( + acrossInputToken: string, + acrossInputAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + deadline: BigNumberish, + v: BigNumberish, + r: BytesLike, + s: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + multicall( + data: BytesLike[], + overrides?: Overrides & { from?: string } + ): Promise; + + swapAndBridge( + swapToken: string, + acrossInputToken: string, + routerCalldata: BytesLike, + swapTokenAmount: BigNumberish, + minExpectedInputTokenAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + overrides?: PayableOverrides & { from?: string } + ): Promise; + + swapAndBridgeWithAuthorization( + swapToken: string, + acrossInputToken: string, + routerCalldata: BytesLike, + swapTokenAmount: BigNumberish, + minExpectedInputTokenAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + validAfter: BigNumberish, + validBefore: BigNumberish, + nonce: BytesLike, + v: BigNumberish, + r: BytesLike, + s: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + swapAndBridgeWithPermit( + swapToken: string, + acrossInputToken: string, + routerCalldata: BytesLike, + swapTokenAmount: BigNumberish, + minExpectedInputTokenAmount: BigNumberish, + depositData: SwapAndBridgeBase.DepositDataStruct, + deadline: BigNumberish, + v: BigNumberish, + r: BytesLike, + s: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + }; +} diff --git a/api/_typechain/SwapAndBridge.sol/index.ts b/api/_typechain/SwapAndBridge.sol/index.ts new file mode 100644 index 000000000..c2a19dda2 --- /dev/null +++ b/api/_typechain/SwapAndBridge.sol/index.ts @@ -0,0 +1,6 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ + +export type { SwapAndBridge } from "./SwapAndBridge"; +export type { SwapAndBridgeBase } from "./SwapAndBridgeBase"; +export type { UniversalSwapAndBridge } from "./UniversalSwapAndBridge"; diff --git a/api/_typechain/factories/SwapAndBridge.sol/SwapAndBridgeBase__factory.ts b/api/_typechain/factories/SwapAndBridge.sol/SwapAndBridgeBase__factory.ts new file mode 100644 index 000000000..39c7908d8 --- /dev/null +++ b/api/_typechain/factories/SwapAndBridge.sol/SwapAndBridgeBase__factory.ts @@ -0,0 +1,153 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ + +import { Contract, Signer, utils } from "ethers"; +import type { Provider } from "@ethersproject/providers"; +import type { + SwapAndBridgeBase, + SwapAndBridgeBaseInterface, +} from "../../SwapAndBridge.sol/SwapAndBridgeBase"; + +const _abi = [ + { + inputs: [], + name: "InvalidFunctionSelector", + type: "error", + }, + { + inputs: [], + name: "LeftoverSrcTokens", + type: "error", + }, + { + inputs: [], + name: "MinimumExpectedInputAmount", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "exchange", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "swapToken", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "acrossInputToken", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "swapTokenAmount", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "acrossInputAmount", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "acrossOutputToken", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "acrossOutputAmount", + type: "uint256", + }, + ], + name: "SwapBeforeBridge", + type: "event", + }, + { + inputs: [], + name: "EXCHANGE", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "SPOKE_POOL", + outputs: [ + { + internalType: "contract V3SpokePoolInterface", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes4", + name: "", + type: "bytes4", + }, + ], + name: "allowedSelectors", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes[]", + name: "data", + type: "bytes[]", + }, + ], + name: "multicall", + outputs: [ + { + internalType: "bytes[]", + name: "results", + type: "bytes[]", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, +] as const; + +export class SwapAndBridgeBase__factory { + static readonly abi = _abi; + static createInterface(): SwapAndBridgeBaseInterface { + return new utils.Interface(_abi) as SwapAndBridgeBaseInterface; + } + static connect( + address: string, + signerOrProvider: Signer | Provider + ): SwapAndBridgeBase { + return new Contract(address, _abi, signerOrProvider) as SwapAndBridgeBase; + } +} diff --git a/api/_typechain/factories/SwapAndBridge.sol/SwapAndBridge__factory.ts b/api/_typechain/factories/SwapAndBridge.sol/SwapAndBridge__factory.ts new file mode 100644 index 000000000..770f17c74 --- /dev/null +++ b/api/_typechain/factories/SwapAndBridge.sol/SwapAndBridge__factory.ts @@ -0,0 +1,367 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ + +import { + Signer, + utils, + Contract, + ContractFactory, + BytesLike, + Overrides, +} from "ethers"; +import type { Provider, TransactionRequest } from "@ethersproject/providers"; +import type { + SwapAndBridge, + SwapAndBridgeInterface, +} from "../../SwapAndBridge.sol/SwapAndBridge"; + +const _abi = [ + { + inputs: [ + { + internalType: "contract V3SpokePoolInterface", + name: "_spokePool", + type: "address", + }, + { + internalType: "contract WETH9Interface", + name: "_wrappedNativeToken", + type: "address", + }, + { + internalType: "address", + name: "_exchange", + type: "address", + }, + { + internalType: "bytes4[]", + name: "_allowedSelectors", + type: "bytes4[]", + }, + { + internalType: "contract IERC20", + name: "_swapToken", + type: "address", + }, + { + internalType: "contract IERC20", + name: "_acrossInputToken", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "InvalidFunctionSelector", + type: "error", + }, + { + inputs: [], + name: "LeftoverSrcTokens", + type: "error", + }, + { + inputs: [], + name: "MinimumExpectedInputAmount", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "exchange", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "swapToken", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "acrossInputToken", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "swapTokenAmount", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "acrossInputAmount", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "acrossOutputToken", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "acrossOutputAmount", + type: "uint256", + }, + ], + name: "SwapBeforeBridge", + type: "event", + }, + { + inputs: [], + name: "ACROSS_INPUT_TOKEN", + outputs: [ + { + internalType: "contract IERC20", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "EXCHANGE", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "SPOKE_POOL", + outputs: [ + { + internalType: "contract V3SpokePoolInterface", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "SWAP_TOKEN", + outputs: [ + { + internalType: "contract IERC20", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes4", + name: "", + type: "bytes4", + }, + ], + name: "allowedSelectors", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes[]", + name: "data", + type: "bytes[]", + }, + ], + name: "multicall", + outputs: [ + { + internalType: "bytes[]", + name: "results", + type: "bytes[]", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes", + name: "routerCalldata", + type: "bytes", + }, + { + internalType: "uint256", + name: "swapTokenAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "minExpectedInputTokenAmount", + type: "uint256", + }, + { + components: [ + { + internalType: "address", + name: "outputToken", + type: "address", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "address", + name: "depositor", + type: "address", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "destinationChainid", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityDeadline", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + internalType: "struct SwapAndBridgeBase.DepositData", + name: "depositData", + type: "tuple", + }, + ], + name: "swapAndBridge", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; + +const _bytecode = + "0x604061012060405234620001d2576200133d803803806200002081620001ea565b92833981019160c082840312620001d25781516001600160a01b03908181168103620001d25760209283850151938385168503620001d25760408601519384168403620001d25760608601516001600160401b039790888111620001d25787019080601f83011215620001d2578151988911620001d6576005918960051b908480620000ae818501620001ea565b809d81520192820101928311620001d2578401905b828210620001b057505050620000ea60a0620000e260808a0162000210565b980162000210565b97600196879660ff19966001885f5416175f5560805260a05260c0525f955b62000174575b888860e05261010090815260405161111791826200022683396080518281816101da0152610faf015260a05182818161041c015281816109600152610d18015260c05182505060e05182818160c9015261082c01525181818161016c015261084e0152f35b8051861015620001aa5786809663ffffffff60e01b8582861b85010151165f52818552855f208288825416179055019562000109565b6200010f565b81516001600160e01b031981168103620001d2578152908401908401620000c3565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b6040519190601f01601f191682016001600160401b03811183821017620001d657604052565b51906001600160a01b0382168203620001d25756fe60806040526004361015610011575f80fd5b5f3560e01c8063393bb94314610084578063652fa4b91461007f5780638021fef71461007a57806385f168eb14610075578063ac9650d814610070578063b50e44b81461006b5763e65ae3ae14610066575f80fd5b61044f565b6103d2565b61034a565b6101fe565b610190565b610122565b34610114575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101145773ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000166080527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8060a0016080f35b5f80fd5b5f91031261011457565b34610114575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011457602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b34610114575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011457602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b346101145760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610114576004357fffffffff000000000000000000000000000000000000000000000000000000008116809103610114575f526001602052602060ff60405f2054166040519015158152f35b5f5b8381106102865750505f910152565b8181015183820152602001610277565b602080820190808352835180925260408301928160408460051b8301019501935f915b8483106102c95750505050505090565b909192939495848080837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc086600196030187527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8c5161033681518092818752878088019101610275565b0116010198019301930191949392906102b9565b346101145760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101145767ffffffffffffffff6004358181116101145736602382011215610114578060040135918211610114573660248360051b83010111610114576103ce9160246103c2920161070a565b60405191829182610296565b0390f35b34610114575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011457602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b90816101409103126101145790565b346101145760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101145767ffffffffffffffff600435818111610114573660238201121561011457806004013582811161011457366024828401011161011457606435928311610114576104cf6104df933690600401610440565b91604435916024803592016107f1565b005b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b67ffffffffffffffff811161052257604052565b6104e1565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761052257604052565b67ffffffffffffffff81116105225760051b60200190565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215610114570180359067ffffffffffffffff82116101145760200191813603831361011457565b90821015610619576106159160051b8101906105ad565b9091565b610580565b908092918237015f815290565b67ffffffffffffffff811161052257601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b3d1561068f573d906106768261062b565b916106846040519384610527565b82523d5f602084013e565b606090565b6020818303126101145780519067ffffffffffffffff8211610114570181601f820112156101145780516106c78161062b565b926106d56040519485610527565b81845260208284010111610114576106f39160208085019101610275565b90565b80518210156106195760209160051b010190565b91909161071683610568565b9060406107266040519384610527565b8483527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe061075386610568565b015f5b8181106107e057505082945f5b818110610771575050505050565b5f8061077e8385886105fe565b9061078d87518093819361061e565b0390305af461079a610665565b90156107c057906001916107ae82886106f6565b526107b981876106f6565b5001610763565b604481511061011457806004610114920151602480918301019101610694565b806060602080938801015201610756565b9193909260ff5f5416156101145761082a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b7f0000000000000000000000000000000000000000000000000000000000000000927f0000000000000000000000000000000000000000000000000000000000000000946108bc6108b86108b16108818486610a73565b7fffffffff00000000000000000000000000000000000000000000000000000000165f52600160205260405f2090565b5460ff1690565b1590565b610a42576040517f70a082310000000000000000000000000000000000000000000000000000000080825230600483015273ffffffffffffffffffffffffffffffffffffffff98919260209182856024818c8f165afa948515610a1c575f95610a21575b506040519081523060048201529982908b9060249082908d165afa948515610a1c576109ac9a89935f976109dc575b50506109a7925f92838093610986887f00000000000000000000000000000000000000000000000000000000000000008096610afe565b6109956040518094819361061e565b03925af16109a1610665565b50610a6c565b610c29565b6109da60017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f5416175f55565b565b5f809491819499506109a7969381610a0892903d10610a15575b610a008183610527565b810190610ab7565b989350509281945061094f565b503d6109f6565b610ac6565b83919550610a3b90823d8411610a1557610a008183610527565b9490610920565b60046040517f42868c9b000000000000000000000000000000000000000000000000000000008152fd5b1561011457565b7fffffffff000000000000000000000000000000000000000000000000000000009035818116939260048110610aa857505050565b60040360031b82901b16169150565b90816020910312610114575190565b6040513d5f823e3d90fd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b60449192602073ffffffffffffffffffffffffffffffffffffffff604051948580927fdd62ed3e000000000000000000000000000000000000000000000000000000008252306004830152808916602483015286165afa928315610a1c575f93610bda575b508201809211610bd5576040517f095ea7b300000000000000000000000000000000000000000000000000000000602082015273ffffffffffffffffffffffffffffffffffffffff9390931660248401526044808401929092529082526109da9190610bd0606483610527565b610e02565b610ad1565b610bf491935060203d602011610a1557610a008183610527565b915f610b63565b91908203918211610bd557565b3573ffffffffffffffffffffffffffffffffffffffff811681036101145790565b6040517f70a082310000000000000000000000000000000000000000000000000000000080825230600483015273ffffffffffffffffffffffffffffffffffffffff989697959695602095878b16959294939087826024818a5afa8015610a1c57610c9b925f91610de5575b50610bfb565b978810610dbb576040519384523060048501528916928581602481875afa8015610a1c578392610cd1925f92610d9c5750610bfb565b03610d72576109da977f646284e396b68ff4b4f34e0aa97bcdb9c100f5b44a20da5c475f62703985384191610d6a610d088a610c08565b918960405194859416988c0135917f0000000000000000000000000000000000000000000000000000000000000000859094939260609273ffffffffffffffffffffffffffffffffffffffff6080840197168352602083015260408201520152565b0390a4610f96565b60046040517fd6cf42f0000000000000000000000000000000000000000000000000000000008152fd5b610db4919250883d8a11610a1557610a008183610527565b905f610c95565b60046040517f0492ff87000000000000000000000000000000000000000000000000000000008152fd5b610dfc9150893d8b11610a1557610a008183610527565b5f610c95565b73ffffffffffffffffffffffffffffffffffffffff166040516040810181811067ffffffffffffffff82111761052257610e7d937f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c656460205f948594604052818152015260208151910182855af1610e77610665565b916110b8565b8051908115918215610e93575b50501561011457565b819250906020918101031261011457602001518015158103610114575f80610e8a565b3563ffffffff811681036101145790565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe093818652868601375f8582860101520116010190565b989590946106f39d9b9792602095610f889a959c976101809d8d73ffffffffffffffffffffffffffffffffffffffff998a8096818096168452169101521660408d01521660608b015260808a015260a089015260c08801521660e086015263ffffffff8092166101008601521661012084015261014083019063ffffffff169052565b816101608201520191610ec7565b9073ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001692610fdb838583610afe565b610fe760408601610c08565b90610ff460608701610c08565b93610ffe87610c08565b9661100b60a08201610c08565b61101760c08301610eb6565b61102360e08401610eb6565b916110316101008501610eb6565b936110406101208201826105ad565b9790968c3b15610114576040519d8e809d819d7f7b939232000000000000000000000000000000000000000000000000000000008352608087013596602001359516916004019c6110909d610f05565b03815a5f948591f18015610a1c576110a55750565b806110b26109da9261050e565b80610118565b90156110d2578151156110c9575090565b3b156101145790565b50805190811561011457602001fdfea2646970667358221220026a7409d965bcdd82516120169821b365afc3acc0d52608453d28ca5160c46464736f6c63430008170033"; + +type SwapAndBridgeConstructorParams = + | [signer?: Signer] + | ConstructorParameters; + +const isSuperArgs = ( + xs: SwapAndBridgeConstructorParams +): xs is ConstructorParameters => xs.length > 1; + +export class SwapAndBridge__factory extends ContractFactory { + constructor(...args: SwapAndBridgeConstructorParams) { + if (isSuperArgs(args)) { + super(...args); + } else { + super(_abi, _bytecode, args[0]); + } + } + + override deploy( + _spokePool: string, + _wrappedNativeToken: string, + _exchange: string, + _allowedSelectors: BytesLike[], + _swapToken: string, + _acrossInputToken: string, + overrides?: Overrides & { from?: string } + ): Promise { + return super.deploy( + _spokePool, + _wrappedNativeToken, + _exchange, + _allowedSelectors, + _swapToken, + _acrossInputToken, + overrides || {} + ) as Promise; + } + override getDeployTransaction( + _spokePool: string, + _wrappedNativeToken: string, + _exchange: string, + _allowedSelectors: BytesLike[], + _swapToken: string, + _acrossInputToken: string, + overrides?: Overrides & { from?: string } + ): TransactionRequest { + return super.getDeployTransaction( + _spokePool, + _wrappedNativeToken, + _exchange, + _allowedSelectors, + _swapToken, + _acrossInputToken, + overrides || {} + ); + } + override attach(address: string): SwapAndBridge { + return super.attach(address) as SwapAndBridge; + } + override connect(signer: Signer): SwapAndBridge__factory { + return super.connect(signer) as SwapAndBridge__factory; + } + + static readonly bytecode = _bytecode; + static readonly abi = _abi; + static createInterface(): SwapAndBridgeInterface { + return new utils.Interface(_abi) as SwapAndBridgeInterface; + } + static connect( + address: string, + signerOrProvider: Signer | Provider + ): SwapAndBridge { + return new Contract(address, _abi, signerOrProvider) as SwapAndBridge; + } +} diff --git a/api/_typechain/factories/SwapAndBridge.sol/UniversalSwapAndBridge__factory.ts b/api/_typechain/factories/SwapAndBridge.sol/UniversalSwapAndBridge__factory.ts new file mode 100644 index 000000000..740fd1bcf --- /dev/null +++ b/api/_typechain/factories/SwapAndBridge.sol/UniversalSwapAndBridge__factory.ts @@ -0,0 +1,777 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ + +import { + Signer, + utils, + Contract, + ContractFactory, + BytesLike, + Overrides, +} from "ethers"; +import type { Provider, TransactionRequest } from "@ethersproject/providers"; +import type { + UniversalSwapAndBridge, + UniversalSwapAndBridgeInterface, +} from "../../SwapAndBridge.sol/UniversalSwapAndBridge"; + +const _abi = [ + { + inputs: [ + { + internalType: "contract V3SpokePoolInterface", + name: "_spokePool", + type: "address", + }, + { + internalType: "contract WETH9Interface", + name: "_wrappedNativeToken", + type: "address", + }, + { + internalType: "address", + name: "_exchange", + type: "address", + }, + { + internalType: "bytes4[]", + name: "_allowedSelectors", + type: "bytes4[]", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "InsufficientSwapValue", + type: "error", + }, + { + inputs: [], + name: "InvalidFunctionSelector", + type: "error", + }, + { + inputs: [], + name: "InvalidSwapToken", + type: "error", + }, + { + inputs: [], + name: "LeftoverSrcTokens", + type: "error", + }, + { + inputs: [], + name: "MinimumExpectedInputAmount", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "exchange", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "swapToken", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "acrossInputToken", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "swapTokenAmount", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "acrossInputAmount", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "acrossOutputToken", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "acrossOutputAmount", + type: "uint256", + }, + ], + name: "SwapBeforeBridge", + type: "event", + }, + { + inputs: [], + name: "EXCHANGE", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "SPOKE_POOL", + outputs: [ + { + internalType: "contract V3SpokePoolInterface", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes4", + name: "", + type: "bytes4", + }, + ], + name: "allowedSelectors", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "contract IERC20Auth", + name: "acrossInputToken", + type: "address", + }, + { + internalType: "uint256", + name: "acrossInputAmount", + type: "uint256", + }, + { + components: [ + { + internalType: "address", + name: "outputToken", + type: "address", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "address", + name: "depositor", + type: "address", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "destinationChainid", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityDeadline", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + internalType: "struct SwapAndBridgeBase.DepositData", + name: "depositData", + type: "tuple", + }, + { + internalType: "uint256", + name: "validAfter", + type: "uint256", + }, + { + internalType: "uint256", + name: "validBefore", + type: "uint256", + }, + { + internalType: "bytes32", + name: "nonce", + type: "bytes32", + }, + { + internalType: "uint8", + name: "v", + type: "uint8", + }, + { + internalType: "bytes32", + name: "r", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "s", + type: "bytes32", + }, + ], + name: "depositWithAuthorization", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "contract IERC20Permit", + name: "acrossInputToken", + type: "address", + }, + { + internalType: "uint256", + name: "acrossInputAmount", + type: "uint256", + }, + { + components: [ + { + internalType: "address", + name: "outputToken", + type: "address", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "address", + name: "depositor", + type: "address", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "destinationChainid", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityDeadline", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + internalType: "struct SwapAndBridgeBase.DepositData", + name: "depositData", + type: "tuple", + }, + { + internalType: "uint256", + name: "deadline", + type: "uint256", + }, + { + internalType: "uint8", + name: "v", + type: "uint8", + }, + { + internalType: "bytes32", + name: "r", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "s", + type: "bytes32", + }, + ], + name: "depositWithPermit", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes[]", + name: "data", + type: "bytes[]", + }, + ], + name: "multicall", + outputs: [ + { + internalType: "bytes[]", + name: "results", + type: "bytes[]", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "contract IERC20", + name: "swapToken", + type: "address", + }, + { + internalType: "contract IERC20", + name: "acrossInputToken", + type: "address", + }, + { + internalType: "bytes", + name: "routerCalldata", + type: "bytes", + }, + { + internalType: "uint256", + name: "swapTokenAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "minExpectedInputTokenAmount", + type: "uint256", + }, + { + components: [ + { + internalType: "address", + name: "outputToken", + type: "address", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "address", + name: "depositor", + type: "address", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "destinationChainid", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityDeadline", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + internalType: "struct SwapAndBridgeBase.DepositData", + name: "depositData", + type: "tuple", + }, + ], + name: "swapAndBridge", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "contract IERC20Auth", + name: "swapToken", + type: "address", + }, + { + internalType: "contract IERC20", + name: "acrossInputToken", + type: "address", + }, + { + internalType: "bytes", + name: "routerCalldata", + type: "bytes", + }, + { + internalType: "uint256", + name: "swapTokenAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "minExpectedInputTokenAmount", + type: "uint256", + }, + { + components: [ + { + internalType: "address", + name: "outputToken", + type: "address", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "address", + name: "depositor", + type: "address", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "destinationChainid", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityDeadline", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + internalType: "struct SwapAndBridgeBase.DepositData", + name: "depositData", + type: "tuple", + }, + { + internalType: "uint256", + name: "validAfter", + type: "uint256", + }, + { + internalType: "uint256", + name: "validBefore", + type: "uint256", + }, + { + internalType: "bytes32", + name: "nonce", + type: "bytes32", + }, + { + internalType: "uint8", + name: "v", + type: "uint8", + }, + { + internalType: "bytes32", + name: "r", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "s", + type: "bytes32", + }, + ], + name: "swapAndBridgeWithAuthorization", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "contract IERC20Permit", + name: "swapToken", + type: "address", + }, + { + internalType: "contract IERC20", + name: "acrossInputToken", + type: "address", + }, + { + internalType: "bytes", + name: "routerCalldata", + type: "bytes", + }, + { + internalType: "uint256", + name: "swapTokenAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "minExpectedInputTokenAmount", + type: "uint256", + }, + { + components: [ + { + internalType: "address", + name: "outputToken", + type: "address", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "address", + name: "depositor", + type: "address", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "destinationChainid", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityDeadline", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + internalType: "struct SwapAndBridgeBase.DepositData", + name: "depositData", + type: "tuple", + }, + { + internalType: "uint256", + name: "deadline", + type: "uint256", + }, + { + internalType: "uint8", + name: "v", + type: "uint8", + }, + { + internalType: "bytes32", + name: "r", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "s", + type: "bytes32", + }, + ], + name: "swapAndBridgeWithPermit", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; + +const _bytecode = + "0x604060e060405234620001905762001b2390813803806200002081620001a8565b9384398201608083820312620001905782516001600160a01b03929091908383168303620001905760209485810151958587168703620001905760408201519586168603620001905760608201516001600160401b03928382116200019057019380601f860112156200019057845192831162000194576005948360051b908380620000ae818501620001a8565b80978152019282010192831162000190578301905b8282106200016e57505050600196879660ff19966001885f5416175f5560805260a05260c0525f955b62000132575b6040516119549081620001cf82396080518181816102fb0152611418015260a05181818161053d015281816111d20152611757015260c051816106c70152f35b8151861015620001685786809663ffffffff60e01b8382881b86010151165f52818352845f2082888254161790550195620000ec565b620000f2565b81516001600160e01b03198116810362000190578152908301908301620000c3565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b6040519190601f01601f191682016001600160401b03811183821017620001945760405256fe60806040526004361015610011575f80fd5b5f3560e01c8063038f12ea146100a4578063277deffe1461009f5780638021fef71461009a57806385f168eb14610095578063ac9650d814610090578063b50e44b81461008b578063bdf52ad314610086578063c51e5eb9146100815763fdf152d31461007c575f80fd5b6107f6565b6105ee565b610561565b6104f3565b61046b565b61031f565b6102b1565b6101ff565b34610153576101807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610153576100dc610175565b6100e4610184565b67ffffffffffffffff919060443583811161015357610107903690600401610191565b9260a435948511610153576101236101519536906004016101bf565b9361012c6101ce565b9261016435956101443595610104359460e4359460c4359460843593606435936109d4565b005b5f80fd5b73ffffffffffffffffffffffffffffffffffffffff81160361015357565b6004359061018282610157565b565b6024359061018282610157565b9181601f840112156101535782359167ffffffffffffffff8311610153576020838186019501011161015357565b90816101409103126101535790565b610124359060ff8216820361015357565b60e4359060ff8216820361015357565b6084359060ff8216820361015357565b34610153576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101535760043561023b81610157565b610243610184565b67ffffffffffffffff919060443583811161015357610266903690600401610191565b919060a435948511610153576102836101519536906004016101bf565b61028b6101df565b926101243595610104359560c435946084359360643593610b86565b5f91031261015357565b34610153575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015357602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b346101535760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610153576004357fffffffff000000000000000000000000000000000000000000000000000000008116809103610153575f526001602052602060ff60405f2054166040519015158152f35b5f5b8381106103a75750505f910152565b8181015183820152602001610398565b602080820190808352835180925260408301928160408460051b8301019501935f915b8483106103ea5750505050505090565b909192939495848080837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc086600196030187527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8c5161045781518092818752878088019101610396565b0116010198019301930191949392906103da565b346101535760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101535767ffffffffffffffff6004358181116101535736602382011215610153578060040135918211610153573660248360051b83010111610153576104ef9160246104e39201610eab565b604051918291826103b7565b0390f35b34610153575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015357602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b34610153576101207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101535760043561059d81610157565b60443567ffffffffffffffff8111610153576105bd9036906004016101bf565b9060c4359160ff831683036101535761015192610104359260e4359260a43591608435916064359160243590610f92565b60c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610153576004803561062581610157565b6024359061063282610157565b67ffffffffffffffff604435818111610153576106529036908601610191565b916064359060a4359081116101535761066e90369088016101bf565b92610677611089565b6106a27fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b34156107e0578134036107b75773ffffffffffffffffffffffffffffffffffffffff807f000000000000000000000000000000000000000000000000000000000000000016809187160361078e57803b15610153575f90604051988980927fd0e30db000000000000000000000000000000000000000000000000000000000825234905af19687156107895761074297610770575b505b608435926110d9565b61015160017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f5416175f55565b8061077d61078392610b21565b806102a7565b5f610737565b610b7b565b876040517f3539a701000000000000000000000000000000000000000000000000000000008152fd5b866040517ff143e0da000000000000000000000000000000000000000000000000000000008152fd5b61074296506107f182303388611299565b610739565b346101535760e07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101535760043561083181610157565b60243560443567ffffffffffffffff8111610153576108549036906004016101bf565b9073ffffffffffffffffffffffffffffffffffffffff6108726101ef565b9361087b611089565b6108a67fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b6040517fdd62ed3e000000000000000000000000000000000000000000000000000000008152336004820152306024820152911690602081604481855afa80156107895783915f916109a5575b5010610910575b610742935061090b82303384611299565b6113ff565b803b15610153576040517fd505accf00000000000000000000000000000000000000000000000000000000815233600482015230602482015260448101839052606480359082015260ff94909416608485015260a480359085015260c48035908501525f8460e48183855af19384156107895761074294610992575b506108fa565b8061077d61099f92610b21565b5f61098c565b6109c7915060203d6020116109cd575b6109bf8183610b3a565b810190610cfa565b5f6108f3565b503d6109b5565b73ffffffffffffffffffffffffffffffffffffffff909c9a919b94979295989396999c6109ff611089565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f5516998a3b15610153576040517fef55bec6000000000000000000000000000000000000000000000000000000008152336004820152306024820152604481018990526064810193909352608483019390935260a482019b909b5260ff909a1660c48b015260e48a01919091526101048901525f8861012481838a5af197881561078957610ab798610ae5575b506110d9565b61018260017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f5416175f55565b610aee90610b21565b5f610ab1565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b67ffffffffffffffff8111610b3557604052565b610af4565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117610b3557604052565b6040513d5f823e3d90fd5b73ffffffffffffffffffffffffffffffffffffffff909a9293949596979891999a610baf611089565b610bda7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b6040517fdd62ed3e000000000000000000000000000000000000000000000000000000008152336004820152306024820152911698906020816044818d5afa80156107895787915f91610cdb575b5010610c48575b505050610ab79750610c4383303389611299565b6110d9565b883b15610153576040517fd505accf00000000000000000000000000000000000000000000000000000000815233600482015230602482015260448101879052606481019190915260ff91909116608482015260a481019990995260c48901525f8860e481838a5af197881561078957610ab798610cc8575b8080610c2f565b8061077d610cd592610b21565b5f610cc1565b610cf4915060203d6020116109cd576109bf8183610b3a565b5f610c28565b90816020910312610153575190565b67ffffffffffffffff8111610b355760051b60200190565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215610153570180359067ffffffffffffffff82116101535760200191813603831361015357565b90821015610dba57610db69160051b810190610d4e565b9091565b610d21565b908092918237015f815290565b67ffffffffffffffff8111610b3557601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b3d15610e30573d90610e1782610dcc565b91610e256040519384610b3a565b82523d5f602084013e565b606090565b6020818303126101535780519067ffffffffffffffff8211610153570181601f82011215610153578051610e6881610dcc565b92610e766040519485610b3a565b8184526020828401011161015357610e949160208085019101610396565b90565b8051821015610dba5760209160051b010190565b919091610eb783610d09565b906040610ec76040519384610b3a565b8483527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0610ef486610d09565b015f5b818110610f8157505082945f5b818110610f12575050505050565b5f80610f1f838588610d9f565b90610f2e875180938193610dbf565b0390305af4610f3b610e06565b9015610f615790600191610f4f8288610e97565b52610f5a8187610e97565b5001610f04565b604481511061015357806004610153920151602480918301019101610e35565b806060602080938801015201610ef7565b73ffffffffffffffffffffffffffffffffffffffff909895989794939297969196610fbb611089565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f551694853b15610153576040517fef55bec6000000000000000000000000000000000000000000000000000000008152336004820152306024820152604481018890526064810193909352608483019390935260a482019790975260ff90961660c487015260e48601919091526101048501525f846101248183855af193841561078957610ab794611073575b506113ff565b61107c90610b21565b5f61106d565b1561015357565b60ff5f54161561015357565b7fffffffff0000000000000000000000000000000000000000000000000000000090358181169392600481106110ca57505050565b60040360031b82901b16169150565b9095949392919561112e61112a6111236110f38486611095565b7fffffffff00000000000000000000000000000000000000000000000000000000165f52600160205260405f2090565b5460ff1690565b1590565b61126f576040517f70a082310000000000000000000000000000000000000000000000000000000080825230600483015273ffffffffffffffffffffffffffffffffffffffff98919260209182856024818c8f165afa948515610789575f9561124e575b506040519081523060048201529982908b9060249082908d165afa948515610789576101829a89935f9761121e575b5050611219925f928380936111f8887f00000000000000000000000000000000000000000000000000000000000000008096611548565b61120760405180948193610dbf565b03925af1611213610e06565b50611082565b611668565b5f8094918194995061121996938161124192903d106109cd576109bf8183610b3a565b98935050928194506111c1565b8391955061126890823d84116109cd576109bf8183610b3a565b9490611192565b60046040517f42868c9b000000000000000000000000000000000000000000000000000000008152fd5b9290604051927f23b872dd00000000000000000000000000000000000000000000000000000000602085015273ffffffffffffffffffffffffffffffffffffffff809216602485015216604483015260648201526064815260a081019181831067ffffffffffffffff841117610b355761018292604052611841565b35610e9481610157565b3563ffffffff811681036101535790565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe093818652868601375f8582860101520116010190565b98959094610e949d9b97926020956113f19a959c976101809d8d73ffffffffffffffffffffffffffffffffffffffff998a8096818096168452169101521660408d01521660608b015260808a015260a089015260c08801521660e086015263ffffffff8092166101008601521661012084015261014083019063ffffffff169052565b816101608201520191611330565b9073ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001692611444838583611548565b61145060408601611315565b9061145d60608701611315565b9361146787611315565b9661147460a08201611315565b61148060c0830161131f565b61148c60e0840161131f565b9161149a610100850161131f565b936114a9610120820182610d4e565b9790968c3b15610153576040519d8e809d819d7f7b939232000000000000000000000000000000000000000000000000000000008352608087013596602001359516916004019c6114f99d61136e565b03815a5f948591f180156107895761150e5750565b8061077d61018292610b21565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b6040517fdd62ed3e00000000000000000000000000000000000000000000000000000000815230600482015273ffffffffffffffffffffffffffffffffffffffff8316602482015291926020838060448101038173ffffffffffffffffffffffffffffffffffffffff86165afa928315610789575f9361163a575b508201809211611635576040517f095ea7b300000000000000000000000000000000000000000000000000000000602082015273ffffffffffffffffffffffffffffffffffffffff9390931660248401526044808401929092529082526101829190611630606483610b3a565b611841565b61151b565b61165491935060203d6020116109cd576109bf8183610b3a565b915f6115c3565b9190820391821161163557565b6040517f70a082310000000000000000000000000000000000000000000000000000000080825230600483015273ffffffffffffffffffffffffffffffffffffffff989697959695602095878b16959294939087826024818a5afa8015610789576116da925f91611824575b5061165b565b9788106117fa576040519384523060048501528916928581602481875afa8015610789578392611710925f926117db575061165b565b036117b157610182977f646284e396b68ff4b4f34e0aa97bcdb9c100f5b44a20da5c475f627039853841916117a96117478a611315565b918960405194859416988c0135917f0000000000000000000000000000000000000000000000000000000000000000859094939260609273ffffffffffffffffffffffffffffffffffffffff6080840197168352602083015260408201520152565b0390a46113ff565b60046040517fd6cf42f0000000000000000000000000000000000000000000000000000000008152fd5b6117f3919250883d8a116109cd576109bf8183610b3a565b905f6116d4565b60046040517f0492ff87000000000000000000000000000000000000000000000000000000008152fd5b61183b9150893d8b116109cd576109bf8183610b3a565b5f6116d4565b73ffffffffffffffffffffffffffffffffffffffff166040516040810181811067ffffffffffffffff821117610b35576118bc937f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c656460205f948594604052818152015260208151910182855af16118b6610e06565b916118f5565b80519081159182156118d2575b50501561015357565b819250906020918101031261015357602001518015158103610153575f806118c9565b901561190f57815115611906575090565b3b156101535790565b50805190811561015357602001fdfea264697066735822122066b7197a155308805fbedee5c26980493cce360e5c829be686db3534152c74bf64736f6c63430008170033"; + +type UniversalSwapAndBridgeConstructorParams = + | [signer?: Signer] + | ConstructorParameters; + +const isSuperArgs = ( + xs: UniversalSwapAndBridgeConstructorParams +): xs is ConstructorParameters => xs.length > 1; + +export class UniversalSwapAndBridge__factory extends ContractFactory { + constructor(...args: UniversalSwapAndBridgeConstructorParams) { + if (isSuperArgs(args)) { + super(...args); + } else { + super(_abi, _bytecode, args[0]); + } + } + + override deploy( + _spokePool: string, + _wrappedNativeToken: string, + _exchange: string, + _allowedSelectors: BytesLike[], + overrides?: Overrides & { from?: string } + ): Promise { + return super.deploy( + _spokePool, + _wrappedNativeToken, + _exchange, + _allowedSelectors, + overrides || {} + ) as Promise; + } + override getDeployTransaction( + _spokePool: string, + _wrappedNativeToken: string, + _exchange: string, + _allowedSelectors: BytesLike[], + overrides?: Overrides & { from?: string } + ): TransactionRequest { + return super.getDeployTransaction( + _spokePool, + _wrappedNativeToken, + _exchange, + _allowedSelectors, + overrides || {} + ); + } + override attach(address: string): UniversalSwapAndBridge { + return super.attach(address) as UniversalSwapAndBridge; + } + override connect(signer: Signer): UniversalSwapAndBridge__factory { + return super.connect(signer) as UniversalSwapAndBridge__factory; + } + + static readonly bytecode = _bytecode; + static readonly abi = _abi; + static createInterface(): UniversalSwapAndBridgeInterface { + return new utils.Interface(_abi) as UniversalSwapAndBridgeInterface; + } + static connect( + address: string, + signerOrProvider: Signer | Provider + ): UniversalSwapAndBridge { + return new Contract( + address, + _abi, + signerOrProvider + ) as UniversalSwapAndBridge; + } +} diff --git a/api/_typechain/factories/SwapAndBridge.sol/index.ts b/api/_typechain/factories/SwapAndBridge.sol/index.ts new file mode 100644 index 000000000..74ba6aa0d --- /dev/null +++ b/api/_typechain/factories/SwapAndBridge.sol/index.ts @@ -0,0 +1,6 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ + +export { SwapAndBridge__factory } from "./SwapAndBridge__factory"; +export { SwapAndBridgeBase__factory } from "./SwapAndBridgeBase__factory"; +export { UniversalSwapAndBridge__factory } from "./UniversalSwapAndBridge__factory"; diff --git a/api/_utils.ts b/api/_utils.ts index 7b43e9b08..3cb83e825 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -346,6 +346,14 @@ function getStaticIsContract(chainId: number, address: string) { return !!deployedAcrossContract; } +export function getWrappedNativeTokenAddress(chainId: number) { + if (chainId === CHAIN_IDs.POLYGON || chainId === CHAIN_IDs.POLYGON_AMOY) { + return TOKEN_SYMBOLS_MAP.WMATIC.addresses[chainId]; + } + + return TOKEN_SYMBOLS_MAP.WETH.addresses[chainId]; +} + /** * Utility function to resolve route details based on given `inputTokenAddress` and `destinationChainId`. * The optional parameter `originChainId` can be omitted if the `inputTokenAddress` is unique across all diff --git a/api/swap.ts b/api/swap.ts index 93b98e949..331725fcb 100644 --- a/api/swap.ts +++ b/api/swap.ts @@ -1,6 +1,6 @@ import { VercelResponse } from "@vercel/node"; import { assert, Infer, type, string, optional } from "superstruct"; -import { BigNumber } from "ethers"; +import { BigNumber, constants, utils } from "ethers"; import { TypedVercelRequest } from "./_types"; import { @@ -11,6 +11,7 @@ import { validAddress, boolStr, getCachedTokenInfo, + getWrappedNativeTokenAddress, } from "./_utils"; import { AMOUNT_TYPE, @@ -22,6 +23,7 @@ import { isValidIntegratorId } from "./_integrator-id"; const SwapQueryParamsSchema = type({ minOutputAmount: optional(positiveIntStr()), + exactOutputAmount: optional(positiveIntStr()), exactInputAmount: optional(positiveIntStr()), inputToken: validAddress(), outputToken: validAddress(), @@ -55,6 +57,7 @@ const handler = async ( outputToken: _outputTokenAddress, exactInputAmount: _exactInputAmount, minOutputAmount: _minOutputAmount, + exactOutputAmount: _exactOutputAmount, originChainId: _originChainId, destinationChainId: _destinationChainId, recipient, @@ -68,19 +71,34 @@ const handler = async ( const originChainId = Number(_originChainId); const destinationChainId = Number(_destinationChainId); const refundOnOrigin = _refundOnOrigin === "true"; - - if (!_minOutputAmount && !_exactInputAmount) { + const isInputNative = _inputTokenAddress === constants.AddressZero; + const isOutputNative = _outputTokenAddress === constants.AddressZero; + const inputTokenAddress = isInputNative + ? getWrappedNativeTokenAddress(originChainId) + : utils.getAddress(_inputTokenAddress); + const outputTokenAddress = isOutputNative + ? getWrappedNativeTokenAddress(destinationChainId) + : utils.getAddress(_outputTokenAddress); + + if (!_minOutputAmount && !_exactInputAmount && !_exactOutputAmount) { throw new MissingParamError({ - param: "minOutputAmount, exactInputAmount", - message: "One of 'minOutputAmount' or 'exactInputAmount' is required", + param: "minOutputAmount, exactInputAmount, exactOutputAmount", + message: + "One of 'minOutputAmount', 'exactInputAmount' or 'exactOutputAmount' is required", }); } - if (_minOutputAmount && _exactInputAmount) { + if (integratorId && !isValidIntegratorId(integratorId)) { throw new InvalidParamError({ - param: "minOutputAmount, exactInputAmount", - message: - "Only one of 'minOutputAmount' or 'exactInputAmount' is allowed", + param: "integratorId", + message: "Invalid integrator ID. Needs to be 2 bytes hex string.", + }); + } + + if (!inputTokenAddress || !outputTokenAddress) { + throw new InvalidParamError({ + param: "inputToken, outputToken", + message: "Invalid input or output token address", }); } @@ -93,21 +111,21 @@ const handler = async ( const amountType = _minOutputAmount ? AMOUNT_TYPE.MIN_OUTPUT - : AMOUNT_TYPE.EXACT_INPUT; + : _exactInputAmount + ? AMOUNT_TYPE.EXACT_INPUT + : AMOUNT_TYPE.EXACT_OUTPUT; const amount = BigNumber.from( - amountType === AMOUNT_TYPE.EXACT_INPUT - ? _exactInputAmount - : _minOutputAmount + _minOutputAmount || _exactInputAmount || _exactOutputAmount ); // 1. Get token details const [inputToken, outputToken] = await Promise.all([ getCachedTokenInfo({ - address: _inputTokenAddress, + address: inputTokenAddress, chainId: originChainId, }), getCachedTokenInfo({ - address: _outputTokenAddress, + address: outputTokenAddress, chainId: destinationChainId, }), ]); @@ -123,8 +141,8 @@ const handler = async ( type: amountType, refundOnOrigin, refundAddress, - // @TODO: Make this configurable via env var or query param - leftoverType: "bridgeableToken", + isInputNative, + isOutputNative, }); // 3. Build cross swap tx diff --git a/scripts/generate-routes.ts b/scripts/generate-routes.ts index af2bf0da3..fb6354cc2 100644 --- a/scripts/generate-routes.ts +++ b/scripts/generate-routes.ts @@ -97,10 +97,10 @@ const enabledRoutes = { [CHAIN_IDs.BASE]: "0x98285D11B9F7aFec2d475805E5255f26B4490167", }, uniswap: { - [CHAIN_IDs.POLYGON]: "0xa55490E20057BD4775618D0FC8D51F59f602FED0", - [CHAIN_IDs.OPTIMISM]: "0x04989eaF03547E6583f9d9e42aeD11D2b78A808b", - [CHAIN_IDs.ARBITRUM]: "0x2414A759d4EFF700Ad81e257Ab5187d07eCeEbAb", - [CHAIN_IDs.BASE]: "0xed8b9c9aE7aCEf12eb4650d26Eb876005a4752d2", + [CHAIN_IDs.POLYGON]: "0xC2dCB88873E00c9d401De2CBBa4C6A28f8A6e2c2", + [CHAIN_IDs.OPTIMISM]: "0x28249CD61170Ed200Db8988D7f69e21331AFFBD0", + [CHAIN_IDs.ARBITRUM]: "0x07b64BE6906A78fac5FaB47cD58d59D0A093B15e", + [CHAIN_IDs.BASE]: "0xef5a54BEeBDB4Ee98E2906e30e65AD53dA8D2b17", }, }, routes: transformChainConfigs(enabledMainnetChainConfigs), diff --git a/scripts/tests/swap.ts b/scripts/tests/swap.ts index e3f40fd34..e7a01ef20 100644 --- a/scripts/tests/swap.ts +++ b/scripts/tests/swap.ts @@ -13,58 +13,170 @@ const depositor = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D"; const MIN_OUTPUT_CASES = [ // B2B { - label: "B2B", - minOutputAmount: ethers.utils.parseUnits("1", 6).toString(), - inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], - originChainId: CHAIN_IDs.BASE, - outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], - destinationChainId: CHAIN_IDs.ARBITRUM, - depositor, + labels: ["B2B", "MIN_OUTPUT", "Base USDC", "Arbitrum USDC"], + params: { + minOutputAmount: ethers.utils.parseUnits("1", 6).toString(), + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], + originChainId: CHAIN_IDs.BASE, + outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], + destinationChainId: CHAIN_IDs.ARBITRUM, + depositor, + }, + }, + { + labels: ["B2B", "MIN_OUTPUT", "Base USDC", "Arbitrum ETH"], + params: { + minOutputAmount: ethers.utils.parseUnits("0.001", 18).toString(), + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], + originChainId: CHAIN_IDs.BASE, + outputToken: ethers.constants.AddressZero, + destinationChainId: CHAIN_IDs.ARBITRUM, + depositor, + }, }, // B2A { - label: "B2A", - minOutputAmount: ethers.utils.parseUnits("1", 18).toString(), - inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], - originChainId: CHAIN_IDs.BASE, - outputToken: "0x74885b4D524d497261259B38900f54e6dbAd2210", // APE Coin - destinationChainId: CHAIN_IDs.ARBITRUM, - depositor, + labels: ["B2A", "MIN_OUTPUT", "Base USDC", "Arbitrum WETH"], + params: { + minOutputAmount: ethers.utils.parseUnits("0.001", 18).toString(), + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], + originChainId: CHAIN_IDs.BASE, + outputToken: TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.ARBITRUM], + destinationChainId: CHAIN_IDs.ARBITRUM, + depositor, + }, + }, + { + labels: ["B2A", "MIN_OUTPUT", "Base USDC", "Arbitrum ETH"], + params: { + minOutputAmount: ethers.utils.parseUnits("0.001", 18).toString(), + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], + originChainId: CHAIN_IDs.BASE, + outputToken: ethers.constants.AddressZero, + destinationChainId: CHAIN_IDs.ARBITRUM, + depositor, + }, + }, + // A2B + { + labels: ["A2B", "MIN_OUTPUT", "Base USDbC", "Arbitrum USDC"], + params: { + minOutputAmount: ethers.utils.parseUnits("1", 6).toString(), + inputToken: TOKEN_SYMBOLS_MAP.USDbC.addresses[CHAIN_IDs.BASE], + originChainId: CHAIN_IDs.BASE, + outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], + destinationChainId: CHAIN_IDs.ARBITRUM, + depositor, + }, + }, + // A2A + { + labels: ["A2A", "MIN_OUTPUT", "Base USDbC", "Arbitrum APE"], + params: { + minOutputAmount: ethers.utils.parseUnits("1", 18).toString(), + inputToken: TOKEN_SYMBOLS_MAP.USDbC.addresses[CHAIN_IDs.BASE], + originChainId: CHAIN_IDs.BASE, + outputToken: "0x74885b4D524d497261259B38900f54e6dbAd2210", // APE Coin + destinationChainId: CHAIN_IDs.ARBITRUM, + depositor, + }, + }, +]; +const EXACT_OUTPUT_CASES = [ + // B2B + { + labels: ["B2B", "EXACT_OUTPUT", "Base USDC", "Arbitrum USDC"], + params: { + exactOutputAmount: ethers.utils.parseUnits("1", 6).toString(), + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], + originChainId: CHAIN_IDs.BASE, + outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], + destinationChainId: CHAIN_IDs.ARBITRUM, + depositor, + }, + }, + { + labels: ["B2B", "EXACT_OUTPUT", "Base USDC", "Arbitrum ETH"], + params: { + exactOutputAmount: ethers.utils.parseUnits("0.001", 18).toString(), + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], + originChainId: CHAIN_IDs.BASE, + outputToken: ethers.constants.AddressZero, + destinationChainId: CHAIN_IDs.ARBITRUM, + depositor, + }, + }, + // B2A + { + labels: ["B2A", "EXACT_OUTPUT", "Base USDC", "Arbitrum WETH"], + params: { + exactOutputAmount: ethers.utils.parseUnits("0.001", 18).toString(), + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], + originChainId: CHAIN_IDs.BASE, + outputToken: TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.ARBITRUM], + destinationChainId: CHAIN_IDs.ARBITRUM, + depositor, + }, + }, + { + labels: ["B2A", "EXACT_OUTPUT", "Base USDC", "Arbitrum ETH"], + params: { + exactOutputAmount: ethers.utils.parseUnits("0.001", 18).toString(), + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], + originChainId: CHAIN_IDs.BASE, + outputToken: ethers.constants.AddressZero, + destinationChainId: CHAIN_IDs.ARBITRUM, + depositor, + }, }, // A2B { - label: "A2B", - minOutputAmount: ethers.utils.parseUnits("1", 6).toString(), - inputToken: TOKEN_SYMBOLS_MAP.USDbC.addresses[CHAIN_IDs.BASE], - originChainId: CHAIN_IDs.BASE, - outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], - destinationChainId: CHAIN_IDs.ARBITRUM, - depositor, + labels: ["A2B", "EXACT_OUTPUT", "Base USDbC", "Arbitrum USDC"], + params: { + minOutputAmount: ethers.utils.parseUnits("1", 6).toString(), + inputToken: TOKEN_SYMBOLS_MAP.USDbC.addresses[CHAIN_IDs.BASE], + originChainId: CHAIN_IDs.BASE, + outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], + destinationChainId: CHAIN_IDs.ARBITRUM, + depositor, + }, }, // A2A { - label: "A2A", - minOutputAmount: ethers.utils.parseUnits("1", 18).toString(), - inputToken: TOKEN_SYMBOLS_MAP.USDbC.addresses[CHAIN_IDs.BASE], - originChainId: CHAIN_IDs.BASE, - outputToken: "0x74885b4D524d497261259B38900f54e6dbAd2210", // APE Coin - destinationChainId: CHAIN_IDs.ARBITRUM, - depositor, - slippageTolerance: 1, + labels: ["A2A", "EXACT_OUTPUT", "Base USDbC", "Arbitrum APE"], + params: { + minOutputAmount: ethers.utils.parseUnits("1", 18).toString(), + inputToken: TOKEN_SYMBOLS_MAP.USDbC.addresses[CHAIN_IDs.BASE], + originChainId: CHAIN_IDs.BASE, + outputToken: "0x74885b4D524d497261259B38900f54e6dbAd2210", // APE Coin + destinationChainId: CHAIN_IDs.ARBITRUM, + depositor, + }, }, ]; async function swap() { - for (const testCase of MIN_OUTPUT_CASES) { - console.log("\nTest case:", testCase.label); + const filterString = process.argv[2]; + const testCases = [...MIN_OUTPUT_CASES, ...EXACT_OUTPUT_CASES]; + const labelsToFilter = filterString ? filterString.split(",") : []; + const filteredTestCases = testCases.filter((testCase) => { + const matches = labelsToFilter.filter((label) => + testCase.labels + .map((label) => label.toLowerCase()) + .includes(label.toLowerCase()) + ); + return matches.length === labelsToFilter.length; + }); + for (const testCase of filteredTestCases) { + console.log("\nTest case:", testCase.labels.join(" ")); const response = await axios.get(`http://localhost:3000/api/swap`, { - params: testCase, + params: testCase.params, }); console.log(response.data); if (process.env.DEV_WALLET_PK) { const wallet = new Wallet(process.env.DEV_WALLET_PK!).connect( - getProvider(testCase.originChainId) + getProvider(testCase.params.originChainId) ); try { const tx = await wallet.sendTransaction({ diff --git a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json index fe47510b2..0ca1aaec2 100644 --- a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json +++ b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json @@ -15,10 +15,10 @@ "42161": "0x81C7601ac0c5825e89F967f9222B977CCD78aD77" }, "uniswap": { - "10": "0x04989eaF03547E6583f9d9e42aeD11D2b78A808b", - "137": "0xa55490E20057BD4775618D0FC8D51F59f602FED0", - "8453": "0xed8b9c9aE7aCEf12eb4650d26Eb876005a4752d2", - "42161": "0x2414A759d4EFF700Ad81e257Ab5187d07eCeEbAb" + "10": "0x28249CD61170Ed200Db8988D7f69e21331AFFBD0", + "137": "0xC2dCB88873E00c9d401De2CBBa4C6A28f8A6e2c2", + "8453": "0xef5a54BEeBDB4Ee98E2906e30e65AD53dA8D2b17", + "42161": "0x07b64BE6906A78fac5FaB47cD58d59D0A093B15e" } }, "routes": [ From 3efbc6a763a2cbc66f3b2015a3e69137e7c1e273 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp <51655063+gsteenkamp89@users.noreply.github.com> Date: Fri, 22 Nov 2024 11:46:48 +0200 Subject: [PATCH 07/47] Feat/support arb tokens on coingecko endpoint (#1278) * add utility for parsing query params with coercion for non string values * extend api to accept tokenAdress & chainId * bump sdk version * bump sdk, fix build error * small refactor * update sdk --- api/_constants.ts | 4 +- api/_utils.ts | 36 ++- api/coingecko.ts | 47 ++-- package.json | 2 +- yarn.lock | 631 ++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 646 insertions(+), 74 deletions(-) diff --git a/api/_constants.ts b/api/_constants.ts index 967368a3c..eb2377630 100644 --- a/api/_constants.ts +++ b/api/_constants.ts @@ -110,8 +110,8 @@ defaultRelayerFeeCapitalCostConfig["USDB"] = { ...defaultRelayerFeeCapitalCostConfig["DAI"], }; -export const coinGeckoAssetPlatformLookup: Record = { - "0x4200000000000000000000000000000000000042": "optimistic-ethereum", +export const coinGeckoAssetPlatformLookup: Record = { + "0x4200000000000000000000000000000000000042": CHAIN_IDs.OPTIMISM, }; export const defaultRelayerAddressOverride: { diff --git a/api/_utils.ts b/api/_utils.ts index 3cb83e825..53b8fc866 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -23,7 +23,17 @@ import { utils, Signer, } from "ethers"; -import { define } from "superstruct"; +import { + assert, + coerce, + create, + define, + Infer, + integer, + min, + string, + Struct, +} from "superstruct"; import enabledMainnetRoutesAsJson from "../src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json"; import enabledSepoliaRoutesAsJson from "../src/data/routes_11155111_0x14224e63716afAcE30C9a417E0542281869f7d9e.json"; @@ -36,7 +46,7 @@ import { } from "./_abis"; import { BatchAccountBalanceResponse } from "./batch-account-balance"; import { StaticJsonRpcProvider } from "@ethersproject/providers"; -import { VercelResponse } from "@vercel/node"; +import { VercelRequestQuery, VercelResponse } from "@vercel/node"; import { BLOCK_TAG_LAG, CHAIN_IDs, @@ -1395,6 +1405,22 @@ export function applyMapFilter( }, []); } +// superstruct first coerces, then validates +export const positiveInt = coerce(min(integer(), 0), string(), (value) => + Number(value) +); + +// parses, coerces and validates query params +// first coerce any fields that can be coerced, then validate +export function parseQuery< + Q extends VercelRequestQuery, + S extends Struct, +>(query: Q, schema: S): Infer { + const coerced = create(query, schema); + assert(coerced, schema); + return coerced; +} + /* ------------------------- superstruct validators ------------------------- */ export function parsableBigNumberString() { @@ -2120,9 +2146,9 @@ export function getCachedFillGasUsage( const { nativeGasCost } = await relayerFeeCalculatorQueries.getGasCosts( buildDepositForSimulation(deposit), overrides?.relayerAddress, - undefined, - undefined, - true + { + omitMarkup: true, + } ); return nativeGasCost; }; diff --git a/api/coingecko.ts b/api/coingecko.ts index 681357d43..cd611afd2 100644 --- a/api/coingecko.ts +++ b/api/coingecko.ts @@ -1,6 +1,6 @@ import { VercelResponse } from "@vercel/node"; import { ethers } from "ethers"; -import { object, assert, Infer, optional, string, pattern } from "superstruct"; +import { object, Infer, optional, string, pattern } from "superstruct"; import { TypedVercelRequest } from "./_types"; import { getLogger, @@ -8,6 +8,8 @@ import { validAddress, getBalancerV2TokenPrice, getCachedTokenPrice, + parseQuery, + positiveInt, } from "./_utils"; import { CHAIN_IDs, @@ -28,7 +30,9 @@ const { } = process.env; const CoingeckoQueryParamsSchema = object({ - l1Token: validAddress(), + l1Token: optional(validAddress()), + tokenAddress: optional(validAddress()), + chainId: optional(positiveInt), baseCurrency: optional(string()), date: optional(pattern(string(), /\d{2}-\d{2}-\d{4}/)), }); @@ -46,13 +50,25 @@ const handler = async ( query, }); try { - assert(query, CoingeckoQueryParamsSchema); - - let { l1Token, baseCurrency, date: dateStr } = query; + let { + l1Token, + tokenAddress, + chainId, + baseCurrency, + date: dateStr, + } = parseQuery(query, CoingeckoQueryParamsSchema); + + let address = l1Token ?? tokenAddress; + if (!address) { + throw new InvalidParamError({ + message: `Token Address is undefined. You must define either "l1Token", or "tokenAddress"`, + param: "l1Token, tokenAddress", + }); + } // Format the params for consistency baseCurrency = (baseCurrency ?? "eth").toLowerCase(); - l1Token = ethers.utils.getAddress(l1Token); + address = ethers.utils.getAddress(address); // Confirm that the base Currency is supported by Coingecko const isDerivedCurrency = SUPPORTED_CG_DERIVED_CURRENCIES.has(baseCurrency); @@ -74,8 +90,8 @@ const handler = async ( // Perform a 1-deep lookup to see if the provided l1Token is // to be "redirected" to another provided token contract address - if (redirectLookupAddresses[l1Token]) { - l1Token = redirectLookupAddresses[l1Token]; + if (redirectLookupAddresses[address]) { + address = redirectLookupAddresses[address]; } const coingeckoClient = Coingecko.get( @@ -90,9 +106,9 @@ const handler = async ( BALANCER_V2_TOKENS ?? "[]" ).map(ethers.utils.getAddress); - const platformId = coinGeckoAssetPlatformLookup[l1Token] ?? "ethereum"; + chainId = coinGeckoAssetPlatformLookup[address] ?? chainId ?? 1; - if (balancerV2PoolTokens.includes(ethers.utils.getAddress(l1Token))) { + if (balancerV2PoolTokens.includes(ethers.utils.getAddress(address))) { if (dateStr) { throw new InvalidParamError({ message: "Historical price not supported for BalancerV2 tokens", @@ -100,7 +116,7 @@ const handler = async ( }); } if (baseCurrency === "usd") { - price = await getBalancerV2TokenPrice(l1Token); + price = await getBalancerV2TokenPrice(address); } else { throw new InvalidParamError({ message: "Only CG base currency allowed for BalancerV2 tokens is usd", @@ -116,15 +132,16 @@ const handler = async ( const modifiedBaseCurrency = isDerivedCurrency ? "usd" : baseCurrency; if (dateStr) { price = await coingeckoClient.getContractHistoricDayPrice( - l1Token, + address, dateStr, - modifiedBaseCurrency + modifiedBaseCurrency, + chainId ); } else { [, price] = await coingeckoClient.getCurrentPriceByContract( - l1Token, + address, modifiedBaseCurrency, - platformId + chainId ); } } diff --git a/package.json b/package.json index 783b96e90..196d4b287 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@across-protocol/constants": "^3.1.16", "@across-protocol/contracts": "^3.0.11", "@across-protocol/contracts-v3.0.6": "npm:@across-protocol/contracts@3.0.6", - "@across-protocol/sdk": "^3.2.9", + "@across-protocol/sdk": "^3.3.18", "@amplitude/analytics-browser": "^2.3.5", "@balancer-labs/sdk": "1.1.6-beta.16", "@emotion/react": "^11.13.0", diff --git a/yarn.lock b/yarn.lock index 95f62d966..62c83c776 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21,6 +21,11 @@ resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.16.tgz#c126085d29d4d051fd02a04c833d804d37c3c219" integrity sha512-+U+AecGWnfY4b4sSfKBvsDj/+yXKEqpTXcZgI8GVVmUTkUhs1efA0kN4q3q10yy5TXI5TtagaG7R9yZg1zgKKg== +"@across-protocol/constants@^3.1.19": + version "3.1.20" + resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.20.tgz#305bd41f5644b7db5d9fd12a6a6b4bbbbe2fd016" + integrity sha512-B5RsvuOQsZdFgLk0WcFZGmoivm6g6gv95a+YKVBydcxZkNxAsyP065UQEDAmvRXvPhqGyehhd52515Xa/3bzyg== + "@across-protocol/contracts-v3.0.6@npm:@across-protocol/contracts@3.0.6": version "3.0.6" resolved "https://registry.yarnpkg.com/@across-protocol/contracts/-/contracts-3.0.6.tgz#7508fc52db50cfa670edc7c4e9e615ad25cdd56c" @@ -70,14 +75,42 @@ axios "^1.7.4" zksync-web3 "^0.14.3" -"@across-protocol/sdk@^3.2.9": - version "3.2.9" - resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-3.2.9.tgz#7a390889572c033f1dc8db96234bb41b0b2f5090" - integrity sha512-EdSZDLGxHyKmKhLNFpMqUr3rty910VV4nJ1nZfKAMH0HGg8l4tDjFReGYu7cQTyeSSwc04ngmjbvErQPb91k9g== +"@across-protocol/contracts@^3.0.16": + version "3.0.16" + resolved "https://registry.yarnpkg.com/@across-protocol/contracts/-/contracts-3.0.16.tgz#22eb0c1dcdb01e8ca504dc2351d46513d9f71cc6" + integrity sha512-vwg+PmWaenlrx7kTHZdjDTTj1PwXWFU3rMlFyfKM8xBXbPWhIfMQCKCYOwFrGmZw2nRTYgoyhoKN/f6rUs/snw== + dependencies: + "@across-protocol/constants" "^3.1.19" + "@coral-xyz/anchor" "^0.30.1" + "@defi-wonderland/smock" "^2.3.4" + "@eth-optimism/contracts" "^0.5.40" + "@ethersproject/abstract-provider" "5.7.0" + "@ethersproject/abstract-signer" "5.7.0" + "@ethersproject/bignumber" "5.7.0" + "@openzeppelin/contracts" "4.9.6" + "@openzeppelin/contracts-upgradeable" "4.9.6" + "@scroll-tech/contracts" "^0.1.0" + "@solana-developers/helpers" "^2.4.0" + "@solana/spl-token" "^0.4.6" + "@solana/web3.js" "^1.31.0" + "@types/yargs" "^17.0.33" + "@uma/common" "^2.34.0" + "@uma/contracts-node" "^0.4.17" + "@uma/core" "^2.56.0" + axios "^1.7.4" + bs58 "^6.0.0" + prettier-plugin-rust "^0.1.9" + yargs "^17.7.2" + zksync-web3 "^0.14.3" + +"@across-protocol/sdk@^3.3.18": + version "3.3.18" + resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-3.3.18.tgz#d39ef359f9f639921fb412a1355167354014a80f" + integrity sha512-Ea40yDPL94T3uc6HhqDj8X7vovPSyOVSmA6Z3C1uZmdwRdDKt8hlg8k7yxIg+8aR5aEJJ7hCZy6bHdI5XHpbFQ== dependencies: "@across-protocol/across-token" "^1.0.0" - "@across-protocol/constants" "^3.1.16" - "@across-protocol/contracts" "^3.0.11" + "@across-protocol/constants" "^3.1.19" + "@across-protocol/contracts" "^3.0.16" "@eth-optimism/sdk" "^3.3.1" "@ethersproject/bignumber" "^5.7.0" "@pinata/sdk" "^2.1.0" @@ -93,6 +126,7 @@ lodash.get "^4.4.2" superstruct "^0.15.4" tslib "^2.6.2" + viem "^2.21.15" "@adraffy/ens-normalize@1.10.0": version "1.10.0" @@ -104,6 +138,11 @@ resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.9.4.tgz#aae21cb858bbb0411949d5b7b3051f4209043f62" integrity sha512-UK0bHA7hh9cR39V+4gl2/NnBBjoXIxkuWAPCaY4X7fbH4L/azIi7ilWOCjMUYfpJgraLUAqkRi2BqrjME8Rynw== +"@adraffy/ens-normalize@^1.10.1": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz#42cc67c5baa407ac25059fcd7d405cc5ecdb0c33" + integrity sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg== + "@amplitude/ampli@^1.34.0": version "1.34.0" resolved "https://registry.yarnpkg.com/@amplitude/ampli/-/ampli-1.34.0.tgz#878bdeddc392ff24d707f8137834f9213098dd8e" @@ -1310,6 +1349,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.25.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.15", "@babel/template@^7.22.5", "@babel/template@^7.3.3": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" @@ -1404,6 +1450,40 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== +"@coral-xyz/anchor-errors@^0.30.1": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor-errors/-/anchor-errors-0.30.1.tgz#bdfd3a353131345244546876eb4afc0e125bec30" + integrity sha512-9Mkradf5yS5xiLWrl9WrpjqOrAV+/W2RQHDlbnAZBivoGpOs1ECjoDCkVk4aRG8ZdiFiB8zQEVlxf+8fKkmSfQ== + +"@coral-xyz/anchor@^0.30.1": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.30.1.tgz#17f3e9134c28cd0ea83574c6bab4e410bcecec5d" + integrity sha512-gDXFoF5oHgpriXAaLpxyWBHdCs8Awgf/gLHIo6crv7Aqm937CNdY+x+6hoj7QR5vaJV7MxWSQ0NGFzL3kPbWEQ== + dependencies: + "@coral-xyz/anchor-errors" "^0.30.1" + "@coral-xyz/borsh" "^0.30.1" + "@noble/hashes" "^1.3.1" + "@solana/web3.js" "^1.68.0" + bn.js "^5.1.2" + bs58 "^4.0.1" + buffer-layout "^1.2.2" + camelcase "^6.3.0" + cross-fetch "^3.1.5" + crypto-hash "^1.3.0" + eventemitter3 "^4.0.7" + pako "^2.0.3" + snake-case "^3.0.4" + superstruct "^0.15.4" + toml "^3.0.0" + +"@coral-xyz/borsh@^0.30.1": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.30.1.tgz#869d8833abe65685c72e9199b8688477a4f6b0e3" + integrity sha512-aaxswpPrCFKl8vZTbxLssA2RvwX2zmKLlRCIktJOwW+VpVwYtXRtlWiIP+c2pPRKneiTiWCN2GEMSH9j1zTlWQ== + dependencies: + bn.js "^5.1.2" + buffer-layout "^1.2.0" + "@dabh/diagnostics@^2.0.2": version "2.0.3" resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" @@ -2294,7 +2374,7 @@ bufio "^1.0.7" chai "^4.3.4" -"@eth-optimism/core-utils@0.13.2", "@eth-optimism/core-utils@^0.13.2": +"@eth-optimism/core-utils@^0.13.2": version "0.13.2" resolved "https://registry.yarnpkg.com/@eth-optimism/core-utils/-/core-utils-0.13.2.tgz#c0187c3abf6d86dad039edf04ff81299253214fe" integrity sha512-u7TOKm1RxH1V5zw7dHmfy91bOuEAZU68LT/9vJPkuWEjaTl+BgvPDRDTurjzclHzN0GbWdcpOqPZg4ftjkJGaw== @@ -3805,6 +3885,13 @@ dependencies: "@noble/hashes" "1.3.3" +"@noble/curves@1.6.0", "@noble/curves@^1.4.0", "@noble/curves@^1.4.2", "@noble/curves@^1.6.0", "@noble/curves@~1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.6.0.tgz#be5296ebcd5a1730fccea4786d420f87abfeb40b" + integrity sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ== + dependencies: + "@noble/hashes" "1.5.0" + "@noble/hashes@1.2.0", "@noble/hashes@~1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" @@ -3820,6 +3907,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== +"@noble/hashes@1.5.0", "@noble/hashes@^1.3.1", "@noble/hashes@^1.5.0", "@noble/hashes@~1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" + integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA== + "@noble/hashes@^1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" @@ -4856,6 +4948,11 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d" integrity sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g== +"@scure/base@~1.1.7", "@scure/base@~1.1.8": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1" + integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg== + "@scure/bip32@1.1.5": version "1.1.5" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.1.5.tgz#d2ccae16dcc2e75bc1d75f5ef3c66a338d1ba300" @@ -4883,6 +4980,15 @@ "@noble/hashes" "~1.3.2" "@scure/base" "~1.1.4" +"@scure/bip32@1.5.0", "@scure/bip32@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.5.0.tgz#dd4a2e1b8a9da60e012e776d954c4186db6328e6" + integrity sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw== + dependencies: + "@noble/curves" "~1.6.0" + "@noble/hashes" "~1.5.0" + "@scure/base" "~1.1.7" + "@scure/bip39@1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.1.tgz#b54557b2e86214319405db819c4b6a370cf340c5" @@ -4907,6 +5013,14 @@ "@noble/hashes" "~1.3.2" "@scure/base" "~1.1.4" +"@scure/bip39@1.4.0", "@scure/bip39@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.4.0.tgz#664d4f851564e2e1d4bffa0339f9546ea55960a6" + integrity sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw== + dependencies: + "@noble/hashes" "~1.5.0" + "@scure/base" "~1.1.8" + "@sentry/browser@7.37.2": version "7.37.2" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.37.2.tgz#355dd28ad12677d63e0b12c5209d12b3f98ac3a4" @@ -5181,6 +5295,143 @@ lodash.union "^4.6.0" lodash.values "^4.3.0" +"@solana-developers/helpers@^2.4.0": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@solana-developers/helpers/-/helpers-2.5.6.tgz#2af7613ea6848ce087c0dec7cf38e6f172abcbd4" + integrity sha512-NPWZblVMl4LuVVSJOZG0ZF0VYnrMUjCyMNTiGwNUXPK2WWYJCqpuDyzs/PMqwvM4gMTjk4pEToBX8N2UxDvZkQ== + dependencies: + "@solana/spl-token" "^0.4.8" + "@solana/spl-token-metadata" "^0.1.4" + "@solana/web3.js" "^1.95.2" + bs58 "^6.0.0" + dotenv "^16.4.5" + +"@solana/buffer-layout-utils@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@solana/buffer-layout-utils/-/buffer-layout-utils-0.2.0.tgz#b45a6cab3293a2eb7597cceb474f229889d875ca" + integrity sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g== + dependencies: + "@solana/buffer-layout" "^4.0.0" + "@solana/web3.js" "^1.32.0" + bigint-buffer "^1.1.5" + bignumber.js "^9.0.1" + +"@solana/buffer-layout@^4.0.0", "@solana/buffer-layout@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz#b996235eaec15b1e0b5092a8ed6028df77fa6c15" + integrity sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA== + dependencies: + buffer "~6.0.3" + +"@solana/codecs-core@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs-core/-/codecs-core-2.0.0-rc.1.tgz#1a2d76b9c7b9e7b7aeb3bd78be81c2ba21e3ce22" + integrity sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ== + dependencies: + "@solana/errors" "2.0.0-rc.1" + +"@solana/codecs-data-structures@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-rc.1.tgz#d47b2363d99fb3d643f5677c97d64a812982b888" + integrity sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/codecs-numbers" "2.0.0-rc.1" + "@solana/errors" "2.0.0-rc.1" + +"@solana/codecs-numbers@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs-numbers/-/codecs-numbers-2.0.0-rc.1.tgz#f34978ddf7ea4016af3aaed5f7577c1d9869a614" + integrity sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/errors" "2.0.0-rc.1" + +"@solana/codecs-strings@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs-strings/-/codecs-strings-2.0.0-rc.1.tgz#e1d9167075b8c5b0b60849f8add69c0f24307018" + integrity sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/codecs-numbers" "2.0.0-rc.1" + "@solana/errors" "2.0.0-rc.1" + +"@solana/codecs@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/codecs/-/codecs-2.0.0-rc.1.tgz#146dc5db58bd3c28e04b4c805e6096c2d2a0a875" + integrity sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/codecs-data-structures" "2.0.0-rc.1" + "@solana/codecs-numbers" "2.0.0-rc.1" + "@solana/codecs-strings" "2.0.0-rc.1" + "@solana/options" "2.0.0-rc.1" + +"@solana/errors@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/errors/-/errors-2.0.0-rc.1.tgz#3882120886eab98a37a595b85f81558861b29d62" + integrity sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ== + dependencies: + chalk "^5.3.0" + commander "^12.1.0" + +"@solana/options@2.0.0-rc.1": + version "2.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@solana/options/-/options-2.0.0-rc.1.tgz#06924ba316dc85791fc46726a51403144a85fc4d" + integrity sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA== + dependencies: + "@solana/codecs-core" "2.0.0-rc.1" + "@solana/codecs-data-structures" "2.0.0-rc.1" + "@solana/codecs-numbers" "2.0.0-rc.1" + "@solana/codecs-strings" "2.0.0-rc.1" + "@solana/errors" "2.0.0-rc.1" + +"@solana/spl-token-group@^0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@solana/spl-token-group/-/spl-token-group-0.0.7.tgz#83c00f0cd0bda33115468cd28b89d94f8ec1fee4" + integrity sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug== + dependencies: + "@solana/codecs" "2.0.0-rc.1" + +"@solana/spl-token-metadata@^0.1.4", "@solana/spl-token-metadata@^0.1.6": + version "0.1.6" + resolved "https://registry.yarnpkg.com/@solana/spl-token-metadata/-/spl-token-metadata-0.1.6.tgz#d240947aed6e7318d637238022a7b0981b32ae80" + integrity sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA== + dependencies: + "@solana/codecs" "2.0.0-rc.1" + +"@solana/spl-token@^0.4.6", "@solana/spl-token@^0.4.8": + version "0.4.9" + resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.4.9.tgz#24d032d2935f237925c3b058ba6bb1e1ece5428c" + integrity sha512-g3wbj4F4gq82YQlwqhPB0gHFXfgsC6UmyGMxtSLf/BozT/oKd59465DbnlUK8L8EcimKMavxsVAMoLcEdeCicg== + dependencies: + "@solana/buffer-layout" "^4.0.0" + "@solana/buffer-layout-utils" "^0.2.0" + "@solana/spl-token-group" "^0.0.7" + "@solana/spl-token-metadata" "^0.1.6" + buffer "^6.0.3" + +"@solana/web3.js@^1.31.0", "@solana/web3.js@^1.32.0", "@solana/web3.js@^1.68.0", "@solana/web3.js@^1.95.2": + version "1.95.5" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.95.5.tgz#ba70da4c205c64249ed94369fe2d617c0347cd85" + integrity sha512-hU9cBrbg1z6gEjLH9vwIckGBVB78Ijm0iZFNk4ocm5OD82piPwuk3MeQ1rfiKD9YQtr95krrcaopb49EmQJlRg== + dependencies: + "@babel/runtime" "^7.25.0" + "@noble/curves" "^1.4.2" + "@noble/hashes" "^1.4.0" + "@solana/buffer-layout" "^4.0.1" + agentkeepalive "^4.5.0" + bigint-buffer "^1.1.5" + bn.js "^5.2.1" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.3" + fast-stable-stringify "^1.0.0" + jayson "^4.1.1" + node-fetch "^2.7.0" + rpc-websockets "^9.0.2" + superstruct "^2.0.2" + "@solidity-parser/parser@^0.14.0": version "0.14.5" resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.14.5.tgz#87bc3cc7b068e08195c219c91cd8ddff5ef1a804" @@ -6110,6 +6361,13 @@ "@svgr/hast-util-to-babel-ast" "^7.0.0" svg-parser "^2.0.4" +"@swc/helpers@^0.5.11": + version "0.5.15" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7" + integrity sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g== + dependencies: + tslib "^2.8.0" + "@synthetixio/ethereum-wallet-mock@0.0.1-alpha.7": version "0.0.1-alpha.7" resolved "https://registry.yarnpkg.com/@synthetixio/ethereum-wallet-mock/-/ethereum-wallet-mock-0.0.1-alpha.7.tgz#313abfab9176d9da71ec69c40a96fbcd334f6a42" @@ -6590,6 +6848,13 @@ dependencies: "@types/node" "*" +"@types/connect@^3.4.33": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + "@types/cross-spawn@^6.0.2": version "6.0.4" resolved "https://registry.yarnpkg.com/@types/cross-spawn/-/cross-spawn-6.0.4.tgz#e658d29e2308a01f48b7b30fd8cdf07aeb2e5a82" @@ -6896,7 +7161,7 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== -"@types/node@^12.0.0", "@types/node@^12.12.6": +"@types/node@^12.0.0", "@types/node@^12.12.54", "@types/node@^12.12.6": version "12.20.55" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== @@ -7096,11 +7361,30 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/uuid@^8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + "@types/webpack-env@^1.17.0": version "1.18.0" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.18.0.tgz#ed6ecaa8e5ed5dfe8b2b3d00181702c9925f13fb" integrity sha512-56/MAlX5WMsPVbOg7tAxnYvNYMMWr/QJiIp6BxVSW3JJXUVzzOn64qW8TzQyMSqSUFM2+PVI4aUHcHOzIz/1tg== +"@types/ws@^7.4.4": + version "7.4.7" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" + integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== + dependencies: + "@types/node" "*" + +"@types/ws@^8.2.2": + version "8.5.13" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.13.tgz#6414c280875e2691d0d1e080b05addbf5cb91e20" + integrity sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA== + dependencies: + "@types/node" "*" + "@types/ws@^8.5.5": version "8.5.5" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.5.tgz#af587964aa06682702ee6dcbc7be41a80e4b28eb" @@ -7120,6 +7404,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^17.0.33": + version "17.0.33" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.33.tgz#8c32303da83eec050a84b3c7ae7b9f922d13e32d" + integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== + dependencies: + "@types/yargs-parser" "*" + "@types/yargs@^17.0.8": version "17.0.24" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.24.tgz#b3ef8d50ad4aa6aecf6ddc97c580a00f5aa11902" @@ -8256,6 +8547,14 @@ resolved "https://registry.yarnpkg.com/@zxing/text-encoding/-/text-encoding-0.9.0.tgz#fb50ffabc6c7c66a0c96b4c03e3d9be74864b70b" integrity sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA== +JSONStream@^1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" + integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + abab@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" @@ -8288,6 +8587,11 @@ abitype@1.0.0: resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.0.tgz#237176dace81d90d018bebf3a45cb42f2a2d9e97" integrity sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ== +abitype@1.0.6, abitype@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.6.tgz#76410903e1d88e34f1362746e2d407513c38565b" + integrity sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A== + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -8399,6 +8703,13 @@ agent-base@^7.0.2: dependencies: debug "^4.3.4" +agentkeepalive@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" + integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== + dependencies: + humanize-ms "^1.2.1" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -9717,6 +10028,11 @@ base-x@^4.0.0: resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a" integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw== +base-x@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-5.0.0.tgz#6d835ceae379130e1a4cb846a70ac4746f28ea9b" + integrity sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ== + base64-js@^1.1.2, base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -9784,6 +10100,13 @@ big.js@^6.0.3: resolved "https://registry.yarnpkg.com/big.js/-/big.js-6.2.1.tgz#7205ce763efb17c2e41f26f121c420c6a7c2744f" integrity sha512-bCtHMwL9LeDIozFn+oNhhFoq+yQ3BNdnsLSASUxLciOb1vgvpHsIO1dsENiGMgbb4SkP5TrzWzRiLddn8ahVOQ== +bigint-buffer@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/bigint-buffer/-/bigint-buffer-1.1.5.tgz#d038f31c8e4534c1f8d0015209bf34b4fa6dd442" + integrity sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA== + dependencies: + bindings "^1.3.0" + bignumber.js@*, bignumber.js@^9.0.0, bignumber.js@^9.0.1, bignumber.js@^9.0.2: version "9.1.2" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" @@ -9817,7 +10140,7 @@ binary@~0.3.0: buffers "~0.1.1" chainsaw "~0.1.0" -bindings@^1.5.0: +bindings@^1.3.0, bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== @@ -9933,6 +10256,15 @@ borc@^2.1.2: json-text-sequence "~0.1.0" readable-stream "^3.6.0" +borsh@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/borsh/-/borsh-0.7.0.tgz#6e9560d719d86d90dc589bca60ffc8a6c51fec2a" + integrity sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA== + dependencies: + bn.js "^5.2.0" + bs58 "^4.0.0" + text-encoding-utf-8 "^1.0.2" + bowser@^2.11.0: version "2.11.0" resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" @@ -10140,6 +10472,13 @@ bs58@^5.0.0: dependencies: base-x "^4.0.0" +bs58@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-6.0.0.tgz#a2cda0130558535dd281a2f8697df79caaf425d8" + integrity sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw== + dependencies: + base-x "^5.0.0" + bs58check@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" @@ -10199,6 +10538,11 @@ buffer-indexof-polyfill@~1.0.0: resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz#d2732135c5999c64b277fcf9b1abe3498254729c" integrity sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A== +buffer-layout@^1.2.0, buffer-layout@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/buffer-layout/-/buffer-layout-1.2.2.tgz#b9814e7c7235783085f9ca4966a0cfff112259d5" + integrity sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA== + buffer-reverse@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-reverse/-/buffer-reverse-1.0.1.tgz#49283c8efa6f901bc01fa3304d06027971ae2f60" @@ -10214,7 +10558,7 @@ buffer-xor@^1.0.3: resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ== -buffer@6.0.3, buffer@^6.0.1, buffer@^6.0.3: +buffer@6.0.3, buffer@^6.0.1, buffer@^6.0.3, buffer@~6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== @@ -10393,7 +10737,7 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -camelcase@^6.0.0, camelcase@^6.2.0: +camelcase@^6.0.0, camelcase@^6.2.0, camelcase@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== @@ -11086,7 +11430,12 @@ commander@^10.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== -commander@^2.11.0, commander@^2.15.0, commander@^2.19.0: +commander@^12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + +commander@^2.11.0, commander@^2.15.0, commander@^2.19.0, commander@^2.20.3: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -11367,6 +11716,13 @@ cross-fetch@^3.0.6, cross-fetch@^3.1.4: dependencies: node-fetch "2.6.7" +cross-fetch@^3.1.5: + version "3.1.8" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" + integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== + dependencies: + node-fetch "^2.6.12" + cross-fetch@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" @@ -11434,6 +11790,11 @@ crypto-es@^1.2.2: resolved "https://registry.yarnpkg.com/crypto-es/-/crypto-es-1.2.7.tgz#754a6d52319a94fb4eb1f119297f17196b360f88" integrity sha512-UUqiVJ2gUuZFmbFsKmud3uuLcNP2+Opt+5ysmljycFCyhA0+T16XJmo1ev/t5kMChMqWh7IEvURNCqsg+SjZGQ== +crypto-hash@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247" + integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg== + crypto-js@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" @@ -11757,6 +12118,11 @@ del@^6.0.0: rimraf "^3.0.2" slash "^3.0.0" +delay@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" + integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -11965,6 +12331,14 @@ dot-case@^2.1.0: dependencies: no-case "^2.2.0" +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + dot-prop@^5.0.0: version "5.3.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" @@ -12424,11 +12798,18 @@ es6-iterator@^2.0.3: es5-ext "^0.10.35" es6-symbol "^3.1.1" -es6-promise@4.2.8, es6-promise@^4.2.8: +es6-promise@4.2.8, es6-promise@^4.0.3, es6-promise@^4.2.8: version "4.2.8" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ== + dependencies: + es6-promise "^4.0.3" + es6-symbol@^3.1.1, es6-symbol@^3.1.3: version "3.1.4" resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.4.tgz#f4e7d28013770b4208ecbf3e0bf14d3bcb557b8c" @@ -13405,16 +13786,16 @@ eventemitter3@4.0.4: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384" integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ== +eventemitter3@5.0.1, eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + eventemitter3@^4.0.0, eventemitter3@^4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -eventemitter3@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" - integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== - eventid@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/eventid/-/eventid-2.0.1.tgz#574e860149457a79a2efe788c459f0c3062d02ec" @@ -13637,6 +14018,11 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== +eyes@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" + integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ== + fake-merkle-patricia-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/fake-merkle-patricia-tree/-/fake-merkle-patricia-tree-1.0.1.tgz#4b8c3acfb520afadf9860b1f14cd8ce3402cddd3" @@ -13692,6 +14078,11 @@ fast-safe-stringify@^2.0.6: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== +fast-stable-stringify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz#5c5543462b22aeeefd36d05b34e51c78cb86d313" + integrity sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag== + fast-text-encoding@^1.0.0, fast-text-encoding@^1.0.3: version "1.0.6" resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" @@ -15324,6 +15715,13 @@ human-signals@^4.3.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ== +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + husky@^8.0.0: version "8.0.1" resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.1.tgz#511cb3e57de3e3190514ae49ed50f6bc3f50b3e9" @@ -16281,6 +16679,11 @@ isomorphic-ws@5.0.0: resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== +isomorphic-ws@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" + integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== + isows@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.3.tgz#93c1cf0575daf56e7120bab5c8c448b0809d0d74" @@ -16291,6 +16694,11 @@ isows@1.0.4: resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.4.tgz#810cd0d90cc4995c26395d2aa4cfa4037ebdf061" integrity sha512-hEzjY+x9u9hPmBom9IIAqdJCwNLax+xrPb51vEPpERoFlIxgmZcHzsT5jKG06nvInKOBGvReAVz80Umed5CczQ== +isows@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.6.tgz#0da29d706fa51551c663c627ace42769850f86e7" + integrity sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw== + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -16455,6 +16863,24 @@ jake@^10.8.5: filelist "^1.0.4" minimatch "^3.1.2" +jayson@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/jayson/-/jayson-4.1.2.tgz#443c26a8658703e0b2e881117b09395d88b6982e" + integrity sha512-5nzMWDHy6f+koZOuYsArh2AXs73NfWYVlFyJJuCedr93GpY+Ku8qq10ropSXVfHK+H0T6paA88ww+/dV+1fBNA== + dependencies: + "@types/connect" "^3.4.33" + "@types/node" "^12.12.54" + "@types/ws" "^7.4.4" + JSONStream "^1.3.5" + commander "^2.20.3" + delay "^5.0.0" + es6-promisify "^5.0.0" + eyes "^0.1.8" + isomorphic-ws "^4.0.1" + json-stringify-safe "^5.0.1" + uuid "^8.3.2" + ws "^7.5.10" + jest-changed-files@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.5.0.tgz#e88786dca8bf2aa899ec4af7644e16d9dcf9b23e" @@ -16868,6 +17294,11 @@ jest@^29.5.0: import-local "^3.0.2" jest-cli "^29.5.0" +jinx-rust@0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/jinx-rust/-/jinx-rust-0.1.6.tgz#c7bce55d97bfbad76a9b930c01fe6a8629a170d7" + integrity sha512-qP+wtQL1PrDDFwtPKhNGtjWOmijCrKdfUHWTV2G/ikxfjrh+cjdvkQTmny9RAsVF0jiui9m+F0INWu4cuRcZeQ== + joi@17.9.1: version "17.9.1" resolved "https://registry.yarnpkg.com/joi/-/joi-17.9.1.tgz#74899b9fa3646904afa984a11df648eca66c9018" @@ -17135,7 +17566,7 @@ json-stable-stringify@^1.0.1: jsonify "^0.0.1" object-keys "^1.1.1" -json-stringify-safe@~5.0.1: +json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== @@ -17197,6 +17628,11 @@ jsonify@^0.0.1: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== +jsonparse@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== + jsonschema@^1.2.4: version "1.4.1" resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.4.1.tgz#cc4c3f0077fb4542982973d8a083b6b34f482dab" @@ -17823,6 +18259,13 @@ lower-case@^1.1.0, lower-case@^1.1.1, lower-case@^1.1.2: resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" integrity sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA== +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" @@ -18463,7 +18906,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.1: +ms@2.1.3, ms@^2.0.0, ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -18756,6 +19199,14 @@ no-case@^2.2.0, no-case@^2.3.2: dependencies: lower-case "^1.1.1" +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + node-addon-api@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" @@ -18807,7 +19258,7 @@ node-fetch@^1.0.1, node-fetch@~1.7.1: encoding "^0.1.11" is-stream "^1.0.1" -node-fetch@^2.0.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7, node-fetch@^2.6.9: +node-fetch@^2.0.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7, node-fetch@^2.6.9, node-fetch@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -19228,6 +19679,19 @@ output-file-sync@^1.1.2: mkdirp "^0.5.1" object-assign "^4.1.0" +ox@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ox/-/ox-0.1.2.tgz#0f791be2ccabeaf4928e6d423498fe1c8094e560" + integrity sha512-ak/8K0Rtphg9vnRJlbOdaX9R7cmxD2MiSthjWGaQdMk3D7hrAlDoM+6Lxn7hN52Za3vrXfZ7enfke/5WjolDww== + dependencies: + "@adraffy/ens-normalize" "^1.10.1" + "@noble/curves" "^1.6.0" + "@noble/hashes" "^1.5.0" + "@scure/bip32" "^1.5.0" + "@scure/bip39" "^1.4.0" + abitype "^1.0.6" + eventemitter3 "5.0.1" + p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" @@ -19327,6 +19791,11 @@ packageurl-js@^1.0.0: resolved "https://registry.yarnpkg.com/packageurl-js/-/packageurl-js-1.0.2.tgz#c568a569848c66be8f2b467ac41b0f1427672b00" integrity sha512-fWC4ZPxo80qlh3xN5FxfIoQD3phVY4+EyzTIqyksjhKNDmaicdpxSvkWwIrYTtv9C1/RcUN6pxaTwGmj2NzS6A== +pako@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== + pako@~0.2.0: version "0.2.9" resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" @@ -19914,12 +20383,20 @@ preserve@^0.2.0: resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks= +prettier-plugin-rust@^0.1.9: + version "0.1.9" + resolved "https://registry.yarnpkg.com/prettier-plugin-rust/-/prettier-plugin-rust-0.1.9.tgz#1a93b035743fa02a006b4980a1035a260ea9e501" + integrity sha512-n1DTTJQaHMdnoG/+nKUvBm3EKsMVWsYES2UPCiOPiZdBrmuAO/pX++m7L3+Hz3uuhtddpH0HRKHB2F3jbtJBOQ== + dependencies: + jinx-rust "0.1.6" + prettier "^2.7.1" + prettier@3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a" integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA== -prettier@^2.8.0: +prettier@^2.7.1, prettier@^2.8.0: version "2.8.8" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== @@ -21310,6 +21787,22 @@ rollup@^4.0.2: "@rollup/rollup-win32-x64-msvc" "4.18.0" fsevents "~2.3.2" +rpc-websockets@^9.0.2: + version "9.0.4" + resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-9.0.4.tgz#9d8ee82533b5d1e13d9ded729e3e38d0d8fa083f" + integrity sha512-yWZWN0M+bivtoNLnaDbtny4XchdAIF5Q4g/ZsC5UC61Ckbp0QczwO8fg44rV3uYmY4WHd+EZQbn90W1d8ojzqQ== + dependencies: + "@swc/helpers" "^0.5.11" + "@types/uuid" "^8.3.4" + "@types/ws" "^8.2.2" + buffer "^6.0.3" + eventemitter3 "^5.0.1" + uuid "^8.3.2" + ws "^8.5.0" + optionalDependencies: + bufferutil "^4.0.1" + utf-8-validate "^5.0.2" + run-async@^2.4.0, run-async@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" @@ -21858,6 +22351,14 @@ snake-case@^2.1.0: dependencies: no-case "^2.2.0" +snake-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -22271,7 +22772,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -22306,15 +22807,6 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -22389,7 +22881,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -22417,13 +22909,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -22534,6 +23019,11 @@ superstruct@^0.15.4: resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.15.4.tgz#e3381dd84ca07e704e19f69eda74eee1a5efb1f9" integrity sha512-eOoMeSbP9ZJChNOm/9RYjE+F36rYR966AAqeG3xhQB02j2sfAUXDp4EQ/7bAOqnlJnuFDB8yvOu50SocvKpUEw== +superstruct@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-2.0.2.tgz#3f6d32fbdc11c357deff127d591a39b996300c54" + integrity sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A== + supports-color@8.1.1, supports-color@^8.0.0, supports-color@^8.1.0: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" @@ -22780,6 +23270,11 @@ testrpc@0.0.1: resolved "https://registry.yarnpkg.com/testrpc/-/testrpc-0.0.1.tgz#83e2195b1f5873aec7be1af8cbe6dcf39edb7aed" integrity sha512-afH1hO+SQ/VPlmaLUFj2636QMeDvPCeQMc/9RBMW0IfjNe9gFD9Ra3ShqYkB7py0do1ZcCna/9acHyzTJ+GcNA== +text-encoding-utf-8@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" + integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== + text-hex@1.0.x: version "1.0.0" resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" @@ -22836,7 +23331,7 @@ through2@^2.0.3: readable-stream "~2.3.6" xtend "~4.0.1" -through@^2.3.6, through@^2.3.8: +"through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== @@ -22991,6 +23486,11 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +toml@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" + integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== + tough-cookie@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2" @@ -23163,6 +23663,11 @@ tslib@^2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tslib@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tsort@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/tsort/-/tsort-0.0.1.tgz#e2280f5e817f8bf4275657fd0f9aebd44f5a2786" @@ -23853,6 +24358,21 @@ viem@^1.0.0: isomorphic-ws "5.0.0" ws "8.13.0" +viem@^2.21.15: + version "2.21.48" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.21.48.tgz#f8f1d0bf5381282e22e6a1f8b72ebd6e64426480" + integrity sha512-/hBHyG1gdIIuiQv0z9YmzXl5eWJa0UCZGwkeuQzH2Bmg6FIEwZeEcxgiytXZydip+p2wMBFa1jdr7o5O1+mrIg== + dependencies: + "@noble/curves" "1.6.0" + "@noble/hashes" "1.5.0" + "@scure/bip32" "1.5.0" + "@scure/bip39" "1.4.0" + abitype "1.0.6" + isows "1.0.6" + ox "0.1.2" + webauthn-p256 "0.0.10" + ws "8.18.0" + vite-plugin-environment@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/vite-plugin-environment/-/vite-plugin-environment-1.1.3.tgz#d01a04abb2f69730a4866c9c9db51d3dab74645b" @@ -24857,6 +25377,14 @@ web3@^1.6.0: web3-shh "1.10.4" web3-utils "1.10.4" +webauthn-p256@0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/webauthn-p256/-/webauthn-p256-0.0.10.tgz#877e75abe8348d3e14485932968edf3325fd2fdd" + integrity sha512-EeYD+gmIT80YkSIDb2iWq0lq2zbHo1CxHlQTeJ+KkCILWpVy3zASH3ByD4bopzfk0uCwXxLqKGLqp2W4O28VFA== + dependencies: + "@noble/curves" "^1.4.0" + "@noble/hashes" "^1.4.0" + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -25087,7 +25615,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -25122,15 +25650,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -25187,6 +25706,11 @@ ws@8.13.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== +ws@8.18.0, ws@^8.5.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + ws@^3.0.0: version "3.3.3" resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" @@ -25215,6 +25739,11 @@ ws@^7.4.6, ws@^7.5.1: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +ws@^7.5.10: + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== + ws@^8.11.0, ws@^8.13.0, ws@^8.2.3: version "8.17.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea" From fc83780df80bf4d701507e3936dd5b908e26888d Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Fri, 22 Nov 2024 17:28:16 +0700 Subject: [PATCH 08/47] feat: add allowance and balance check (#1281) --- api/_dexes/cross-swap.ts | 89 ++++++---- api/_erc20.ts | 49 ++++++ api/_utils.ts | 16 ++ api/swap.ts | 172 ------------------- api/swap/_utils.ts | 125 ++++++++++++++ api/swap/allowance.ts | 102 +++++++++++ scripts/tests/{swap.ts => swap-allowance.ts} | 81 ++++++--- 7 files changed, 398 insertions(+), 236 deletions(-) create mode 100644 api/_erc20.ts delete mode 100644 api/swap.ts create mode 100644 api/swap/_utils.ts create mode 100644 api/swap/allowance.ts rename scripts/tests/{swap.ts => swap-allowance.ts} (67%) diff --git a/api/_dexes/cross-swap.ts b/api/_dexes/cross-swap.ts index 800c0a70d..b33113c30 100644 --- a/api/_dexes/cross-swap.ts +++ b/api/_dexes/cross-swap.ts @@ -1,5 +1,6 @@ import { SpokePool } from "@across-protocol/contracts/dist/typechain"; -import { PopulatedTransaction } from "ethers"; +import { ethers, PopulatedTransaction } from "ethers"; +import { utils } from "@across-protocol/sdk"; import { isRouteEnabled, @@ -7,7 +8,7 @@ import { isOutputTokenBridgeable, getBridgeQuoteForMinOutput, getSpokePool, - latestGasPriceCache, + getSpokePoolVerifier, } from "../_utils"; import { getUniswapCrossSwapQuotesForOutputB2A, @@ -162,40 +163,26 @@ export function getCrossSwapType(params: { return CROSS_SWAP_TYPE.BRIDGEABLE_TO_BRIDGEABLE; } - if (isInputTokenBridgeable(params.inputToken, params.originChainId)) { - return CROSS_SWAP_TYPE.BRIDGEABLE_TO_ANY; - } - if (isOutputTokenBridgeable(params.outputToken, params.destinationChainId)) { return CROSS_SWAP_TYPE.ANY_TO_BRIDGEABLE; } + if (isInputTokenBridgeable(params.inputToken, params.originChainId)) { + return CROSS_SWAP_TYPE.BRIDGEABLE_TO_ANY; + } + return CROSS_SWAP_TYPE.ANY_TO_ANY; } -export async function buildCrossSwapTx( +export async function buildCrossSwapTxForAllowanceHolder( crossSwapQuotes: CrossSwapQuotes, integratorId?: string ) { const originChainId = crossSwapQuotes.crossSwap.inputToken.chainId; - const destinationChainId = crossSwapQuotes.crossSwap.outputToken.chainId; + const isInputNative = crossSwapQuotes.crossSwap.isInputNative; const spokePool = getSpokePool(originChainId); - const deposit = { - depositor: crossSwapQuotes.crossSwap.depositor, - recipient: getMultiCallHandlerAddress(destinationChainId), - inputToken: crossSwapQuotes.bridgeQuote.inputToken.address, - outputToken: crossSwapQuotes.bridgeQuote.outputToken.address, - inputAmount: crossSwapQuotes.bridgeQuote.inputAmount, - outputAmount: crossSwapQuotes.bridgeQuote.outputAmount, - destinationChainid: crossSwapQuotes.bridgeQuote.outputToken.chainId, - exclusiveRelayer: - crossSwapQuotes.bridgeQuote.suggestedFees.exclusiveRelayer, - quoteTimestamp: crossSwapQuotes.bridgeQuote.suggestedFees.timestamp, - fillDeadline: await getFillDeadline(spokePool), - exclusivityDeadline: - crossSwapQuotes.bridgeQuote.suggestedFees.exclusivityDeadline, - message: crossSwapQuotes.bridgeQuote.message || "0x", - }; + const spokePoolVerifier = getSpokePoolVerifier(originChainId); + const deposit = await extractDepositDataStruct(crossSwapQuotes); let tx: PopulatedTransaction; let toAddress: string; @@ -216,10 +203,23 @@ export async function buildCrossSwapTx( } ); toAddress = swapAndBridge.address; - } else { - const spokePool = getSpokePool( - crossSwapQuotes.crossSwap.inputToken.chainId + } else if (isInputNative && spokePoolVerifier) { + tx = await spokePoolVerifier.populateTransaction.deposit( + spokePool.address, + deposit.recipient, + deposit.inputToken, + deposit.inputAmount, + deposit.destinationChainid, + crossSwapQuotes.bridgeQuote.suggestedFees.totalRelayFee.pct, + deposit.quoteTimestamp, + deposit.message, + ethers.constants.MaxUint256, + { + value: deposit.inputAmount, + } ); + toAddress = spokePoolVerifier.address; + } else { tx = await spokePool.populateTransaction.depositV3( deposit.depositor, deposit.recipient, @@ -242,23 +242,38 @@ export async function buildCrossSwapTx( toAddress = spokePool.address; } - const [gas, gasPrice] = await Promise.all([ - spokePool.provider.estimateGas({ - from: crossSwapQuotes.crossSwap.depositor, - ...tx, - }), - latestGasPriceCache(originChainId).get(), - ]); - return { from: crossSwapQuotes.crossSwap.depositor, to: toAddress, data: integratorId ? tagIntegratorId(integratorId, tx.data!) : tx.data, - gas, - gasPrice, value: tx.value, }; } +async function extractDepositDataStruct(crossSwapQuotes: CrossSwapQuotes) { + const originChainId = crossSwapQuotes.crossSwap.inputToken.chainId; + const destinationChainId = crossSwapQuotes.crossSwap.outputToken.chainId; + const spokePool = getSpokePool(originChainId); + const message = crossSwapQuotes.bridgeQuote.message || "0x"; + const deposit = { + depositor: crossSwapQuotes.crossSwap.depositor, + recipient: utils.isMessageEmpty(message) + ? getMultiCallHandlerAddress(destinationChainId) + : crossSwapQuotes.crossSwap.recipient, + inputToken: crossSwapQuotes.bridgeQuote.inputToken.address, + outputToken: crossSwapQuotes.bridgeQuote.outputToken.address, + inputAmount: crossSwapQuotes.bridgeQuote.inputAmount, + outputAmount: crossSwapQuotes.bridgeQuote.outputAmount, + destinationChainid: destinationChainId, + exclusiveRelayer: + crossSwapQuotes.bridgeQuote.suggestedFees.exclusiveRelayer, + quoteTimestamp: crossSwapQuotes.bridgeQuote.suggestedFees.timestamp, + fillDeadline: await getFillDeadline(spokePool), + exclusivityDeadline: + crossSwapQuotes.bridgeQuote.suggestedFees.exclusivityDeadline, + message, + }; + return deposit; +} async function getFillDeadline(spokePool: SpokePool): Promise { const calls = [ diff --git a/api/_erc20.ts b/api/_erc20.ts new file mode 100644 index 000000000..122eeff7f --- /dev/null +++ b/api/_erc20.ts @@ -0,0 +1,49 @@ +import { ERC20__factory } from "@across-protocol/contracts"; +import { callViaMulticall3, getProvider } from "./_utils"; + +export async function getAllowance(params: { + chainId: number; + tokenAddress: string; + owner: string; + spender: string; +}) { + const erc20 = getErc20(params); + return erc20.allowance(params.owner, params.spender); +} + +export async function getBalance(params: { + chainId: number; + tokenAddress: string; + owner: string; +}) { + const erc20 = getErc20(params); + return erc20.balanceOf(params.owner); +} + +export async function getBalanceAndAllowance(params: { + chainId: number; + tokenAddress: string; + owner: string; + spender: string; +}) { + const provider = getProvider(params.chainId); + const erc20 = getErc20(params); + const [balance, allowance] = await callViaMulticall3(provider, [ + { + contract: erc20, + functionName: "balanceOf", + args: [params.owner], + }, + { + contract: erc20, + functionName: "allowance", + args: [params.owner, params.spender], + }, + ]); + return { balance, allowance }; +} + +export function getErc20(params: { chainId: number; tokenAddress: string }) { + const provider = getProvider(params.chainId); + return ERC20__factory.connect(params.tokenAddress, provider); +} diff --git a/api/_utils.ts b/api/_utils.ts index 53b8fc866..85a1a531b 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -5,6 +5,9 @@ import { SpokePool, SpokePool__factory, } from "@across-protocol/contracts/dist/typechain"; +// NOTE: We are still on v3.0.6 of verifier deployments until audit went through. Because the interface changed, we need to use the old factory. +// export { SpokePoolVerifier__factory } from "@across-protocol/contracts/dist/typechain/factories/contracts/SpokePoolVerifier__factory"; +import { SpokePoolVerifier__factory } from "@across-protocol/contracts-v3.0.6/dist/typechain/factories/contracts/SpokePoolVerifier__factory"; import acrossDeployments from "@across-protocol/contracts/dist/deployments/deployments.json"; import * as sdk from "@across-protocol/sdk"; import { asL2Provider } from "@eth-optimism/sdk"; @@ -2336,3 +2339,16 @@ export async function getTokenInfo({ chainId, address }: TokenOptions): Promise< }); } } + +export function getSpokePoolVerifier(chainId: number) { + const isSpokePoolVerifierDeployed = ( + ENABLED_ROUTES.spokePoolVerifier.enabledChains as number[] + ).includes(chainId); + + if (!isSpokePoolVerifierDeployed) { + return undefined; + } + + const address = ENABLED_ROUTES.spokePoolVerifier.address; + return SpokePoolVerifier__factory.connect(address, getProvider(chainId)); +} diff --git a/api/swap.ts b/api/swap.ts deleted file mode 100644 index 331725fcb..000000000 --- a/api/swap.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { VercelResponse } from "@vercel/node"; -import { assert, Infer, type, string, optional } from "superstruct"; -import { BigNumber, constants, utils } from "ethers"; - -import { TypedVercelRequest } from "./_types"; -import { - getLogger, - handleErrorCondition, - positiveFloatStr, - positiveIntStr, - validAddress, - boolStr, - getCachedTokenInfo, - getWrappedNativeTokenAddress, -} from "./_utils"; -import { - AMOUNT_TYPE, - buildCrossSwapTx, - getCrossSwapQuotes, -} from "./_dexes/cross-swap"; -import { InvalidParamError, MissingParamError } from "./_errors"; -import { isValidIntegratorId } from "./_integrator-id"; - -const SwapQueryParamsSchema = type({ - minOutputAmount: optional(positiveIntStr()), - exactOutputAmount: optional(positiveIntStr()), - exactInputAmount: optional(positiveIntStr()), - inputToken: validAddress(), - outputToken: validAddress(), - originChainId: positiveIntStr(), - destinationChainId: positiveIntStr(), - depositor: validAddress(), - recipient: optional(validAddress()), - integratorId: optional(string()), - refundAddress: optional(validAddress()), - refundOnOrigin: optional(boolStr()), - slippageTolerance: optional(positiveFloatStr(50)), // max. 50% slippage -}); - -type SwapQueryParams = Infer; - -const handler = async ( - { query }: TypedVercelRequest, - response: VercelResponse -) => { - const logger = getLogger(); - logger.debug({ - at: "Swap", - message: "Query data", - query, - }); - try { - assert(query, SwapQueryParamsSchema); - - const { - inputToken: _inputTokenAddress, - outputToken: _outputTokenAddress, - exactInputAmount: _exactInputAmount, - minOutputAmount: _minOutputAmount, - exactOutputAmount: _exactOutputAmount, - originChainId: _originChainId, - destinationChainId: _destinationChainId, - recipient, - depositor, - integratorId, - refundAddress, - refundOnOrigin: _refundOnOrigin = "true", - slippageTolerance = "1", // Default to 1% slippage - } = query; - - const originChainId = Number(_originChainId); - const destinationChainId = Number(_destinationChainId); - const refundOnOrigin = _refundOnOrigin === "true"; - const isInputNative = _inputTokenAddress === constants.AddressZero; - const isOutputNative = _outputTokenAddress === constants.AddressZero; - const inputTokenAddress = isInputNative - ? getWrappedNativeTokenAddress(originChainId) - : utils.getAddress(_inputTokenAddress); - const outputTokenAddress = isOutputNative - ? getWrappedNativeTokenAddress(destinationChainId) - : utils.getAddress(_outputTokenAddress); - - if (!_minOutputAmount && !_exactInputAmount && !_exactOutputAmount) { - throw new MissingParamError({ - param: "minOutputAmount, exactInputAmount, exactOutputAmount", - message: - "One of 'minOutputAmount', 'exactInputAmount' or 'exactOutputAmount' is required", - }); - } - - if (integratorId && !isValidIntegratorId(integratorId)) { - throw new InvalidParamError({ - param: "integratorId", - message: "Invalid integrator ID. Needs to be 2 bytes hex string.", - }); - } - - if (!inputTokenAddress || !outputTokenAddress) { - throw new InvalidParamError({ - param: "inputToken, outputToken", - message: "Invalid input or output token address", - }); - } - - if (integratorId && !isValidIntegratorId(integratorId)) { - throw new InvalidParamError({ - param: "integratorId", - message: "Invalid integrator ID. Needs to be 2 bytes hex string.", - }); - } - - const amountType = _minOutputAmount - ? AMOUNT_TYPE.MIN_OUTPUT - : _exactInputAmount - ? AMOUNT_TYPE.EXACT_INPUT - : AMOUNT_TYPE.EXACT_OUTPUT; - const amount = BigNumber.from( - _minOutputAmount || _exactInputAmount || _exactOutputAmount - ); - - // 1. Get token details - const [inputToken, outputToken] = await Promise.all([ - getCachedTokenInfo({ - address: inputTokenAddress, - chainId: originChainId, - }), - getCachedTokenInfo({ - address: outputTokenAddress, - chainId: destinationChainId, - }), - ]); - - // 2. Get swap quotes and calldata based on the swap type - const crossSwapQuotes = await getCrossSwapQuotes({ - amount, - inputToken, - outputToken, - depositor, - recipient: recipient || depositor, - slippageTolerance: Number(slippageTolerance), - type: amountType, - refundOnOrigin, - refundAddress, - isInputNative, - isOutputNative, - }); - - // 3. Build cross swap tx - const crossSwapTx = await buildCrossSwapTx(crossSwapQuotes, integratorId); - - const responseJson = { - tx: { - to: crossSwapTx.to, - data: crossSwapTx.data, - value: crossSwapTx.value?.toString(), - gas: crossSwapTx.gas?.toString(), - gasPrice: crossSwapTx.gasPrice?.toString(), - }, - }; - - logger.debug({ - at: "Swap", - message: "Response data", - responseJson, - }); - response.status(200).json(responseJson); - } catch (error: unknown) { - return handleErrorCondition("swap", response, logger, error); - } -}; - -export default handler; diff --git a/api/swap/_utils.ts b/api/swap/_utils.ts new file mode 100644 index 000000000..af1ca09c8 --- /dev/null +++ b/api/swap/_utils.ts @@ -0,0 +1,125 @@ +import { assert, Infer, type, string, optional, enums } from "superstruct"; +import { BigNumber, constants, utils } from "ethers"; + +import { TypedVercelRequest } from "../_types"; +import { + positiveFloatStr, + positiveIntStr, + validAddress, + boolStr, + getCachedTokenInfo, + getWrappedNativeTokenAddress, +} from "../_utils"; +import { + AMOUNT_TYPE, + getCrossSwapQuotes, + AmountType, +} from "../_dexes/cross-swap"; +import { InvalidParamError } from "../_errors"; +import { isValidIntegratorId } from "../_integrator-id"; + +export const BaseSwapQueryParamsSchema = type({ + amount: positiveIntStr(), + tradeType: optional(enums(["minOutput", "exactOutput"])), + inputToken: validAddress(), + outputToken: validAddress(), + originChainId: positiveIntStr(), + destinationChainId: positiveIntStr(), + depositor: validAddress(), + recipient: optional(validAddress()), + integratorId: optional(string()), + refundAddress: optional(validAddress()), + refundOnOrigin: optional(boolStr()), + slippageTolerance: optional(positiveFloatStr(50)), // max. 50% slippage + skipOriginTxEstimation: optional(boolStr()), +}); + +export type BaseSwapQueryParams = Infer; + +export async function handleBaseSwapQueryParams({ + query, +}: TypedVercelRequest) { + assert(query, BaseSwapQueryParamsSchema); + + const { + inputToken: _inputTokenAddress, + outputToken: _outputTokenAddress, + originChainId: _originChainId, + destinationChainId: _destinationChainId, + amount: _amount, + tradeType = AMOUNT_TYPE.MIN_OUTPUT, + recipient, + depositor, + integratorId, + refundAddress, + refundOnOrigin: _refundOnOrigin = "true", + slippageTolerance = "1", // Default to 1% slippage + skipOriginTxEstimation: _skipOriginTxEstimation = "false", + } = query; + + const originChainId = Number(_originChainId); + const destinationChainId = Number(_destinationChainId); + const refundOnOrigin = _refundOnOrigin === "true"; + const skipOriginTxEstimation = _skipOriginTxEstimation === "true"; + const isInputNative = _inputTokenAddress === constants.AddressZero; + const isOutputNative = _outputTokenAddress === constants.AddressZero; + const inputTokenAddress = isInputNative + ? getWrappedNativeTokenAddress(originChainId) + : utils.getAddress(_inputTokenAddress); + const outputTokenAddress = isOutputNative + ? getWrappedNativeTokenAddress(destinationChainId) + : utils.getAddress(_outputTokenAddress); + + if (integratorId && !isValidIntegratorId(integratorId)) { + throw new InvalidParamError({ + param: "integratorId", + message: "Invalid integrator ID. Needs to be 2 bytes hex string.", + }); + } + + if (!inputTokenAddress || !outputTokenAddress) { + throw new InvalidParamError({ + param: "inputToken, outputToken", + message: "Invalid input or output token address", + }); + } + + if (integratorId && !isValidIntegratorId(integratorId)) { + throw new InvalidParamError({ + param: "integratorId", + message: "Invalid integrator ID. Needs to be 2 bytes hex string.", + }); + } + + const amountType = tradeType as AmountType; + const amount = BigNumber.from(_amount); + + // 1. Get token details + const [inputToken, outputToken] = await Promise.all([ + getCachedTokenInfo({ + address: inputTokenAddress, + chainId: originChainId, + }), + getCachedTokenInfo({ + address: outputTokenAddress, + chainId: destinationChainId, + }), + ]); + + // 2. Get swap quotes and calldata based on the swap type + const crossSwapQuotes = await getCrossSwapQuotes({ + amount, + inputToken, + outputToken, + depositor, + recipient: recipient || depositor, + slippageTolerance: Number(slippageTolerance), + type: amountType, + refundOnOrigin, + refundAddress, + isInputNative, + isOutputNative, + }); + + return { crossSwapQuotes, integratorId, skipOriginTxEstimation }; +} diff --git a/api/swap/allowance.ts b/api/swap/allowance.ts new file mode 100644 index 000000000..590a78532 --- /dev/null +++ b/api/swap/allowance.ts @@ -0,0 +1,102 @@ +import { VercelResponse } from "@vercel/node"; + +import { TypedVercelRequest } from "../_types"; +import { + getLogger, + getProvider, + handleErrorCondition, + latestGasPriceCache, +} from "../_utils"; +import { buildCrossSwapTxForAllowanceHolder } from "../_dexes/cross-swap"; +import { handleBaseSwapQueryParams, BaseSwapQueryParams } from "./_utils"; +import { BigNumber } from "ethers"; +import { getBalanceAndAllowance } from "../_erc20"; + +const handler = async ( + request: TypedVercelRequest, + response: VercelResponse +) => { + const logger = getLogger(); + logger.debug({ + at: "Swap/allowance", + message: "Query data", + query: request.query, + }); + try { + const { crossSwapQuotes, integratorId, skipOriginTxEstimation } = + await handleBaseSwapQueryParams(request); + + const crossSwapTx = await buildCrossSwapTxForAllowanceHolder( + crossSwapQuotes, + integratorId + ); + + const originChainId = crossSwapQuotes.crossSwap.inputToken.chainId; + const inputTokenAddress = crossSwapQuotes.crossSwap.inputToken.address; + const inputAmount = + crossSwapQuotes.originSwapQuote?.maximumAmountIn || + crossSwapQuotes.bridgeQuote.inputAmount; + + const { allowance, balance } = await getBalanceAndAllowance({ + chainId: originChainId, + tokenAddress: inputTokenAddress, + owner: crossSwapQuotes.crossSwap.depositor, + spender: crossSwapTx.to, + }); + + const isSwapTxEstimationPossible = + !skipOriginTxEstimation && + allowance.lt(inputAmount) && + balance.gte(inputAmount); + + let originTxGas: BigNumber | undefined; + let originTxGasPrice: BigNumber | undefined; + if (isSwapTxEstimationPossible) { + const provider = getProvider(originChainId); + [originTxGas, originTxGasPrice] = await Promise.all([ + provider.estimateGas({ + ...crossSwapTx, + from: crossSwapQuotes.crossSwap.depositor, + }), + latestGasPriceCache(originChainId).get(), + ]); + } else { + originTxGasPrice = await latestGasPriceCache(originChainId).get(); + } + + const responseJson = { + checks: { + allowance: { + token: inputTokenAddress, + spender: crossSwapTx.to, + actual: allowance.toString(), + expected: inputAmount.toString(), + }, + balance: { + token: inputTokenAddress, + actual: balance.toString(), + expected: inputAmount.toString(), + }, + }, + tx: { + chainId: originChainId, + to: crossSwapTx.to, + data: crossSwapTx.data, + value: crossSwapTx.value?.toString(), + gas: originTxGas?.toString(), + gasPrice: originTxGasPrice?.toString(), + }, + }; + + logger.debug({ + at: "Swap/allowance", + message: "Response data", + responseJson, + }); + response.status(200).json(responseJson); + } catch (error: unknown) { + return handleErrorCondition("swap/allowance", response, logger, error); + } +}; + +export default handler; diff --git a/scripts/tests/swap.ts b/scripts/tests/swap-allowance.ts similarity index 67% rename from scripts/tests/swap.ts rename to scripts/tests/swap-allowance.ts index e7a01ef20..aed95c357 100644 --- a/scripts/tests/swap.ts +++ b/scripts/tests/swap-allowance.ts @@ -13,9 +13,10 @@ const depositor = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D"; const MIN_OUTPUT_CASES = [ // B2B { - labels: ["B2B", "MIN_OUTPUT", "Base USDC", "Arbitrum USDC"], + labels: ["B2B", "MIN_OUTPUT", "Base USDC - Arbitrum USDC"], params: { - minOutputAmount: ethers.utils.parseUnits("1", 6).toString(), + amount: ethers.utils.parseUnits("1", 6).toString(), + tradeType: "minOutput", inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], originChainId: CHAIN_IDs.BASE, outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], @@ -24,9 +25,10 @@ const MIN_OUTPUT_CASES = [ }, }, { - labels: ["B2B", "MIN_OUTPUT", "Base USDC", "Arbitrum ETH"], + labels: ["B2B", "MIN_OUTPUT", "Base USDC - Arbitrum ETH"], params: { - minOutputAmount: ethers.utils.parseUnits("0.001", 18).toString(), + amount: ethers.utils.parseUnits("0.001", 18).toString(), + tradeType: "minOutput", inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], originChainId: CHAIN_IDs.BASE, outputToken: ethers.constants.AddressZero, @@ -34,11 +36,24 @@ const MIN_OUTPUT_CASES = [ depositor, }, }, + { + labels: ["B2B", "MIN_OUTPUT", "Arbitrum ETH - Base USDC"], + params: { + amount: ethers.utils.parseUnits("3", 6).toString(), + tradeType: "minOutput", + inputToken: ethers.constants.AddressZero, + originChainId: CHAIN_IDs.ARBITRUM, + outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], + destinationChainId: CHAIN_IDs.BASE, + depositor, + }, + }, // B2A { - labels: ["B2A", "MIN_OUTPUT", "Base USDC", "Arbitrum WETH"], + labels: ["B2A", "MIN_OUTPUT", "Base USDC - Arbitrum WETH"], params: { - minOutputAmount: ethers.utils.parseUnits("0.001", 18).toString(), + amount: ethers.utils.parseUnits("0.001", 18).toString(), + tradeType: "minOutput", inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], originChainId: CHAIN_IDs.BASE, outputToken: TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.ARBITRUM], @@ -47,9 +62,10 @@ const MIN_OUTPUT_CASES = [ }, }, { - labels: ["B2A", "MIN_OUTPUT", "Base USDC", "Arbitrum ETH"], + labels: ["B2A", "MIN_OUTPUT", "Base USDC - Arbitrum ETH"], params: { - minOutputAmount: ethers.utils.parseUnits("0.001", 18).toString(), + amount: ethers.utils.parseUnits("0.001", 18).toString(), + tradeType: "minOutput", inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], originChainId: CHAIN_IDs.BASE, outputToken: ethers.constants.AddressZero, @@ -59,9 +75,10 @@ const MIN_OUTPUT_CASES = [ }, // A2B { - labels: ["A2B", "MIN_OUTPUT", "Base USDbC", "Arbitrum USDC"], + labels: ["A2B", "MIN_OUTPUT", "Base USDbC - Arbitrum USDC"], params: { - minOutputAmount: ethers.utils.parseUnits("1", 6).toString(), + amount: ethers.utils.parseUnits("1", 6).toString(), + tradeType: "minOutput", inputToken: TOKEN_SYMBOLS_MAP.USDbC.addresses[CHAIN_IDs.BASE], originChainId: CHAIN_IDs.BASE, outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], @@ -71,9 +88,10 @@ const MIN_OUTPUT_CASES = [ }, // A2A { - labels: ["A2A", "MIN_OUTPUT", "Base USDbC", "Arbitrum APE"], + labels: ["A2A", "MIN_OUTPUT", "Base USDbC - Arbitrum APE"], params: { - minOutputAmount: ethers.utils.parseUnits("1", 18).toString(), + amount: ethers.utils.parseUnits("1", 18).toString(), + tradeType: "minOutput", inputToken: TOKEN_SYMBOLS_MAP.USDbC.addresses[CHAIN_IDs.BASE], originChainId: CHAIN_IDs.BASE, outputToken: "0x74885b4D524d497261259B38900f54e6dbAd2210", // APE Coin @@ -85,9 +103,10 @@ const MIN_OUTPUT_CASES = [ const EXACT_OUTPUT_CASES = [ // B2B { - labels: ["B2B", "EXACT_OUTPUT", "Base USDC", "Arbitrum USDC"], + labels: ["B2B", "EXACT_OUTPUT", "Base USDC - Arbitrum USDC"], params: { - exactOutputAmount: ethers.utils.parseUnits("1", 6).toString(), + amount: ethers.utils.parseUnits("1", 6).toString(), + tradeType: "exactOutput", inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], originChainId: CHAIN_IDs.BASE, outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], @@ -96,9 +115,10 @@ const EXACT_OUTPUT_CASES = [ }, }, { - labels: ["B2B", "EXACT_OUTPUT", "Base USDC", "Arbitrum ETH"], + labels: ["B2B", "EXACT_OUTPUT", "Base USDC -Arbitrum ETH"], params: { - exactOutputAmount: ethers.utils.parseUnits("0.001", 18).toString(), + amount: ethers.utils.parseUnits("0.001", 18).toString(), + tradeType: "exactOutput", inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], originChainId: CHAIN_IDs.BASE, outputToken: ethers.constants.AddressZero, @@ -108,9 +128,10 @@ const EXACT_OUTPUT_CASES = [ }, // B2A { - labels: ["B2A", "EXACT_OUTPUT", "Base USDC", "Arbitrum WETH"], + labels: ["B2A", "EXACT_OUTPUT", "Base USDC - Arbitrum WETH"], params: { - exactOutputAmount: ethers.utils.parseUnits("0.001", 18).toString(), + amount: ethers.utils.parseUnits("0.001", 18).toString(), + tradeType: "exactOutput", inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], originChainId: CHAIN_IDs.BASE, outputToken: TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.ARBITRUM], @@ -119,9 +140,10 @@ const EXACT_OUTPUT_CASES = [ }, }, { - labels: ["B2A", "EXACT_OUTPUT", "Base USDC", "Arbitrum ETH"], + labels: ["B2A", "EXACT_OUTPUT", "Base USDC - Arbitrum ETH"], params: { - exactOutputAmount: ethers.utils.parseUnits("0.001", 18).toString(), + amount: ethers.utils.parseUnits("0.001", 18).toString(), + tradeType: "exactOutput", inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], originChainId: CHAIN_IDs.BASE, outputToken: ethers.constants.AddressZero, @@ -131,9 +153,10 @@ const EXACT_OUTPUT_CASES = [ }, // A2B { - labels: ["A2B", "EXACT_OUTPUT", "Base USDbC", "Arbitrum USDC"], + labels: ["A2B", "EXACT_OUTPUT", "Base USDbC - Arbitrum USDC"], params: { - minOutputAmount: ethers.utils.parseUnits("1", 6).toString(), + amount: ethers.utils.parseUnits("1", 6).toString(), + tradeType: "exactOutput", inputToken: TOKEN_SYMBOLS_MAP.USDbC.addresses[CHAIN_IDs.BASE], originChainId: CHAIN_IDs.BASE, outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], @@ -143,9 +166,10 @@ const EXACT_OUTPUT_CASES = [ }, // A2A { - labels: ["A2A", "EXACT_OUTPUT", "Base USDbC", "Arbitrum APE"], + labels: ["A2A", "EXACT_OUTPUT", "Base USDbC - Arbitrum APE"], params: { - minOutputAmount: ethers.utils.parseUnits("1", 18).toString(), + amount: ethers.utils.parseUnits("1", 18).toString(), + tradeType: "exactOutput", inputToken: TOKEN_SYMBOLS_MAP.USDbC.addresses[CHAIN_IDs.BASE], originChainId: CHAIN_IDs.BASE, outputToken: "0x74885b4D524d497261259B38900f54e6dbAd2210", // APE Coin @@ -169,9 +193,12 @@ async function swap() { }); for (const testCase of filteredTestCases) { console.log("\nTest case:", testCase.labels.join(" ")); - const response = await axios.get(`http://localhost:3000/api/swap`, { - params: testCase.params, - }); + const response = await axios.get( + `http://localhost:3000/api/swap/allowance`, + { + params: testCase.params, + } + ); console.log(response.data); if (process.env.DEV_WALLET_PK) { From 60313fdca6f7c623788158bbfac74e0802abad31 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Fri, 22 Nov 2024 18:25:04 +0700 Subject: [PATCH 09/47] fix: allowance and balance check --- api/_erc20.ts | 2 +- api/swap/allowance.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/_erc20.ts b/api/_erc20.ts index 122eeff7f..855fcae32 100644 --- a/api/_erc20.ts +++ b/api/_erc20.ts @@ -40,7 +40,7 @@ export async function getBalanceAndAllowance(params: { args: [params.owner, params.spender], }, ]); - return { balance, allowance }; + return { balance: balance[0], allowance: allowance[0] }; } export function getErc20(params: { chainId: number; tokenAddress: string }) { diff --git a/api/swap/allowance.ts b/api/swap/allowance.ts index 590a78532..642481585 100644 --- a/api/swap/allowance.ts +++ b/api/swap/allowance.ts @@ -46,7 +46,7 @@ const handler = async ( const isSwapTxEstimationPossible = !skipOriginTxEstimation && - allowance.lt(inputAmount) && + allowance.gte(inputAmount) && balance.gte(inputAmount); let originTxGas: BigNumber | undefined; From 334a245d6050d7ffabc204173947b74fc6082724 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Fri, 22 Nov 2024 18:39:52 +0700 Subject: [PATCH 10/47] fixup --- api/_dexes/uniswap.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/api/_dexes/uniswap.ts b/api/_dexes/uniswap.ts index 02f46cdc4..5e0f37f9a 100644 --- a/api/_dexes/uniswap.ts +++ b/api/_dexes/uniswap.ts @@ -273,7 +273,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( ); // 2.2. Re-fetch origin swap quote with updated input amount and EXACT_INPUT type. // This prevents leftover tokens in the SwapAndBridge contract. - const adjOriginSwapQuote = await getUniswapQuote( + let adjOriginSwapQuote = await getUniswapQuote( { ...originSwap, amount: originSwapQuote.maximumAmountIn.toString(), @@ -282,10 +282,22 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( ); if (adjOriginSwapQuote.minAmountOut.lt(bridgeQuote.inputAmount)) { - throw new Error( - `Origin swap quote min. output amount ${adjOriginSwapQuote.minAmountOut.toString()} ` + - `is less than required bridge input amount ${bridgeQuote.inputAmount.toString()}` + adjOriginSwapQuote = await getUniswapQuote( + { + ...originSwap, + amount: addSlippageToAmount( + bridgeQuote.inputAmount, + crossSwap.slippageTolerance.toString() + ), + }, + TradeType.EXACT_INPUT ); + if (adjOriginSwapQuote.minAmountOut.lt(bridgeQuote.inputAmount)) { + throw new Error( + `Origin swap quote min. output amount ${adjOriginSwapQuote.minAmountOut.toString()} ` + + `is less than required bridge input amount ${bridgeQuote.inputAmount.toString()}` + ); + } } return { From a0a2e418fb7db53fbcf883e3fea1304cfd64f59b Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Fri, 22 Nov 2024 20:52:56 +0700 Subject: [PATCH 11/47] fixup --- yarn.lock | 66 ++++++++++++++++++++++++------------------------------- 1 file changed, 29 insertions(+), 37 deletions(-) diff --git a/yarn.lock b/yarn.lock index 967cf7c2f..835c699df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,12 +16,7 @@ "@uma/common" "^2.17.0" hardhat "^2.9.3" -"@across-protocol/constants@^3.1.19", "@across-protocol/constants@^3.1.20": - version "3.1.20" - resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.20.tgz#305bd41f5644b7db5d9fd12a6a6b4bbbbe2fd016" - integrity sha512-B5RsvuOQsZdFgLk0WcFZGmoivm6g6gv95a+YKVBydcxZkNxAsyP065UQEDAmvRXvPhqGyehhd52515Xa/3bzyg== - -"@across-protocol/constants@^3.1.19": +"@across-protocol/constants@^3.1.19", "@across-protocol/constants@^3.1.20", "@across-protocol/constants@^3.1.9": version "3.1.20" resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.20.tgz#305bd41f5644b7db5d9fd12a6a6b4bbbbe2fd016" integrity sha512-B5RsvuOQsZdFgLk0WcFZGmoivm6g6gv95a+YKVBydcxZkNxAsyP065UQEDAmvRXvPhqGyehhd52515Xa/3bzyg== @@ -83,34 +78,6 @@ yargs "^17.7.2" zksync-web3 "^0.14.3" -"@across-protocol/contracts@^3.0.16": - version "3.0.16" - resolved "https://registry.yarnpkg.com/@across-protocol/contracts/-/contracts-3.0.16.tgz#22eb0c1dcdb01e8ca504dc2351d46513d9f71cc6" - integrity sha512-vwg+PmWaenlrx7kTHZdjDTTj1PwXWFU3rMlFyfKM8xBXbPWhIfMQCKCYOwFrGmZw2nRTYgoyhoKN/f6rUs/snw== - dependencies: - "@across-protocol/constants" "^3.1.19" - "@coral-xyz/anchor" "^0.30.1" - "@defi-wonderland/smock" "^2.3.4" - "@eth-optimism/contracts" "^0.5.40" - "@ethersproject/abstract-provider" "5.7.0" - "@ethersproject/abstract-signer" "5.7.0" - "@ethersproject/bignumber" "5.7.0" - "@openzeppelin/contracts" "4.9.6" - "@openzeppelin/contracts-upgradeable" "4.9.6" - "@scroll-tech/contracts" "^0.1.0" - "@solana-developers/helpers" "^2.4.0" - "@solana/spl-token" "^0.4.6" - "@solana/web3.js" "^1.31.0" - "@types/yargs" "^17.0.33" - "@uma/common" "^2.34.0" - "@uma/contracts-node" "^0.4.17" - "@uma/core" "^2.56.0" - axios "^1.7.4" - bs58 "^6.0.0" - prettier-plugin-rust "^0.1.9" - yargs "^17.7.2" - zksync-web3 "^0.14.3" - "@across-protocol/sdk@^3.3.18": version "3.3.18" resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-3.3.18.tgz#d39ef359f9f639921fb412a1355167354014a80f" @@ -22780,7 +22747,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -22815,6 +22782,15 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -22889,7 +22865,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -22917,6 +22893,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -25623,7 +25606,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -25658,6 +25641,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 2d8627e297e4a61e030e8894fa0cd519f084618c Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Fri, 22 Nov 2024 22:23:31 +0700 Subject: [PATCH 12/47] feat: add missing response fields --- api/swap/allowance.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/api/swap/allowance.ts b/api/swap/allowance.ts index 642481585..6d2129ed8 100644 --- a/api/swap/allowance.ts +++ b/api/swap/allowance.ts @@ -86,6 +86,17 @@ const handler = async ( gas: originTxGas?.toString(), gasPrice: originTxGasPrice?.toString(), }, + inputAmount: + crossSwapQuotes.originSwapQuote?.expectedAmountIn ?? + crossSwapQuotes.bridgeQuote.inputAmount, + expectedOutputAmount: + crossSwapQuotes.destinationSwapQuote?.expectedAmountOut ?? + crossSwapQuotes.bridgeQuote.outputAmount, + minOutputAmount: + crossSwapQuotes.originSwapQuote?.minAmountOut ?? + crossSwapQuotes.bridgeQuote.outputAmount, + expectedFillTime: + crossSwapQuotes.bridgeQuote.suggestedFees.estimatedFillTimeSec, }; logger.debug({ From f0646962b09c62976d1bbc123caf822de8d2a5b4 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Fri, 22 Nov 2024 22:25:51 +0700 Subject: [PATCH 13/47] fixup --- api/swap/allowance.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api/swap/allowance.ts b/api/swap/allowance.ts index 6d2129ed8..0efdf4fb4 100644 --- a/api/swap/allowance.ts +++ b/api/swap/allowance.ts @@ -87,14 +87,14 @@ const handler = async ( gasPrice: originTxGasPrice?.toString(), }, inputAmount: - crossSwapQuotes.originSwapQuote?.expectedAmountIn ?? - crossSwapQuotes.bridgeQuote.inputAmount, + crossSwapQuotes.originSwapQuote?.expectedAmountIn.toString() ?? + crossSwapQuotes.bridgeQuote.inputAmount.toString(), expectedOutputAmount: - crossSwapQuotes.destinationSwapQuote?.expectedAmountOut ?? - crossSwapQuotes.bridgeQuote.outputAmount, + crossSwapQuotes.destinationSwapQuote?.expectedAmountOut.toString() ?? + crossSwapQuotes.bridgeQuote.outputAmount.toString(), minOutputAmount: - crossSwapQuotes.originSwapQuote?.minAmountOut ?? - crossSwapQuotes.bridgeQuote.outputAmount, + crossSwapQuotes.originSwapQuote?.minAmountOut.toString() ?? + crossSwapQuotes.bridgeQuote.outputAmount.toString(), expectedFillTime: crossSwapQuotes.bridgeQuote.suggestedFees.estimatedFillTimeSec, }; From 6d8d45afa95039bf86dd9c1eb9e1e9a119177271 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Fri, 22 Nov 2024 22:31:46 +0700 Subject: [PATCH 14/47] feat: skip spokepool verifier temp (#1286) --- api/_dexes/cross-swap.ts | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/api/_dexes/cross-swap.ts b/api/_dexes/cross-swap.ts index b33113c30..30d8a3532 100644 --- a/api/_dexes/cross-swap.ts +++ b/api/_dexes/cross-swap.ts @@ -1,5 +1,5 @@ import { SpokePool } from "@across-protocol/contracts/dist/typechain"; -import { ethers, PopulatedTransaction } from "ethers"; +import { PopulatedTransaction } from "ethers"; import { utils } from "@across-protocol/sdk"; import { @@ -8,7 +8,6 @@ import { isOutputTokenBridgeable, getBridgeQuoteForMinOutput, getSpokePool, - getSpokePoolVerifier, } from "../_utils"; import { getUniswapCrossSwapQuotesForOutputB2A, @@ -179,9 +178,7 @@ export async function buildCrossSwapTxForAllowanceHolder( integratorId?: string ) { const originChainId = crossSwapQuotes.crossSwap.inputToken.chainId; - const isInputNative = crossSwapQuotes.crossSwap.isInputNative; const spokePool = getSpokePool(originChainId); - const spokePoolVerifier = getSpokePoolVerifier(originChainId); const deposit = await extractDepositDataStruct(crossSwapQuotes); let tx: PopulatedTransaction; @@ -203,22 +200,6 @@ export async function buildCrossSwapTxForAllowanceHolder( } ); toAddress = swapAndBridge.address; - } else if (isInputNative && spokePoolVerifier) { - tx = await spokePoolVerifier.populateTransaction.deposit( - spokePool.address, - deposit.recipient, - deposit.inputToken, - deposit.inputAmount, - deposit.destinationChainid, - crossSwapQuotes.bridgeQuote.suggestedFees.totalRelayFee.pct, - deposit.quoteTimestamp, - deposit.message, - ethers.constants.MaxUint256, - { - value: deposit.inputAmount, - } - ); - toAddress = spokePoolVerifier.address; } else { tx = await spokePool.populateTransaction.depositV3( deposit.depositor, @@ -249,6 +230,7 @@ export async function buildCrossSwapTxForAllowanceHolder( value: tx.value, }; } + async function extractDepositDataStruct(crossSwapQuotes: CrossSwapQuotes) { const originChainId = crossSwapQuotes.crossSwap.inputToken.chainId; const destinationChainId = crossSwapQuotes.crossSwap.outputToken.chainId; From 11f2640cb29c64c2454dba344aac55a3e5e58d88 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Fri, 22 Nov 2024 22:40:33 +0700 Subject: [PATCH 15/47] feat: add more swap and bridge addresses --- scripts/generate-routes.ts | 4 ++++ ...routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/generate-routes.ts b/scripts/generate-routes.ts index 694f18b05..057bf4e08 100644 --- a/scripts/generate-routes.ts +++ b/scripts/generate-routes.ts @@ -103,6 +103,10 @@ const enabledRoutes = { [CHAIN_IDs.OPTIMISM]: "0x28249CD61170Ed200Db8988D7f69e21331AFFBD0", [CHAIN_IDs.ARBITRUM]: "0x07b64BE6906A78fac5FaB47cD58d59D0A093B15e", [CHAIN_IDs.BASE]: "0xef5a54BEeBDB4Ee98E2906e30e65AD53dA8D2b17", + [CHAIN_IDs.ZK_SYNC]: "0xB007dFe9A6b70e1AB5BD5E97C22e47C5e0c0B8D8", + [CHAIN_IDs.WORLD_CHAIN]: "0x1F4dC6D69E3b4dAC139E149E213a7e863a813466", + [CHAIN_IDs.BLAST]: "0x52313039f1b849B49dce4bdf6a43AC76995bE366", + [CHAIN_IDs.ZORA]: "0x9367CC85B16932b19ee160A7AA2B251C8606d5b4", }, }, routes: transformChainConfigs(enabledMainnetChainConfigs), diff --git a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json index 000b3eac2..fe71971bd 100644 --- a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json +++ b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json @@ -17,8 +17,12 @@ "uniswap": { "10": "0x28249CD61170Ed200Db8988D7f69e21331AFFBD0", "137": "0xC2dCB88873E00c9d401De2CBBa4C6A28f8A6e2c2", + "324": "0xB007dFe9A6b70e1AB5BD5E97C22e47C5e0c0B8D8", + "480": "0x1F4dC6D69E3b4dAC139E149E213a7e863a813466", "8453": "0xef5a54BEeBDB4Ee98E2906e30e65AD53dA8D2b17", - "42161": "0x07b64BE6906A78fac5FaB47cD58d59D0A093B15e" + "42161": "0x07b64BE6906A78fac5FaB47cD58d59D0A093B15e", + "81457": "0x52313039f1b849B49dce4bdf6a43AC76995bE366", + "7777777": "0x9367CC85B16932b19ee160A7AA2B251C8606d5b4" } }, "routes": [ From 81dee5a9bb66f25af74f411a4706cc80c27a2699 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Fri, 22 Nov 2024 22:55:37 +0700 Subject: [PATCH 16/47] feat: mainnet --- scripts/generate-routes.ts | 15 ++++++++------- ...xc186fA914353c44b2E33eBE05f21846F1048bEda.json | 15 ++++++++------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/scripts/generate-routes.ts b/scripts/generate-routes.ts index 057bf4e08..86276288b 100644 --- a/scripts/generate-routes.ts +++ b/scripts/generate-routes.ts @@ -99,14 +99,15 @@ const enabledRoutes = { [CHAIN_IDs.BASE]: "0x98285D11B9F7aFec2d475805E5255f26B4490167", }, uniswap: { - [CHAIN_IDs.POLYGON]: "0xC2dCB88873E00c9d401De2CBBa4C6A28f8A6e2c2", - [CHAIN_IDs.OPTIMISM]: "0x28249CD61170Ed200Db8988D7f69e21331AFFBD0", - [CHAIN_IDs.ARBITRUM]: "0x07b64BE6906A78fac5FaB47cD58d59D0A093B15e", - [CHAIN_IDs.BASE]: "0xef5a54BEeBDB4Ee98E2906e30e65AD53dA8D2b17", + [CHAIN_IDs.POLYGON]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", + [CHAIN_IDs.OPTIMISM]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", + [CHAIN_IDs.ARBITRUM]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", + [CHAIN_IDs.BASE]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", [CHAIN_IDs.ZK_SYNC]: "0xB007dFe9A6b70e1AB5BD5E97C22e47C5e0c0B8D8", - [CHAIN_IDs.WORLD_CHAIN]: "0x1F4dC6D69E3b4dAC139E149E213a7e863a813466", - [CHAIN_IDs.BLAST]: "0x52313039f1b849B49dce4bdf6a43AC76995bE366", - [CHAIN_IDs.ZORA]: "0x9367CC85B16932b19ee160A7AA2B251C8606d5b4", + [CHAIN_IDs.WORLD_CHAIN]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", + [CHAIN_IDs.BLAST]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", + [CHAIN_IDs.ZORA]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", + [CHAIN_IDs.MAINNET]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", }, }, routes: transformChainConfigs(enabledMainnetChainConfigs), diff --git a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json index fe71971bd..ad0fbfe36 100644 --- a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json +++ b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json @@ -15,14 +15,15 @@ "42161": "0x81C7601ac0c5825e89F967f9222B977CCD78aD77" }, "uniswap": { - "10": "0x28249CD61170Ed200Db8988D7f69e21331AFFBD0", - "137": "0xC2dCB88873E00c9d401De2CBBa4C6A28f8A6e2c2", + "1": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", + "10": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", + "137": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", "324": "0xB007dFe9A6b70e1AB5BD5E97C22e47C5e0c0B8D8", - "480": "0x1F4dC6D69E3b4dAC139E149E213a7e863a813466", - "8453": "0xef5a54BEeBDB4Ee98E2906e30e65AD53dA8D2b17", - "42161": "0x07b64BE6906A78fac5FaB47cD58d59D0A093B15e", - "81457": "0x52313039f1b849B49dce4bdf6a43AC76995bE366", - "7777777": "0x9367CC85B16932b19ee160A7AA2B251C8606d5b4" + "480": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", + "8453": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", + "42161": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", + "81457": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", + "7777777": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e" } }, "routes": [ From 1addbd7d4e9be8151a3013f87020a24b0057e014 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Fri, 22 Nov 2024 22:58:51 +0700 Subject: [PATCH 17/47] fixup --- api/swap/allowance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/swap/allowance.ts b/api/swap/allowance.ts index 0efdf4fb4..c36a537fe 100644 --- a/api/swap/allowance.ts +++ b/api/swap/allowance.ts @@ -93,7 +93,7 @@ const handler = async ( crossSwapQuotes.destinationSwapQuote?.expectedAmountOut.toString() ?? crossSwapQuotes.bridgeQuote.outputAmount.toString(), minOutputAmount: - crossSwapQuotes.originSwapQuote?.minAmountOut.toString() ?? + crossSwapQuotes.destinationSwapQuote?.minAmountOut.toString() ?? crossSwapQuotes.bridgeQuote.outputAmount.toString(), expectedFillTime: crossSwapQuotes.bridgeQuote.suggestedFees.estimatedFillTimeSec, From 91c4a5638b19c1aadef643d2bdd967a29f54a392 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Sat, 23 Nov 2024 11:20:29 +0700 Subject: [PATCH 18/47] feat: refund on origin flag (#1284) * feat: handle refund on origin flag * fixup * allow custom refund address via origin * fix: refund address for leftovers * add refundToken to response * fixup --- api/_dexes/cross-swap.ts | 7 ++- api/_dexes/uniswap.ts | 99 +++++++++++++++++++++++++--------------- api/_dexes/utils.ts | 16 ++++--- api/swap/allowance.ts | 3 ++ 4 files changed, 80 insertions(+), 45 deletions(-) diff --git a/api/_dexes/cross-swap.ts b/api/_dexes/cross-swap.ts index 30d8a3532..86a108f0c 100644 --- a/api/_dexes/cross-swap.ts +++ b/api/_dexes/cross-swap.ts @@ -236,8 +236,13 @@ async function extractDepositDataStruct(crossSwapQuotes: CrossSwapQuotes) { const destinationChainId = crossSwapQuotes.crossSwap.outputToken.chainId; const spokePool = getSpokePool(originChainId); const message = crossSwapQuotes.bridgeQuote.message || "0x"; + const refundAddress = + crossSwapQuotes.crossSwap.refundAddress ?? + crossSwapQuotes.crossSwap.depositor; const deposit = { - depositor: crossSwapQuotes.crossSwap.depositor, + depositor: crossSwapQuotes.crossSwap.refundOnOrigin + ? refundAddress + : crossSwapQuotes.crossSwap.depositor, recipient: utils.isMessageEmpty(message) ? getMultiCallHandlerAddress(destinationChainId) : crossSwapQuotes.crossSwap.recipient, diff --git a/api/_dexes/uniswap.ts b/api/_dexes/uniswap.ts index 5e0f37f9a..7eab8fe7a 100644 --- a/api/_dexes/uniswap.ts +++ b/api/_dexes/uniswap.ts @@ -35,6 +35,7 @@ import { import { buildExactOutputBridgeTokenMessage, buildMinOutputBridgeTokenMessage, + getFallbackRecipient, getSwapAndBridgeAddress, NoSwapRouteError, } from "./utils"; @@ -637,44 +638,66 @@ function buildDestinationSwapCrossChainMessage({ destinationSwapQuote: SwapQuote; }) { const destinationSwapChainId = destinationSwapQuote.tokenOut.chainId; - const transferActions = crossSwap.isOutputNative - ? // If output token is native, we need to unwrap WETH before sending it to the - // recipient. This is because we only handle WETH in the destination swap. - [ - { - target: crossSwap.outputToken.address, - callData: encodeWethWithdrawCalldata(crossSwap.amount), - value: "0", - }, - { - target: crossSwap.recipient, - callData: "0x", - value: crossSwap.amount.toString(), - }, - ] - : [ - { - target: crossSwap.outputToken.address, - callData: encodeTransferCalldata( - crossSwap.recipient, - crossSwap.amount - ), - value: "0", - }, - { - target: getMultiCallHandlerAddress(destinationSwapChainId), - callData: encodeDrainCalldata( - crossSwap.outputToken.address, - crossSwap.type === AMOUNT_TYPE.EXACT_OUTPUT - ? crossSwap.depositor - : crossSwap.recipient - ), - value: "0", - }, - ]; + + let transferActions: { + target: string; + callData: string; + value: string; + }[] = []; + + // If output token is native, we need to unwrap WETH before sending it to the + // recipient. This is because we only handle WETH in the destination swap. + if (crossSwap.isOutputNative) { + transferActions = [ + { + target: crossSwap.outputToken.address, + callData: encodeWethWithdrawCalldata(crossSwap.amount), + value: "0", + }, + { + target: crossSwap.recipient, + callData: "0x", + value: crossSwap.amount.toString(), + }, + ]; + } + // If output token is an ERC-20 token and amount type is EXACT_OUTPUT, we need + // to transfer the EXACT output amount to the recipient. The refundAddress / depositor + // will receive any leftovers. + else if (crossSwap.type === AMOUNT_TYPE.EXACT_OUTPUT) { + transferActions = [ + { + target: crossSwap.outputToken.address, + callData: encodeTransferCalldata(crossSwap.recipient, crossSwap.amount), + value: "0", + }, + { + target: getMultiCallHandlerAddress(destinationSwapChainId), + callData: encodeDrainCalldata( + crossSwap.outputToken.address, + crossSwap.refundAddress ?? crossSwap.depositor + ), + value: "0", + }, + ]; + } + // If output token is an ERC-20 token and amount type is MIN_OUTPUT, we need + // to transfer all realized output tokens to the recipient. + else if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT) { + transferActions = [ + { + target: getMultiCallHandlerAddress(destinationSwapChainId), + callData: encodeDrainCalldata( + crossSwap.outputToken.address, + crossSwap.recipient + ), + value: "0", + }, + ]; + } + return buildMulticallHandlerMessage({ - // @TODO: handle fallback recipient for params `refundOnOrigin` and `refundAddress` - fallbackRecipient: crossSwap.depositor, + fallbackRecipient: getFallbackRecipient(crossSwap), actions: [ // approve bridgeable output token { @@ -699,7 +722,7 @@ function buildDestinationSwapCrossChainMessage({ callData: encodeDrainCalldata( bridgeableOutputToken.address, crossSwap.type === AMOUNT_TYPE.EXACT_OUTPUT - ? crossSwap.depositor + ? crossSwap.refundAddress ?? crossSwap.depositor : crossSwap.recipient ), value: "0", diff --git a/api/_dexes/utils.ts b/api/_dexes/utils.ts index 3702417dd..d4d6ba962 100644 --- a/api/_dexes/utils.ts +++ b/api/_dexes/utils.ts @@ -1,5 +1,5 @@ import { UniversalSwapAndBridge__factory } from "../_typechain/factories/SwapAndBridge.sol"; -import { BigNumber } from "ethers"; +import { BigNumber, constants } from "ethers"; import { ENABLED_ROUTES, getProvider } from "../_utils"; import { @@ -102,8 +102,7 @@ export function buildExactOutputBridgeTokenMessage(crossSwap: CrossSwap) { }, ]; return buildMulticallHandlerMessage({ - // @TODO: handle fallback recipient for params `refundOnOrigin` and `refundAddress` - fallbackRecipient: crossSwap.depositor, + fallbackRecipient: getFallbackRecipient(crossSwap), actions: [ ...transferActions, // drain remaining bridgeable output tokens from MultiCallHandler contract @@ -111,7 +110,7 @@ export function buildExactOutputBridgeTokenMessage(crossSwap: CrossSwap) { target: getMultiCallHandlerAddress(crossSwap.outputToken.chainId), callData: encodeDrainCalldata( crossSwap.outputToken.address, - crossSwap.depositor + crossSwap.refundAddress ?? crossSwap.depositor ), value: "0", }, @@ -146,8 +145,7 @@ export function buildMinOutputBridgeTokenMessage( : // ERC-20 token transfer []; return buildMulticallHandlerMessage({ - // @TODO: handle fallback recipient for params `refundOnOrigin` and `refundAddress` - fallbackRecipient: crossSwap.depositor, + fallbackRecipient: getFallbackRecipient(crossSwap), actions: [ ...transferActions, // drain remaining bridgeable output tokens from MultiCallHandler contract @@ -162,3 +160,9 @@ export function buildMinOutputBridgeTokenMessage( ], }); } + +export function getFallbackRecipient(crossSwap: CrossSwap) { + return crossSwap.refundOnOrigin + ? constants.AddressZero + : crossSwap.refundAddress ?? crossSwap.depositor; +} diff --git a/api/swap/allowance.ts b/api/swap/allowance.ts index c36a537fe..9b2324a02 100644 --- a/api/swap/allowance.ts +++ b/api/swap/allowance.ts @@ -86,6 +86,9 @@ const handler = async ( gas: originTxGas?.toString(), gasPrice: originTxGasPrice?.toString(), }, + refundToken: crossSwapQuotes.crossSwap.refundOnOrigin + ? crossSwapQuotes.bridgeQuote.inputToken + : crossSwapQuotes.bridgeQuote.outputToken, inputAmount: crossSwapQuotes.originSwapQuote?.expectedAmountIn.toString() ?? crossSwapQuotes.bridgeQuote.inputAmount.toString(), From e9b902ad16491f60985ce1a1f968a3b34d8b73fa Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Sat, 23 Nov 2024 11:33:25 +0700 Subject: [PATCH 19/47] fix: native eth handling in allowance + balance check --- api/_erc20.ts | 8 ++++++++ api/swap/_utils.ts | 7 ++++++- api/swap/allowance.ts | 29 ++++++++++++++++++++++------- scripts/tests/swap-allowance.ts | 5 +++++ 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/api/_erc20.ts b/api/_erc20.ts index 855fcae32..e2cfd5953 100644 --- a/api/_erc20.ts +++ b/api/_erc20.ts @@ -1,4 +1,6 @@ import { ERC20__factory } from "@across-protocol/contracts"; +import { constants } from "ethers"; + import { callViaMulticall3, getProvider } from "./_utils"; export async function getAllowance(params: { @@ -27,6 +29,12 @@ export async function getBalanceAndAllowance(params: { spender: string; }) { const provider = getProvider(params.chainId); + + if (params.tokenAddress === constants.AddressZero) { + const balance = await provider.getBalance(params.owner); + return { balance, allowance: constants.MaxUint256 }; + } + const erc20 = getErc20(params); const [balance, allowance] = await callViaMulticall3(provider, [ { diff --git a/api/swap/_utils.ts b/api/swap/_utils.ts index af1ca09c8..a6e49a3aa 100644 --- a/api/swap/_utils.ts +++ b/api/swap/_utils.ts @@ -121,5 +121,10 @@ export async function handleBaseSwapQueryParams({ isOutputNative, }); - return { crossSwapQuotes, integratorId, skipOriginTxEstimation }; + return { + crossSwapQuotes, + integratorId, + skipOriginTxEstimation, + isInputNative, + }; } diff --git a/api/swap/allowance.ts b/api/swap/allowance.ts index 9b2324a02..8736603a0 100644 --- a/api/swap/allowance.ts +++ b/api/swap/allowance.ts @@ -1,4 +1,5 @@ import { VercelResponse } from "@vercel/node"; +import { BigNumber, constants } from "ethers"; import { TypedVercelRequest } from "../_types"; import { @@ -9,7 +10,6 @@ import { } from "../_utils"; import { buildCrossSwapTxForAllowanceHolder } from "../_dexes/cross-swap"; import { handleBaseSwapQueryParams, BaseSwapQueryParams } from "./_utils"; -import { BigNumber } from "ethers"; import { getBalanceAndAllowance } from "../_erc20"; const handler = async ( @@ -23,8 +23,12 @@ const handler = async ( query: request.query, }); try { - const { crossSwapQuotes, integratorId, skipOriginTxEstimation } = - await handleBaseSwapQueryParams(request); + const { + crossSwapQuotes, + integratorId, + skipOriginTxEstimation, + isInputNative, + } = await handleBaseSwapQueryParams(request); const crossSwapTx = await buildCrossSwapTxForAllowanceHolder( crossSwapQuotes, @@ -32,7 +36,9 @@ const handler = async ( ); const originChainId = crossSwapQuotes.crossSwap.inputToken.chainId; - const inputTokenAddress = crossSwapQuotes.crossSwap.inputToken.address; + const inputTokenAddress = isInputNative + ? constants.AddressZero + : crossSwapQuotes.crossSwap.inputToken.address; const inputAmount = crossSwapQuotes.originSwapQuote?.maximumAmountIn || crossSwapQuotes.bridgeQuote.inputAmount; @@ -64,6 +70,10 @@ const handler = async ( originTxGasPrice = await latestGasPriceCache(originChainId).get(); } + const refundToken = crossSwapQuotes.crossSwap.refundOnOrigin + ? crossSwapQuotes.bridgeQuote.inputToken + : crossSwapQuotes.bridgeQuote.outputToken; + const responseJson = { checks: { allowance: { @@ -79,6 +89,7 @@ const handler = async ( }, }, tx: { + simulationSuccess: !!originTxGas, chainId: originChainId, to: crossSwapTx.to, data: crossSwapTx.data, @@ -86,9 +97,13 @@ const handler = async ( gas: originTxGas?.toString(), gasPrice: originTxGasPrice?.toString(), }, - refundToken: crossSwapQuotes.crossSwap.refundOnOrigin - ? crossSwapQuotes.bridgeQuote.inputToken - : crossSwapQuotes.bridgeQuote.outputToken, + refundToken: + refundToken.symbol === "ETH" + ? { + ...refundToken, + symbol: "WETH", + } + : refundToken, inputAmount: crossSwapQuotes.originSwapQuote?.expectedAmountIn.toString() ?? crossSwapQuotes.bridgeQuote.inputAmount.toString(), diff --git a/scripts/tests/swap-allowance.ts b/scripts/tests/swap-allowance.ts index aed95c357..3496c144d 100644 --- a/scripts/tests/swap-allowance.ts +++ b/scripts/tests/swap-allowance.ts @@ -201,6 +201,11 @@ async function swap() { ); console.log(response.data); + if (!response.data.tx.simulationSuccess) { + console.error("Tx simulation failed"); + continue; + } + if (process.env.DEV_WALLET_PK) { const wallet = new Wallet(process.env.DEV_WALLET_PK!).connect( getProvider(testCase.params.originChainId) From 8ca16587da45f86f7540888c0fcd76d9d0f4c8e0 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Sat, 23 Nov 2024 16:13:31 +0700 Subject: [PATCH 20/47] fix: amount adjustments --- api/_dexes/cross-swap.ts | 6 +++--- api/_dexes/uniswap.ts | 2 +- api/_dexes/utils.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/_dexes/cross-swap.ts b/api/_dexes/cross-swap.ts index 86a108f0c..f1315d7d5 100644 --- a/api/_dexes/cross-swap.ts +++ b/api/_dexes/cross-swap.ts @@ -195,7 +195,7 @@ export async function buildCrossSwapTxForAllowanceHolder( deposit, { value: crossSwapQuotes.crossSwap.isInputNative - ? deposit.inputAmount + ? crossSwapQuotes.originSwapQuote.maximumAmountIn : 0, } ); @@ -244,8 +244,8 @@ async function extractDepositDataStruct(crossSwapQuotes: CrossSwapQuotes) { ? refundAddress : crossSwapQuotes.crossSwap.depositor, recipient: utils.isMessageEmpty(message) - ? getMultiCallHandlerAddress(destinationChainId) - : crossSwapQuotes.crossSwap.recipient, + ? crossSwapQuotes.crossSwap.recipient + : getMultiCallHandlerAddress(destinationChainId), inputToken: crossSwapQuotes.bridgeQuote.inputToken.address, outputToken: crossSwapQuotes.bridgeQuote.outputToken.address, inputAmount: crossSwapQuotes.bridgeQuote.inputAmount, diff --git a/api/_dexes/uniswap.ts b/api/_dexes/uniswap.ts index 7eab8fe7a..87108dfc8 100644 --- a/api/_dexes/uniswap.ts +++ b/api/_dexes/uniswap.ts @@ -287,7 +287,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( { ...originSwap, amount: addSlippageToAmount( - bridgeQuote.inputAmount, + adjOriginSwapQuote.maximumAmountIn, crossSwap.slippageTolerance.toString() ), }, diff --git a/api/_dexes/utils.ts b/api/_dexes/utils.ts index d4d6ba962..050aa226f 100644 --- a/api/_dexes/utils.ts +++ b/api/_dexes/utils.ts @@ -139,7 +139,7 @@ export function buildMinOutputBridgeTokenMessage( { target: crossSwap.recipient, callData: "0x", - value: crossSwap.amount.toString(), + value: (unwrapAmount || crossSwap.amount).toString(), }, ] : // ERC-20 token transfer From 0357a38b9546a3b393b43feacb7a4d09c8734de1 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Sat, 23 Nov 2024 16:14:01 +0700 Subject: [PATCH 21/47] feat: add approvalTxns to response --- api/swap/_utils.ts | 30 ++++++++++++++++++++++++++++++ api/swap/allowance.ts | 26 ++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/api/swap/_utils.ts b/api/swap/_utils.ts index a6e49a3aa..36476a031 100644 --- a/api/swap/_utils.ts +++ b/api/swap/_utils.ts @@ -17,6 +17,8 @@ import { } from "../_dexes/cross-swap"; import { InvalidParamError } from "../_errors"; import { isValidIntegratorId } from "../_integrator-id"; +import { Token } from "../_dexes/types"; +import { encodeApproveCalldata } from "../_multicall-handler"; export const BaseSwapQueryParamsSchema = type({ amount: positiveIntStr(), @@ -128,3 +130,31 @@ export async function handleBaseSwapQueryParams({ isInputNative, }; } + +export function getApprovalTxns(params: { + token: Token; + spender: string; + amount: BigNumber; +}) { + const approvalTxns: { + chainId: number; + to: string; + data: string; + }[] = []; + // USDT has a different approval flow when changing an already approve amount. + // We need to set the allowance to 0 first. + // See https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#code#L201 + if (params.token.symbol === "USDT") { + approvalTxns.push({ + chainId: params.token.chainId, + to: params.token.address, + data: encodeApproveCalldata(params.spender, BigNumber.from(0)), + }); + } + approvalTxns.push({ + chainId: params.token.chainId, + to: params.token.address, + data: encodeApproveCalldata(params.spender, params.amount), + }); + return approvalTxns; +} diff --git a/api/swap/allowance.ts b/api/swap/allowance.ts index 8736603a0..3978777c3 100644 --- a/api/swap/allowance.ts +++ b/api/swap/allowance.ts @@ -9,7 +9,11 @@ import { latestGasPriceCache, } from "../_utils"; import { buildCrossSwapTxForAllowanceHolder } from "../_dexes/cross-swap"; -import { handleBaseSwapQueryParams, BaseSwapQueryParams } from "./_utils"; +import { + handleBaseSwapQueryParams, + BaseSwapQueryParams, + getApprovalTxns, +} from "./_utils"; import { getBalanceAndAllowance } from "../_erc20"; const handler = async ( @@ -70,6 +74,23 @@ const handler = async ( originTxGasPrice = await latestGasPriceCache(originChainId).get(); } + let approvalTxns: + | { + chainId: number; + to: string; + data: string; + }[] + | undefined; + // @TODO: Allow for just enough approval amount to be set. + const approvalAmount = constants.MaxUint256; + if (allowance.lt(inputAmount)) { + approvalTxns = getApprovalTxns({ + token: crossSwapQuotes.crossSwap.inputToken, + spender: crossSwapTx.to, + amount: approvalAmount, + }); + } + const refundToken = crossSwapQuotes.crossSwap.refundOnOrigin ? crossSwapQuotes.bridgeQuote.inputToken : crossSwapQuotes.bridgeQuote.outputToken; @@ -88,7 +109,8 @@ const handler = async ( expected: inputAmount.toString(), }, }, - tx: { + approvalTxns, + swapTx: { simulationSuccess: !!originTxGas, chainId: originChainId, to: crossSwapTx.to, From bd9e370084cc8a581b568fda91164462393eafe9 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Sat, 23 Nov 2024 16:14:24 +0700 Subject: [PATCH 22/47] test: improve test script --- scripts/tests/swap-allowance.ts | 204 ++++++++++++++------------------ 1 file changed, 86 insertions(+), 118 deletions(-) diff --git a/scripts/tests/swap-allowance.ts b/scripts/tests/swap-allowance.ts index 3496c144d..c3f022ab2 100644 --- a/scripts/tests/swap-allowance.ts +++ b/scripts/tests/swap-allowance.ts @@ -10,174 +10,125 @@ dotenv.config(); */ const depositor = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D"; +const originChainId = CHAIN_IDs.OPTIMISM; +const destinationChainId = CHAIN_IDs.ARBITRUM; +const anyDestinationOutputTokens = { + [CHAIN_IDs.ARBITRUM]: "0x74885b4D524d497261259B38900f54e6dbAd2210", // APE +}; + const MIN_OUTPUT_CASES = [ // B2B { - labels: ["B2B", "MIN_OUTPUT", "Base USDC - Arbitrum USDC"], + labels: ["B2B", "MIN_OUTPUT", "USDC - USDC"], params: { amount: ethers.utils.parseUnits("1", 6).toString(), tradeType: "minOutput", - inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], - originChainId: CHAIN_IDs.BASE, - outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], - destinationChainId: CHAIN_IDs.ARBITRUM, + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[originChainId], + originChainId, + outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[destinationChainId], + destinationChainId, depositor, }, }, { - labels: ["B2B", "MIN_OUTPUT", "Base USDC - Arbitrum ETH"], + labels: ["B2B", "MIN_OUTPUT", "Native ETH - Native ETH"], params: { amount: ethers.utils.parseUnits("0.001", 18).toString(), tradeType: "minOutput", - inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], - originChainId: CHAIN_IDs.BASE, - outputToken: ethers.constants.AddressZero, - destinationChainId: CHAIN_IDs.ARBITRUM, - depositor, - }, - }, - { - labels: ["B2B", "MIN_OUTPUT", "Arbitrum ETH - Base USDC"], - params: { - amount: ethers.utils.parseUnits("3", 6).toString(), - tradeType: "minOutput", inputToken: ethers.constants.AddressZero, - originChainId: CHAIN_IDs.ARBITRUM, - outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], - destinationChainId: CHAIN_IDs.BASE, - depositor, - }, - }, - // B2A - { - labels: ["B2A", "MIN_OUTPUT", "Base USDC - Arbitrum WETH"], - params: { - amount: ethers.utils.parseUnits("0.001", 18).toString(), - tradeType: "minOutput", - inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], - originChainId: CHAIN_IDs.BASE, - outputToken: TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.ARBITRUM], - destinationChainId: CHAIN_IDs.ARBITRUM, + originChainId, + outputToken: ethers.constants.AddressZero, + destinationChainId, depositor, }, }, { - labels: ["B2A", "MIN_OUTPUT", "Base USDC - Arbitrum ETH"], + labels: ["B2B", "MIN_OUTPUT", "USDC - Native ETH"], params: { amount: ethers.utils.parseUnits("0.001", 18).toString(), tradeType: "minOutput", - inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], - originChainId: CHAIN_IDs.BASE, + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[originChainId], + originChainId, outputToken: ethers.constants.AddressZero, - destinationChainId: CHAIN_IDs.ARBITRUM, + destinationChainId, depositor, }, }, - // A2B { - labels: ["A2B", "MIN_OUTPUT", "Base USDbC - Arbitrum USDC"], + labels: ["B2B", "MIN_OUTPUT", "Native ETH - USDC"], params: { - amount: ethers.utils.parseUnits("1", 6).toString(), + amount: ethers.utils.parseUnits("3", 6).toString(), tradeType: "minOutput", - inputToken: TOKEN_SYMBOLS_MAP.USDbC.addresses[CHAIN_IDs.BASE], - originChainId: CHAIN_IDs.BASE, - outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], - destinationChainId: CHAIN_IDs.ARBITRUM, + inputToken: ethers.constants.AddressZero, + originChainId, + outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[destinationChainId], + destinationChainId, depositor, }, }, - // A2A + // B2A { - labels: ["A2A", "MIN_OUTPUT", "Base USDbC - Arbitrum APE"], + labels: ["B2A", "MIN_OUTPUT", "USDC - APE"], params: { amount: ethers.utils.parseUnits("1", 18).toString(), tradeType: "minOutput", - inputToken: TOKEN_SYMBOLS_MAP.USDbC.addresses[CHAIN_IDs.BASE], - originChainId: CHAIN_IDs.BASE, - outputToken: "0x74885b4D524d497261259B38900f54e6dbAd2210", // APE Coin - destinationChainId: CHAIN_IDs.ARBITRUM, - depositor, - }, - }, -]; -const EXACT_OUTPUT_CASES = [ - // B2B - { - labels: ["B2B", "EXACT_OUTPUT", "Base USDC - Arbitrum USDC"], - params: { - amount: ethers.utils.parseUnits("1", 6).toString(), - tradeType: "exactOutput", - inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], - originChainId: CHAIN_IDs.BASE, - outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], - destinationChainId: CHAIN_IDs.ARBITRUM, - depositor, - }, - }, - { - labels: ["B2B", "EXACT_OUTPUT", "Base USDC -Arbitrum ETH"], - params: { - amount: ethers.utils.parseUnits("0.001", 18).toString(), - tradeType: "exactOutput", - inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], - originChainId: CHAIN_IDs.BASE, - outputToken: ethers.constants.AddressZero, - destinationChainId: CHAIN_IDs.ARBITRUM, - depositor, - }, - }, - // B2A - { - labels: ["B2A", "EXACT_OUTPUT", "Base USDC - Arbitrum WETH"], - params: { - amount: ethers.utils.parseUnits("0.001", 18).toString(), - tradeType: "exactOutput", - inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], - originChainId: CHAIN_IDs.BASE, - outputToken: TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.ARBITRUM], - destinationChainId: CHAIN_IDs.ARBITRUM, + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[originChainId], + originChainId, + outputToken: anyDestinationOutputTokens[destinationChainId], + destinationChainId, depositor, }, }, { - labels: ["B2A", "EXACT_OUTPUT", "Base USDC - Arbitrum ETH"], + labels: ["B2A", "MIN_OUTPUT", "USDC - Native ETH"], params: { amount: ethers.utils.parseUnits("0.001", 18).toString(), - tradeType: "exactOutput", - inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], - originChainId: CHAIN_IDs.BASE, + tradeType: "minOutput", + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[originChainId], + originChainId, outputToken: ethers.constants.AddressZero, - destinationChainId: CHAIN_IDs.ARBITRUM, + destinationChainId, depositor, }, }, // A2B { - labels: ["A2B", "EXACT_OUTPUT", "Base USDbC - Arbitrum USDC"], + labels: ["A2B", "MIN_OUTPUT", "bridged USDC - USDC"], params: { amount: ethers.utils.parseUnits("1", 6).toString(), - tradeType: "exactOutput", - inputToken: TOKEN_SYMBOLS_MAP.USDbC.addresses[CHAIN_IDs.BASE], - originChainId: CHAIN_IDs.BASE, - outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], - destinationChainId: CHAIN_IDs.ARBITRUM, + tradeType: "minOutput", + inputToken: + TOKEN_SYMBOLS_MAP["USDC.e"].addresses[originChainId] || + TOKEN_SYMBOLS_MAP.USDbC.addresses[originChainId], + originChainId, + outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[destinationChainId], + destinationChainId, depositor, }, }, // A2A { - labels: ["A2A", "EXACT_OUTPUT", "Base USDbC - Arbitrum APE"], + labels: ["A2A", "MIN_OUTPUT", "bridged USDC - APE"], params: { amount: ethers.utils.parseUnits("1", 18).toString(), - tradeType: "exactOutput", - inputToken: TOKEN_SYMBOLS_MAP.USDbC.addresses[CHAIN_IDs.BASE], + tradeType: "minOutput", + inputToken: + TOKEN_SYMBOLS_MAP["USDC.e"].addresses[originChainId] || + TOKEN_SYMBOLS_MAP.USDbC.addresses[originChainId], originChainId: CHAIN_IDs.BASE, - outputToken: "0x74885b4D524d497261259B38900f54e6dbAd2210", // APE Coin - destinationChainId: CHAIN_IDs.ARBITRUM, + outputToken: anyDestinationOutputTokens[destinationChainId], // APE Coin + destinationChainId, depositor, }, }, ]; +const EXACT_OUTPUT_CASES = MIN_OUTPUT_CASES.map((testCase) => ({ + labels: testCase.labels.map((label) => label.replace("MIN", "EXACT")), + params: { + ...testCase.params, + tradeType: "exactOutput", + }, +})); async function swap() { const filterString = process.argv[2]; @@ -201,22 +152,34 @@ async function swap() { ); console.log(response.data); - if (!response.data.tx.simulationSuccess) { - console.error("Tx simulation failed"); - continue; - } - if (process.env.DEV_WALLET_PK) { const wallet = new Wallet(process.env.DEV_WALLET_PK!).connect( getProvider(testCase.params.originChainId) ); + + if (response.data.approvalTxns) { + console.log("Approval needed..."); + let step = 1; + for (const approvalTxn of response.data.approvalTxns) { + const stepLabel = `(${step}/${response.data.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: response.data.tx.to, - data: response.data.tx.data, - value: response.data.tx.value, - gasLimit: response.data.tx.gas, - gasPrice: response.data.tx.gasPrice, + to: response.data.swapTx.to, + data: response.data.swapTx.data, + value: response.data.swapTx.value, + gasLimit: response.data.swapTx.gas, + gasPrice: response.data.swapTx.gasPrice, }); console.log("Tx hash: ", tx.hash); await tx.wait(); @@ -230,4 +193,9 @@ async function swap() { swap() .then(() => console.log("Done")) - .catch(console.error); + .catch((e) => { + console.error(e); + if (e.response?.data) { + console.log("Tx for debug sim:", e.response.data.transaction); + } + }); From 820d4720d0eff822cd8467264e2a2f55daf23077 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Mon, 25 Nov 2024 21:25:00 +0700 Subject: [PATCH 23/47] feat: support old `SwapAndBridge` and new periphery (#1287) * feat: recover old `SwapAndBridge` func + more explicit separation to upcoming periphery contract * fix: drain leftover swap input tokens to refund * fixup --- api/_dexes/cross-swap.ts | 12 +- api/_dexes/uniswap/swap-quoter.ts | 215 +++++ .../{uniswap.ts => uniswap/swap-router-02.ts} | 118 +-- api/_dexes/uniswap/utils.ts | 31 + api/_dexes/utils.ts | 4 +- api/_spoke-pool-periphery.ts | 38 + ...apAndBridge.ts => SpokePoolV3Periphery.ts} | 261 ++++-- .../SwapAndBridge.sol/SwapAndBridge.ts | 345 ------- .../SwapAndBridge.sol/SwapAndBridgeBase.ts | 211 ----- api/_typechain/SwapAndBridge.sol/index.ts | 6 - .../factories/SpokePoolV3Periphery.ts | 838 ++++++++++++++++++ .../SwapAndBridgeBase__factory.ts | 153 ---- .../SwapAndBridge__factory.ts | 367 -------- .../UniversalSwapAndBridge__factory.ts | 777 ---------------- .../factories/SwapAndBridge.sol/index.ts | 6 - api/swap-quote.ts | 4 +- scripts/generate-routes.ts | 33 +- ...24e63716afAcE30C9a417E0542281869f7d9e.json | 3 + ...6fA914353c44b2E33eBE05f21846F1048bEda.json | 15 +- src/utils/bridge.ts | 2 - src/utils/config.ts | 8 +- 21 files changed, 1392 insertions(+), 2055 deletions(-) create mode 100644 api/_dexes/uniswap/swap-quoter.ts rename api/_dexes/{uniswap.ts => uniswap/swap-router-02.ts} (86%) create mode 100644 api/_dexes/uniswap/utils.ts create mode 100644 api/_spoke-pool-periphery.ts rename api/_typechain/{SwapAndBridge.sol/UniversalSwapAndBridge.ts => SpokePoolV3Periphery.ts} (71%) delete mode 100644 api/_typechain/SwapAndBridge.sol/SwapAndBridge.ts delete mode 100644 api/_typechain/SwapAndBridge.sol/SwapAndBridgeBase.ts delete mode 100644 api/_typechain/SwapAndBridge.sol/index.ts create mode 100644 api/_typechain/factories/SpokePoolV3Periphery.ts delete mode 100644 api/_typechain/factories/SwapAndBridge.sol/SwapAndBridgeBase__factory.ts delete mode 100644 api/_typechain/factories/SwapAndBridge.sol/SwapAndBridge__factory.ts delete mode 100644 api/_typechain/factories/SwapAndBridge.sol/UniversalSwapAndBridge__factory.ts delete mode 100644 api/_typechain/factories/SwapAndBridge.sol/index.ts diff --git a/api/_dexes/cross-swap.ts b/api/_dexes/cross-swap.ts index f1315d7d5..ede9bcfcb 100644 --- a/api/_dexes/cross-swap.ts +++ b/api/_dexes/cross-swap.ts @@ -13,13 +13,13 @@ import { getUniswapCrossSwapQuotesForOutputB2A, getUniswapCrossSwapQuotesForOutputA2B, getBestUniswapCrossSwapQuotesForOutputA2A, -} from "./uniswap"; +} from "./uniswap/swap-router-02"; import { CrossSwap, CrossSwapQuotes } from "./types"; import { buildExactOutputBridgeTokenMessage, buildMinOutputBridgeTokenMessage, - getSwapAndBridge, } from "./utils"; +import { getSpokePoolPeriphery } from "../_spoke-pool-periphery"; import { tagIntegratorId } from "../_integrator-id"; import { getMultiCallHandlerAddress } from "../_multicall-handler"; @@ -179,14 +179,14 @@ export async function buildCrossSwapTxForAllowanceHolder( ) { const originChainId = crossSwapQuotes.crossSwap.inputToken.chainId; const spokePool = getSpokePool(originChainId); + const spokePoolPeriphery = getSpokePoolPeriphery("uniswap", originChainId); const deposit = await extractDepositDataStruct(crossSwapQuotes); let tx: PopulatedTransaction; let toAddress: string; if (crossSwapQuotes.originSwapQuote) { - const swapAndBridge = getSwapAndBridge("uniswap", originChainId); - tx = await swapAndBridge.populateTransaction.swapAndBridge( + tx = await spokePoolPeriphery.populateTransaction.swapAndBridge( crossSwapQuotes.originSwapQuote.tokenIn.address, crossSwapQuotes.originSwapQuote.tokenOut.address, crossSwapQuotes.originSwapQuote.swapTx.data, @@ -199,7 +199,7 @@ export async function buildCrossSwapTxForAllowanceHolder( : 0, } ); - toAddress = swapAndBridge.address; + toAddress = spokePoolPeriphery.address; } else { tx = await spokePool.populateTransaction.depositV3( deposit.depositor, @@ -257,6 +257,8 @@ async function extractDepositDataStruct(crossSwapQuotes: CrossSwapQuotes) { fillDeadline: await getFillDeadline(spokePool), exclusivityDeadline: crossSwapQuotes.bridgeQuote.suggestedFees.exclusivityDeadline, + exclusivityParameter: + crossSwapQuotes.bridgeQuote.suggestedFees.exclusivityDeadline, message, }; return deposit; diff --git a/api/_dexes/uniswap/swap-quoter.ts b/api/_dexes/uniswap/swap-quoter.ts new file mode 100644 index 000000000..f22768cae --- /dev/null +++ b/api/_dexes/uniswap/swap-quoter.ts @@ -0,0 +1,215 @@ +import { ethers } from "ethers"; +import { + FeeAmount, + Pool, + Route, + SwapOptions, + SwapQuoter, + SwapRouter, + Trade, + computePoolAddress, +} from "@uniswap/v3-sdk"; +import { + Currency, + CurrencyAmount, + Percent, + Token, + TradeType, +} from "@uniswap/sdk-core"; +import JSBI from "jsbi"; +import IUniswapV3PoolABI from "@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json"; +import { CHAIN_IDs } from "@across-protocol/constants"; +import { utils } from "@across-protocol/sdk"; + +import { Swap } from "../types"; +import { getSwapAndBridgeAddress } from "../utils"; +import { getProdToken } from "./utils"; +import { callViaMulticall3, getProvider } from "../../_utils"; + +// https://docs.uniswap.org/contracts/v3/reference/deployments/ +const POOL_FACTORY_CONTRACT_ADDRESS = { + [CHAIN_IDs.MAINNET]: "0x1F98431c8aD98523631AE4a59f267346ea31F984", + [CHAIN_IDs.OPTIMISM]: "0x1F98431c8aD98523631AE4a59f267346ea31F984", + [CHAIN_IDs.ARBITRUM]: "0x1F98431c8aD98523631AE4a59f267346ea31F984", + [CHAIN_IDs.BASE]: "0x33128a8fC17869897dcE68Ed026d694621f6FDfD", + [CHAIN_IDs.POLYGON]: "0x1F98431c8aD98523631AE4a59f267346ea31F984", +}; +const QUOTER_CONTRACT_ADDRESS = { + [CHAIN_IDs.MAINNET]: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e", + [CHAIN_IDs.OPTIMISM]: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e", + [CHAIN_IDs.ARBITRUM]: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e", + [CHAIN_IDs.BASE]: "0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a", + [CHAIN_IDs.POLYGON]: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e", +}; + +type SwapParam = Omit; + +export async function getUniswapQuoteWithSwapQuoter(swap: SwapParam) { + const swapAndBridgeAddress = getSwapAndBridgeAddress( + "uniswap", + swap.tokenIn.chainId + ); + + const initialTokenIn = { ...swap.tokenIn }; + const initialTokenOut = { ...swap.tokenOut }; + + // Always use mainnet tokens for retrieving quote, so that we can get equivalent quotes + // for testnet tokens. + swap.tokenIn = getProdToken(swap.tokenIn); + swap.tokenOut = getProdToken(swap.tokenOut); + + const poolInfo = await getPoolInfo(swap); + const tokenA = new Token( + swap.tokenIn.chainId, + swap.tokenIn.address, + swap.tokenIn.decimals + ); + const tokenB = new Token( + swap.tokenOut.chainId, + swap.tokenOut.address, + swap.tokenOut.decimals + ); + const pool = new Pool( + tokenA, + tokenB, + FeeAmount.LOW, + poolInfo.sqrtPriceX96.toString(), + poolInfo.liquidity.toString(), + poolInfo.tick + ); + + const swapRoute = new Route([pool], tokenA, tokenB); + + const amountOut = await getOutputQuote(swap, swapRoute); + + const uncheckedTrade = Trade.createUncheckedTrade({ + route: swapRoute, + inputAmount: CurrencyAmount.fromRawAmount(tokenA, swap.amount), + outputAmount: CurrencyAmount.fromRawAmount(tokenB, JSBI.BigInt(amountOut)), + // @TODO: Support other trade types + tradeType: TradeType.EXACT_INPUT, + }); + + const options: SwapOptions = { + slippageTolerance: new Percent( + // max. slippage decimals is 2 + Number(swap.slippageTolerance.toFixed(2)) * 100, + 10_000 + ), + // 20 minutes from the current Unix time + deadline: utils.getCurrentTime() + 60 * 20, + recipient: swapAndBridgeAddress, + }; + + const methodParameters = SwapRouter.swapCallParameters( + [uncheckedTrade], + options + ); + + // replace mainnet token addresses with initial token addresses in calldata + methodParameters.calldata = methodParameters.calldata.replace( + swap.tokenIn.address.toLowerCase().slice(2), + initialTokenIn.address.toLowerCase().slice(2) + ); + methodParameters.calldata = methodParameters.calldata.replace( + swap.tokenOut.address.toLowerCase().slice(2), + initialTokenOut.address.toLowerCase().slice(2) + ); + + return { + minExpectedInputTokenAmount: ethers.utils + .parseUnits( + uncheckedTrade.minimumAmountOut(options.slippageTolerance).toExact(), + swap.tokenOut.decimals + ) + .toString(), + routerCalldata: methodParameters.calldata, + value: ethers.BigNumber.from(methodParameters.value).toString(), + swapAndBridgeAddress, + dex: "uniswap", + slippage: swap.slippageTolerance, + }; +} + +async function getOutputQuote( + swap: SwapParam, + route: Route +) { + const provider = getProvider(route.chainId); + const { calldata } = SwapQuoter.quoteCallParameters( + route, + CurrencyAmount.fromRawAmount( + new Token( + swap.tokenIn.chainId, + swap.tokenIn.address, + swap.tokenIn.decimals + ), + swap.amount + ), + // @TODO: Support other trade types + TradeType.EXACT_INPUT, + { useQuoterV2: true } + ); + const quoteCallReturnData = await provider.call({ + to: QUOTER_CONTRACT_ADDRESS[swap.tokenIn.chainId], + data: calldata, + }); + + return ethers.utils.defaultAbiCoder.decode(["uint256"], quoteCallReturnData); +} + +async function getPoolInfo({ tokenIn, tokenOut }: SwapParam): Promise<{ + token0: string; + token1: string; + fee: number; + tickSpacing: number; + sqrtPriceX96: ethers.BigNumber; + liquidity: ethers.BigNumber; + tick: number; +}> { + const provider = getProvider(tokenIn.chainId); + const poolContract = new ethers.Contract( + computePoolAddress({ + factoryAddress: POOL_FACTORY_CONTRACT_ADDRESS[tokenIn.chainId], + tokenA: new Token(tokenIn.chainId, tokenIn.address, tokenIn.decimals), + tokenB: new Token(tokenOut.chainId, tokenOut.address, tokenOut.decimals), + fee: FeeAmount.LOW, + }), + IUniswapV3PoolABI.abi, + provider + ); + const poolMethods = [ + "token0", + "token1", + "fee", + "tickSpacing", + "liquidity", + "slot0", + ]; + const [token0, token1, fee, tickSpacing, liquidity, slot0] = + await callViaMulticall3( + provider, + poolMethods.map((method) => ({ + contract: poolContract, + functionName: method, + })) + ); + + return { + token0, + token1, + fee, + tickSpacing, + liquidity, + sqrtPriceX96: slot0[0], + tick: slot0[1], + } as unknown as { + token0: string; + token1: string; + fee: number; + tickSpacing: number; + sqrtPriceX96: ethers.BigNumber; + liquidity: ethers.BigNumber; + tick: number; + }; +} diff --git a/api/_dexes/uniswap.ts b/api/_dexes/uniswap/swap-router-02.ts similarity index 86% rename from api/_dexes/uniswap.ts rename to api/_dexes/uniswap/swap-router-02.ts index 87108dfc8..580c672b7 100644 --- a/api/_dexes/uniswap.ts +++ b/api/_dexes/uniswap/swap-router-02.ts @@ -15,8 +15,7 @@ import { getBridgeQuoteForMinOutput, getRoutesByChainIds, getRouteByOutputTokenAndOriginChain, -} from "../_utils"; -import { TOKEN_SYMBOLS_MAP } from "../_constants"; +} from "../../_utils"; import { buildMulticallHandlerMessage, encodeApproveCalldata, @@ -24,22 +23,16 @@ import { encodeTransferCalldata, encodeWethWithdrawCalldata, getMultiCallHandlerAddress, -} from "../_multicall-handler"; -import { - OriginSwapQuoteAndCalldata, - Token as AcrossToken, - Swap, - CrossSwap, - SwapQuote, -} from "./types"; +} from "../../_multicall-handler"; +import { Token as AcrossToken, Swap, CrossSwap, SwapQuote } from "../types"; import { buildExactOutputBridgeTokenMessage, buildMinOutputBridgeTokenMessage, getFallbackRecipient, getSwapAndBridgeAddress, NoSwapRouteError, -} from "./utils"; -import { AMOUNT_TYPE } from "./cross-swap"; +} from "../utils"; +import { AMOUNT_TYPE } from "../cross-swap"; // Taken from here: https://docs.uniswap.org/contracts/v3/reference/deployments/ export const SWAP_ROUTER_02_ADDRESS = { @@ -54,59 +47,6 @@ export const SWAP_ROUTER_02_ADDRESS = { [CHAIN_IDs.ZORA]: "0x7De04c96BE5159c3b5CeffC82aa176dc81281557", }; -// Maps testnet chain IDs to their prod counterparts. Used to get the prod token -// info for testnet tokens. -const TESTNET_TO_PROD = { - [CHAIN_IDs.SEPOLIA]: CHAIN_IDs.MAINNET, - [CHAIN_IDs.BASE_SEPOLIA]: CHAIN_IDs.BASE, - [CHAIN_IDs.OPTIMISM_SEPOLIA]: CHAIN_IDs.OPTIMISM, - [CHAIN_IDs.ARBITRUM_SEPOLIA]: CHAIN_IDs.ARBITRUM, -}; - -/** - * Returns Uniswap v3 quote for a swap with exact input amount. - * @param swap - */ -export async function getUniswapQuoteForOriginSwapExactInput( - swap: Omit -): Promise { - const swapAndBridgeAddress = getSwapAndBridgeAddress("uniswap", swap.chainId); - - const initialTokenIn = { ...swap.tokenIn }; - const initialTokenOut = { ...swap.tokenOut }; - // Always use mainnet tokens for retrieving quote, so that we can get equivalent quotes - // for testnet tokens. - swap.tokenIn = getProdToken(swap.tokenIn); - swap.tokenOut = getProdToken(swap.tokenOut); - - const { swapTx, minAmountOut } = await getUniswapQuote( - { - ...swap, - recipient: swapAndBridgeAddress, - }, - TradeType.EXACT_INPUT - ); - - // replace mainnet token addresses with initial token addresses in calldata - swapTx.data = swapTx.data.replace( - swap.tokenIn.address.toLowerCase().slice(2), - initialTokenIn.address.toLowerCase().slice(2) - ); - swapTx.data = swapTx.data.replace( - swap.tokenOut.address.toLowerCase().slice(2), - initialTokenOut.address.toLowerCase().slice(2) - ); - - return { - minExpectedInputTokenAmount: minAmountOut.toString(), - routerCalldata: swapTx.data, - value: BigNumber.from(swapTx.value).toString(), - swapAndBridgeAddress, - dex: "uniswap", - slippage: swap.slippageTolerance, - }; -} - /** * Returns Uniswap v3 quote for a swap with min. output amount for route * BRIDGEABLE input token -> ANY output token, e.g. USDC -> ARB. Required steps: @@ -156,7 +96,7 @@ export async function getUniswapCrossSwapQuotesForOutputB2A( }; // 1.1. Get destination swap quote for bridgeable output token -> any token // with exact output amount. - let destinationSwapQuote = await getUniswapQuote( + let destinationSwapQuote = await getUniswapQuoteWithSwapRouter02( { ...destinationSwap, amount: crossSwap.amount.toString(), @@ -166,7 +106,7 @@ export async function getUniswapCrossSwapQuotesForOutputB2A( // 1.2. Re-fetch destination swap quote with exact input amount if leftover tokens // should be sent to receiver. if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT) { - destinationSwapQuote = await getUniswapQuote( + destinationSwapQuote = await getUniswapQuoteWithSwapRouter02( { ...destinationSwap, amount: addSlippageToAmount( @@ -265,7 +205,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( slippageTolerance: crossSwap.slippageTolerance, }; // 2.1. Get origin swap quote for any input token -> bridgeable input token - const originSwapQuote = await getUniswapQuote( + const originSwapQuote = await getUniswapQuoteWithSwapRouter02( { ...originSwap, amount: bridgeQuote.inputAmount.toString(), @@ -274,7 +214,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( ); // 2.2. Re-fetch origin swap quote with updated input amount and EXACT_INPUT type. // This prevents leftover tokens in the SwapAndBridge contract. - let adjOriginSwapQuote = await getUniswapQuote( + let adjOriginSwapQuote = await getUniswapQuoteWithSwapRouter02( { ...originSwap, amount: originSwapQuote.maximumAmountIn.toString(), @@ -283,7 +223,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( ); if (adjOriginSwapQuote.minAmountOut.lt(bridgeQuote.inputAmount)) { - adjOriginSwapQuote = await getUniswapQuote( + adjOriginSwapQuote = await getUniswapQuoteWithSwapRouter02( { ...originSwap, amount: addSlippageToAmount( @@ -435,7 +375,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( // 1.1. Get destination swap quote for bridgeable output token -> any token // with exact output amount - let destinationSwapQuote = await getUniswapQuote( + let destinationSwapQuote = await getUniswapQuoteWithSwapRouter02( { ...destinationSwap, amount: crossSwap.amount.toString(), @@ -445,7 +385,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( // 1.2. Re-fetch destination swap quote with exact input amount if leftover tokens // should be sent to receiver. if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT) { - destinationSwapQuote = await getUniswapQuote( + destinationSwapQuote = await getUniswapQuoteWithSwapRouter02( { ...destinationSwap, amount: addSlippageToAmount( @@ -472,7 +412,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( }); // 3.1. Get origin swap quote for any input token -> bridgeable input token - const originSwapQuote = await getUniswapQuote( + const originSwapQuote = await getUniswapQuoteWithSwapRouter02( { ...originSwap, amount: bridgeQuote.inputAmount.toString(), @@ -481,7 +421,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( ); // 3.2. Re-fetch origin swap quote with updated input amount and EXACT_INPUT type. // This prevents leftover tokens in the SwapAndBridge contract. - const adjOriginSwapQuote = await getUniswapQuote( + const adjOriginSwapQuote = await getUniswapQuoteWithSwapRouter02( { ...originSwap, amount: originSwapQuote.maximumAmountIn.toString(), @@ -497,11 +437,11 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( }; } -export async function getUniswapQuote( +export async function getUniswapQuoteWithSwapRouter02( swap: Omit, tradeType: TradeType ): Promise { - const { router, options } = getSwapRouterAndOptions(swap); + const { router, options } = getSwapRouter02AndOptions(swap); const amountCurrency = tradeType === TradeType.EXACT_INPUT ? swap.tokenIn : swap.tokenOut; @@ -578,7 +518,7 @@ export async function getUniswapQuote( return swapQuote; } -function getSwapRouterAndOptions(params: { +function getSwapRouter02AndOptions(params: { chainId: number; recipient: string; slippageTolerance: number; @@ -608,26 +548,6 @@ function floatToPercent(value: number) { ); } -function getProdToken(token: AcrossToken) { - const prodChainId = TESTNET_TO_PROD[token.chainId] || token.chainId; - - const prodToken = - TOKEN_SYMBOLS_MAP[token.symbol as keyof typeof TOKEN_SYMBOLS_MAP]; - const prodTokenAddress = prodToken?.addresses[prodChainId]; - - if (!prodToken || !prodTokenAddress) { - throw new Error( - `Prod token not found for ${token.symbol} on chain ${token.chainId}` - ); - } - - return { - ...prodToken, - chainId: prodChainId, - address: prodTokenAddress, - }; -} - function buildDestinationSwapCrossChainMessage({ crossSwap, destinationSwapQuote, @@ -721,9 +641,7 @@ function buildDestinationSwapCrossChainMessage({ target: getMultiCallHandlerAddress(destinationSwapChainId), callData: encodeDrainCalldata( bridgeableOutputToken.address, - crossSwap.type === AMOUNT_TYPE.EXACT_OUTPUT - ? crossSwap.refundAddress ?? crossSwap.depositor - : crossSwap.recipient + crossSwap.refundAddress ?? crossSwap.depositor ), value: "0", }, diff --git a/api/_dexes/uniswap/utils.ts b/api/_dexes/uniswap/utils.ts new file mode 100644 index 000000000..66d218f4b --- /dev/null +++ b/api/_dexes/uniswap/utils.ts @@ -0,0 +1,31 @@ +import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants"; +import { Token } from "../types"; + +// Maps testnet chain IDs to their prod counterparts. Used to get the prod token +// info for testnet tokens. +const TESTNET_TO_PROD = { + [CHAIN_IDs.SEPOLIA]: CHAIN_IDs.MAINNET, + [CHAIN_IDs.BASE_SEPOLIA]: CHAIN_IDs.BASE, + [CHAIN_IDs.OPTIMISM_SEPOLIA]: CHAIN_IDs.OPTIMISM, + [CHAIN_IDs.ARBITRUM_SEPOLIA]: CHAIN_IDs.ARBITRUM, +}; + +export function getProdToken(token: Token) { + const prodChainId = TESTNET_TO_PROD[token.chainId] || token.chainId; + + const prodToken = + TOKEN_SYMBOLS_MAP[token.symbol as keyof typeof TOKEN_SYMBOLS_MAP]; + const prodTokenAddress = prodToken?.addresses[prodChainId]; + + if (!prodToken || !prodTokenAddress) { + throw new Error( + `Prod token not found for ${token.symbol} on chain ${token.chainId}` + ); + } + + return { + ...prodToken, + chainId: prodChainId, + address: prodTokenAddress, + }; +} diff --git a/api/_dexes/utils.ts b/api/_dexes/utils.ts index 050aa226f..6c1dfccbc 100644 --- a/api/_dexes/utils.ts +++ b/api/_dexes/utils.ts @@ -1,4 +1,4 @@ -import { UniversalSwapAndBridge__factory } from "../_typechain/factories/SwapAndBridge.sol"; +import { SwapAndBridge__factory } from "@across-protocol/contracts"; import { BigNumber, constants } from "ethers"; import { ENABLED_ROUTES, getProvider } from "../_utils"; @@ -58,7 +58,7 @@ export function getSwapAndBridgeAddress(dex: string, chainId: number) { export function getSwapAndBridge(dex: string, chainId: number) { const swapAndBridgeAddress = getSwapAndBridgeAddress(dex, chainId); - return UniversalSwapAndBridge__factory.connect( + return SwapAndBridge__factory.connect( swapAndBridgeAddress, getProvider(chainId) ); diff --git a/api/_spoke-pool-periphery.ts b/api/_spoke-pool-periphery.ts new file mode 100644 index 000000000..26a2e440c --- /dev/null +++ b/api/_spoke-pool-periphery.ts @@ -0,0 +1,38 @@ +import { SpokePoolV3Periphery__factory } from "./_typechain/factories/SpokePoolV3Periphery"; +import { ENABLED_ROUTES, getProvider } from "./_utils"; + +export class UnknownPeripheryForDexOnChain extends Error { + constructor(chainId: number, dex: string) { + super(`Unknown SpokePoolPeriphery for DEX ${dex} on chain ${chainId}`); + } +} + +export const spokePoolPeripheryDexes = Object.keys( + ENABLED_ROUTES.spokePoolPeripheryAddresses +); + +export function getSpokePoolPeripheryAddress(dex: string, chainId: number) { + if (!_isDexSupported(dex)) { + throw new UnknownPeripheryForDexOnChain(chainId, dex); + } + + const address = ( + ENABLED_ROUTES.spokePoolPeripheryAddresses[dex] as Record + )?.[chainId]; + if (!address) { + throw new UnknownPeripheryForDexOnChain(chainId, dex); + } + return address; +} + +export function getSpokePoolPeriphery(dex: string, chainId: number) { + const address = getSpokePoolPeripheryAddress(dex, chainId); + + return SpokePoolV3Periphery__factory.connect(address, getProvider(chainId)); +} + +function _isDexSupported( + dex: string +): dex is keyof typeof ENABLED_ROUTES.spokePoolPeripheryAddresses { + return spokePoolPeripheryDexes.includes(dex); +} diff --git a/api/_typechain/SwapAndBridge.sol/UniversalSwapAndBridge.ts b/api/_typechain/SpokePoolV3Periphery.ts similarity index 71% rename from api/_typechain/SwapAndBridge.sol/UniversalSwapAndBridge.ts rename to api/_typechain/SpokePoolV3Periphery.ts index 1612a08c3..ed66cf043 100644 --- a/api/_typechain/SwapAndBridge.sol/UniversalSwapAndBridge.ts +++ b/api/_typechain/SpokePoolV3Periphery.ts @@ -27,7 +27,7 @@ import type { OnEvent, } from "@across-protocol/contracts/dist/typechain/common"; -export declare namespace SwapAndBridgeBase { +export declare namespace SpokePoolV3Periphery { export type DepositDataStruct = { outputToken: string; outputAmount: BigNumberish; @@ -37,7 +37,7 @@ export declare namespace SwapAndBridgeBase { exclusiveRelayer: string; quoteTimestamp: BigNumberish; fillDeadline: BigNumberish; - exclusivityDeadline: BigNumberish; + exclusivityParameter: BigNumberish; message: BytesLike; }; @@ -61,19 +61,21 @@ export declare namespace SwapAndBridgeBase { exclusiveRelayer: string; quoteTimestamp: number; fillDeadline: number; - exclusivityDeadline: number; + exclusivityParameter: number; message: string; }; } -export interface UniversalSwapAndBridgeInterface extends utils.Interface { +export interface SpokePoolV3PeripheryInterface extends utils.Interface { functions: { - "EXCHANGE()": FunctionFragment; - "SPOKE_POOL()": FunctionFragment; "allowedSelectors(bytes4)": FunctionFragment; + "deposit(address,address,uint256,uint256,uint256,address,uint32,uint32,uint32,bytes)": FunctionFragment; "depositWithAuthorization(address,uint256,(address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes),uint256,uint256,bytes32,uint8,bytes32,bytes32)": FunctionFragment; "depositWithPermit(address,uint256,(address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes),uint256,uint8,bytes32,bytes32)": FunctionFragment; + "exchange()": FunctionFragment; + "initialize(address,address,address)": FunctionFragment; "multicall(bytes[])": FunctionFragment; + "spokePool()": FunctionFragment; "swapAndBridge(address,address,bytes,uint256,uint256,(address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes))": FunctionFragment; "swapAndBridgeWithAuthorization(address,address,bytes,uint256,uint256,(address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes),uint256,uint256,bytes32,uint8,bytes32,bytes32)": FunctionFragment; "swapAndBridgeWithPermit(address,address,bytes,uint256,uint256,(address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes),uint256,uint8,bytes32,bytes32)": FunctionFragment; @@ -81,32 +83,44 @@ export interface UniversalSwapAndBridgeInterface extends utils.Interface { getFunction( nameOrSignatureOrTopic: - | "EXCHANGE" - | "SPOKE_POOL" | "allowedSelectors" + | "deposit" | "depositWithAuthorization" | "depositWithPermit" + | "exchange" + | "initialize" | "multicall" + | "spokePool" | "swapAndBridge" | "swapAndBridgeWithAuthorization" | "swapAndBridgeWithPermit" ): FunctionFragment; - encodeFunctionData(functionFragment: "EXCHANGE", values?: undefined): string; - encodeFunctionData( - functionFragment: "SPOKE_POOL", - values?: undefined - ): string; encodeFunctionData( functionFragment: "allowedSelectors", values: [BytesLike] ): string; + encodeFunctionData( + functionFragment: "deposit", + values: [ + string, + string, + BigNumberish, + BigNumberish, + BigNumberish, + string, + BigNumberish, + BigNumberish, + BigNumberish, + BytesLike, + ] + ): string; encodeFunctionData( functionFragment: "depositWithAuthorization", values: [ string, BigNumberish, - SwapAndBridgeBase.DepositDataStruct, + SpokePoolV3Periphery.DepositDataStruct, BigNumberish, BigNumberish, BytesLike, @@ -120,17 +134,23 @@ export interface UniversalSwapAndBridgeInterface extends utils.Interface { values: [ string, BigNumberish, - SwapAndBridgeBase.DepositDataStruct, + SpokePoolV3Periphery.DepositDataStruct, BigNumberish, BigNumberish, BytesLike, BytesLike, ] ): string; + encodeFunctionData(functionFragment: "exchange", values?: undefined): string; + encodeFunctionData( + functionFragment: "initialize", + values: [string, string, string] + ): string; encodeFunctionData( functionFragment: "multicall", values: [BytesLike[]] ): string; + encodeFunctionData(functionFragment: "spokePool", values?: undefined): string; encodeFunctionData( functionFragment: "swapAndBridge", values: [ @@ -139,7 +159,7 @@ export interface UniversalSwapAndBridgeInterface extends utils.Interface { BytesLike, BigNumberish, BigNumberish, - SwapAndBridgeBase.DepositDataStruct, + SpokePoolV3Periphery.DepositDataStruct, ] ): string; encodeFunctionData( @@ -150,7 +170,7 @@ export interface UniversalSwapAndBridgeInterface extends utils.Interface { BytesLike, BigNumberish, BigNumberish, - SwapAndBridgeBase.DepositDataStruct, + SpokePoolV3Periphery.DepositDataStruct, BigNumberish, BigNumberish, BytesLike, @@ -167,7 +187,7 @@ export interface UniversalSwapAndBridgeInterface extends utils.Interface { BytesLike, BigNumberish, BigNumberish, - SwapAndBridgeBase.DepositDataStruct, + SpokePoolV3Periphery.DepositDataStruct, BigNumberish, BigNumberish, BytesLike, @@ -175,12 +195,11 @@ export interface UniversalSwapAndBridgeInterface extends utils.Interface { ] ): string; - decodeFunctionResult(functionFragment: "EXCHANGE", data: BytesLike): Result; - decodeFunctionResult(functionFragment: "SPOKE_POOL", data: BytesLike): Result; decodeFunctionResult( functionFragment: "allowedSelectors", data: BytesLike ): Result; + decodeFunctionResult(functionFragment: "deposit", data: BytesLike): Result; decodeFunctionResult( functionFragment: "depositWithAuthorization", data: BytesLike @@ -189,7 +208,10 @@ export interface UniversalSwapAndBridgeInterface extends utils.Interface { functionFragment: "depositWithPermit", data: BytesLike ): Result; + decodeFunctionResult(functionFragment: "exchange", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "initialize", data: BytesLike): Result; decodeFunctionResult(functionFragment: "multicall", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "spokePool", data: BytesLike): Result; decodeFunctionResult( functionFragment: "swapAndBridge", data: BytesLike @@ -227,12 +249,12 @@ export type SwapBeforeBridgeEvent = TypedEvent< export type SwapBeforeBridgeEventFilter = TypedEventFilter; -export interface UniversalSwapAndBridge extends BaseContract { +export interface SpokePoolV3Periphery extends BaseContract { connect(signerOrProvider: Signer | Provider | string): this; attach(addressOrName: string): this; deployed(): Promise; - interface: UniversalSwapAndBridgeInterface; + interface: SpokePoolV3PeripheryInterface; queryFilter( event: TypedEventFilter, @@ -254,19 +276,29 @@ export interface UniversalSwapAndBridge extends BaseContract { removeListener: OnEvent; functions: { - EXCHANGE(overrides?: CallOverrides): Promise<[string]>; - - SPOKE_POOL(overrides?: CallOverrides): Promise<[string]>; - allowedSelectors( arg0: BytesLike, overrides?: CallOverrides ): Promise<[boolean]>; + deposit( + recipient: string, + inputToken: string, + inputAmount: BigNumberish, + outputAmount: BigNumberish, + destinationChainId: BigNumberish, + exclusiveRelayer: string, + quoteTimestamp: BigNumberish, + fillDeadline: BigNumberish, + exclusivityParameter: BigNumberish, + message: BytesLike, + overrides?: PayableOverrides & { from?: string } + ): Promise; + depositWithAuthorization( acrossInputToken: string, acrossInputAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, validAfter: BigNumberish, validBefore: BigNumberish, nonce: BytesLike, @@ -279,7 +311,7 @@ export interface UniversalSwapAndBridge extends BaseContract { depositWithPermit( acrossInputToken: string, acrossInputAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, deadline: BigNumberish, v: BigNumberish, r: BytesLike, @@ -287,18 +319,29 @@ export interface UniversalSwapAndBridge extends BaseContract { overrides?: Overrides & { from?: string } ): Promise; + exchange(overrides?: CallOverrides): Promise<[string]>; + + initialize( + _spokePool: string, + _wrappedNativeToken: string, + _exchange: string, + overrides?: Overrides & { from?: string } + ): Promise; + multicall( data: BytesLike[], overrides?: Overrides & { from?: string } ): Promise; + spokePool(overrides?: CallOverrides): Promise<[string]>; + swapAndBridge( swapToken: string, acrossInputToken: string, routerCalldata: BytesLike, swapTokenAmount: BigNumberish, minExpectedInputTokenAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, overrides?: PayableOverrides & { from?: string } ): Promise; @@ -308,7 +351,7 @@ export interface UniversalSwapAndBridge extends BaseContract { routerCalldata: BytesLike, swapTokenAmount: BigNumberish, minExpectedInputTokenAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, validAfter: BigNumberish, validBefore: BigNumberish, nonce: BytesLike, @@ -324,7 +367,7 @@ export interface UniversalSwapAndBridge extends BaseContract { routerCalldata: BytesLike, swapTokenAmount: BigNumberish, minExpectedInputTokenAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, deadline: BigNumberish, v: BigNumberish, r: BytesLike, @@ -333,19 +376,29 @@ export interface UniversalSwapAndBridge extends BaseContract { ): Promise; }; - EXCHANGE(overrides?: CallOverrides): Promise; - - SPOKE_POOL(overrides?: CallOverrides): Promise; - allowedSelectors( arg0: BytesLike, overrides?: CallOverrides ): Promise; + deposit( + recipient: string, + inputToken: string, + inputAmount: BigNumberish, + outputAmount: BigNumberish, + destinationChainId: BigNumberish, + exclusiveRelayer: string, + quoteTimestamp: BigNumberish, + fillDeadline: BigNumberish, + exclusivityParameter: BigNumberish, + message: BytesLike, + overrides?: PayableOverrides & { from?: string } + ): Promise; + depositWithAuthorization( acrossInputToken: string, acrossInputAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, validAfter: BigNumberish, validBefore: BigNumberish, nonce: BytesLike, @@ -358,7 +411,7 @@ export interface UniversalSwapAndBridge extends BaseContract { depositWithPermit( acrossInputToken: string, acrossInputAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, deadline: BigNumberish, v: BigNumberish, r: BytesLike, @@ -366,18 +419,29 @@ export interface UniversalSwapAndBridge extends BaseContract { overrides?: Overrides & { from?: string } ): Promise; + exchange(overrides?: CallOverrides): Promise; + + initialize( + _spokePool: string, + _wrappedNativeToken: string, + _exchange: string, + overrides?: Overrides & { from?: string } + ): Promise; + multicall( data: BytesLike[], overrides?: Overrides & { from?: string } ): Promise; + spokePool(overrides?: CallOverrides): Promise; + swapAndBridge( swapToken: string, acrossInputToken: string, routerCalldata: BytesLike, swapTokenAmount: BigNumberish, minExpectedInputTokenAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, overrides?: PayableOverrides & { from?: string } ): Promise; @@ -387,7 +451,7 @@ export interface UniversalSwapAndBridge extends BaseContract { routerCalldata: BytesLike, swapTokenAmount: BigNumberish, minExpectedInputTokenAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, validAfter: BigNumberish, validBefore: BigNumberish, nonce: BytesLike, @@ -403,7 +467,7 @@ export interface UniversalSwapAndBridge extends BaseContract { routerCalldata: BytesLike, swapTokenAmount: BigNumberish, minExpectedInputTokenAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, deadline: BigNumberish, v: BigNumberish, r: BytesLike, @@ -412,19 +476,29 @@ export interface UniversalSwapAndBridge extends BaseContract { ): Promise; callStatic: { - EXCHANGE(overrides?: CallOverrides): Promise; - - SPOKE_POOL(overrides?: CallOverrides): Promise; - allowedSelectors( arg0: BytesLike, overrides?: CallOverrides ): Promise; + deposit( + recipient: string, + inputToken: string, + inputAmount: BigNumberish, + outputAmount: BigNumberish, + destinationChainId: BigNumberish, + exclusiveRelayer: string, + quoteTimestamp: BigNumberish, + fillDeadline: BigNumberish, + exclusivityParameter: BigNumberish, + message: BytesLike, + overrides?: CallOverrides + ): Promise; + depositWithAuthorization( acrossInputToken: string, acrossInputAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, validAfter: BigNumberish, validBefore: BigNumberish, nonce: BytesLike, @@ -437,7 +511,7 @@ export interface UniversalSwapAndBridge extends BaseContract { depositWithPermit( acrossInputToken: string, acrossInputAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, deadline: BigNumberish, v: BigNumberish, r: BytesLike, @@ -445,15 +519,26 @@ export interface UniversalSwapAndBridge extends BaseContract { overrides?: CallOverrides ): Promise; + exchange(overrides?: CallOverrides): Promise; + + initialize( + _spokePool: string, + _wrappedNativeToken: string, + _exchange: string, + overrides?: CallOverrides + ): Promise; + multicall(data: BytesLike[], overrides?: CallOverrides): Promise; + spokePool(overrides?: CallOverrides): Promise; + swapAndBridge( swapToken: string, acrossInputToken: string, routerCalldata: BytesLike, swapTokenAmount: BigNumberish, minExpectedInputTokenAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, overrides?: CallOverrides ): Promise; @@ -463,7 +548,7 @@ export interface UniversalSwapAndBridge extends BaseContract { routerCalldata: BytesLike, swapTokenAmount: BigNumberish, minExpectedInputTokenAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, validAfter: BigNumberish, validBefore: BigNumberish, nonce: BytesLike, @@ -479,7 +564,7 @@ export interface UniversalSwapAndBridge extends BaseContract { routerCalldata: BytesLike, swapTokenAmount: BigNumberish, minExpectedInputTokenAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, deadline: BigNumberish, v: BigNumberish, r: BytesLike, @@ -510,19 +595,29 @@ export interface UniversalSwapAndBridge extends BaseContract { }; estimateGas: { - EXCHANGE(overrides?: CallOverrides): Promise; - - SPOKE_POOL(overrides?: CallOverrides): Promise; - allowedSelectors( arg0: BytesLike, overrides?: CallOverrides ): Promise; + deposit( + recipient: string, + inputToken: string, + inputAmount: BigNumberish, + outputAmount: BigNumberish, + destinationChainId: BigNumberish, + exclusiveRelayer: string, + quoteTimestamp: BigNumberish, + fillDeadline: BigNumberish, + exclusivityParameter: BigNumberish, + message: BytesLike, + overrides?: PayableOverrides & { from?: string } + ): Promise; + depositWithAuthorization( acrossInputToken: string, acrossInputAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, validAfter: BigNumberish, validBefore: BigNumberish, nonce: BytesLike, @@ -535,7 +630,7 @@ export interface UniversalSwapAndBridge extends BaseContract { depositWithPermit( acrossInputToken: string, acrossInputAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, deadline: BigNumberish, v: BigNumberish, r: BytesLike, @@ -543,18 +638,29 @@ export interface UniversalSwapAndBridge extends BaseContract { overrides?: Overrides & { from?: string } ): Promise; + exchange(overrides?: CallOverrides): Promise; + + initialize( + _spokePool: string, + _wrappedNativeToken: string, + _exchange: string, + overrides?: Overrides & { from?: string } + ): Promise; + multicall( data: BytesLike[], overrides?: Overrides & { from?: string } ): Promise; + spokePool(overrides?: CallOverrides): Promise; + swapAndBridge( swapToken: string, acrossInputToken: string, routerCalldata: BytesLike, swapTokenAmount: BigNumberish, minExpectedInputTokenAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, overrides?: PayableOverrides & { from?: string } ): Promise; @@ -564,7 +670,7 @@ export interface UniversalSwapAndBridge extends BaseContract { routerCalldata: BytesLike, swapTokenAmount: BigNumberish, minExpectedInputTokenAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, validAfter: BigNumberish, validBefore: BigNumberish, nonce: BytesLike, @@ -580,7 +686,7 @@ export interface UniversalSwapAndBridge extends BaseContract { routerCalldata: BytesLike, swapTokenAmount: BigNumberish, minExpectedInputTokenAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, deadline: BigNumberish, v: BigNumberish, r: BytesLike, @@ -590,19 +696,29 @@ export interface UniversalSwapAndBridge extends BaseContract { }; populateTransaction: { - EXCHANGE(overrides?: CallOverrides): Promise; - - SPOKE_POOL(overrides?: CallOverrides): Promise; - allowedSelectors( arg0: BytesLike, overrides?: CallOverrides ): Promise; + deposit( + recipient: string, + inputToken: string, + inputAmount: BigNumberish, + outputAmount: BigNumberish, + destinationChainId: BigNumberish, + exclusiveRelayer: string, + quoteTimestamp: BigNumberish, + fillDeadline: BigNumberish, + exclusivityParameter: BigNumberish, + message: BytesLike, + overrides?: PayableOverrides & { from?: string } + ): Promise; + depositWithAuthorization( acrossInputToken: string, acrossInputAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, validAfter: BigNumberish, validBefore: BigNumberish, nonce: BytesLike, @@ -615,7 +731,7 @@ export interface UniversalSwapAndBridge extends BaseContract { depositWithPermit( acrossInputToken: string, acrossInputAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, deadline: BigNumberish, v: BigNumberish, r: BytesLike, @@ -623,18 +739,29 @@ export interface UniversalSwapAndBridge extends BaseContract { overrides?: Overrides & { from?: string } ): Promise; + exchange(overrides?: CallOverrides): Promise; + + initialize( + _spokePool: string, + _wrappedNativeToken: string, + _exchange: string, + overrides?: Overrides & { from?: string } + ): Promise; + multicall( data: BytesLike[], overrides?: Overrides & { from?: string } ): Promise; + spokePool(overrides?: CallOverrides): Promise; + swapAndBridge( swapToken: string, acrossInputToken: string, routerCalldata: BytesLike, swapTokenAmount: BigNumberish, minExpectedInputTokenAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, overrides?: PayableOverrides & { from?: string } ): Promise; @@ -644,7 +771,7 @@ export interface UniversalSwapAndBridge extends BaseContract { routerCalldata: BytesLike, swapTokenAmount: BigNumberish, minExpectedInputTokenAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, validAfter: BigNumberish, validBefore: BigNumberish, nonce: BytesLike, @@ -660,7 +787,7 @@ export interface UniversalSwapAndBridge extends BaseContract { routerCalldata: BytesLike, swapTokenAmount: BigNumberish, minExpectedInputTokenAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, + depositData: SpokePoolV3Periphery.DepositDataStruct, deadline: BigNumberish, v: BigNumberish, r: BytesLike, diff --git a/api/_typechain/SwapAndBridge.sol/SwapAndBridge.ts b/api/_typechain/SwapAndBridge.sol/SwapAndBridge.ts deleted file mode 100644 index 8909d98ba..000000000 --- a/api/_typechain/SwapAndBridge.sol/SwapAndBridge.ts +++ /dev/null @@ -1,345 +0,0 @@ -/* Autogenerated file. Do not edit manually. */ -/* tslint:disable */ -/* eslint-disable */ -import type { - BaseContract, - BigNumber, - BigNumberish, - BytesLike, - CallOverrides, - ContractTransaction, - Overrides, - PopulatedTransaction, - Signer, - utils, -} from "ethers"; -import type { - FunctionFragment, - Result, - EventFragment, -} from "@ethersproject/abi"; -import type { Listener, Provider } from "@ethersproject/providers"; -import type { - TypedEventFilter, - TypedEvent, - TypedListener, - OnEvent, -} from "@across-protocol/contracts/dist/typechain/common"; - -export declare namespace SwapAndBridgeBase { - export type DepositDataStruct = { - outputToken: string; - outputAmount: BigNumberish; - depositor: string; - recipient: string; - destinationChainid: BigNumberish; - exclusiveRelayer: string; - quoteTimestamp: BigNumberish; - fillDeadline: BigNumberish; - exclusivityDeadline: BigNumberish; - message: BytesLike; - }; - - export type DepositDataStructOutput = [ - string, - BigNumber, - string, - string, - BigNumber, - string, - number, - number, - number, - string, - ] & { - outputToken: string; - outputAmount: BigNumber; - depositor: string; - recipient: string; - destinationChainid: BigNumber; - exclusiveRelayer: string; - quoteTimestamp: number; - fillDeadline: number; - exclusivityDeadline: number; - message: string; - }; -} - -export interface SwapAndBridgeInterface extends utils.Interface { - functions: { - "ACROSS_INPUT_TOKEN()": FunctionFragment; - "EXCHANGE()": FunctionFragment; - "SPOKE_POOL()": FunctionFragment; - "SWAP_TOKEN()": FunctionFragment; - "allowedSelectors(bytes4)": FunctionFragment; - "multicall(bytes[])": FunctionFragment; - "swapAndBridge(bytes,uint256,uint256,(address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes))": FunctionFragment; - }; - - getFunction( - nameOrSignatureOrTopic: - | "ACROSS_INPUT_TOKEN" - | "EXCHANGE" - | "SPOKE_POOL" - | "SWAP_TOKEN" - | "allowedSelectors" - | "multicall" - | "swapAndBridge" - ): FunctionFragment; - - encodeFunctionData( - functionFragment: "ACROSS_INPUT_TOKEN", - values?: undefined - ): string; - encodeFunctionData(functionFragment: "EXCHANGE", values?: undefined): string; - encodeFunctionData( - functionFragment: "SPOKE_POOL", - values?: undefined - ): string; - encodeFunctionData( - functionFragment: "SWAP_TOKEN", - values?: undefined - ): string; - encodeFunctionData( - functionFragment: "allowedSelectors", - values: [BytesLike] - ): string; - encodeFunctionData( - functionFragment: "multicall", - values: [BytesLike[]] - ): string; - encodeFunctionData( - functionFragment: "swapAndBridge", - values: [ - BytesLike, - BigNumberish, - BigNumberish, - SwapAndBridgeBase.DepositDataStruct, - ] - ): string; - - decodeFunctionResult( - functionFragment: "ACROSS_INPUT_TOKEN", - data: BytesLike - ): Result; - decodeFunctionResult(functionFragment: "EXCHANGE", data: BytesLike): Result; - decodeFunctionResult(functionFragment: "SPOKE_POOL", data: BytesLike): Result; - decodeFunctionResult(functionFragment: "SWAP_TOKEN", data: BytesLike): Result; - decodeFunctionResult( - functionFragment: "allowedSelectors", - data: BytesLike - ): Result; - decodeFunctionResult(functionFragment: "multicall", data: BytesLike): Result; - decodeFunctionResult( - functionFragment: "swapAndBridge", - data: BytesLike - ): Result; - - events: { - "SwapBeforeBridge(address,address,address,uint256,uint256,address,uint256)": EventFragment; - }; - - getEvent(nameOrSignatureOrTopic: "SwapBeforeBridge"): EventFragment; -} - -export interface SwapBeforeBridgeEventObject { - exchange: string; - swapToken: string; - acrossInputToken: string; - swapTokenAmount: BigNumber; - acrossInputAmount: BigNumber; - acrossOutputToken: string; - acrossOutputAmount: BigNumber; -} -export type SwapBeforeBridgeEvent = TypedEvent< - [string, string, string, BigNumber, BigNumber, string, BigNumber], - SwapBeforeBridgeEventObject ->; - -export type SwapBeforeBridgeEventFilter = - TypedEventFilter; - -export interface SwapAndBridge extends BaseContract { - connect(signerOrProvider: Signer | Provider | string): this; - attach(addressOrName: string): this; - deployed(): Promise; - - interface: SwapAndBridgeInterface; - - queryFilter( - event: TypedEventFilter, - fromBlockOrBlockhash?: string | number | undefined, - toBlock?: string | number | undefined - ): Promise>; - - listeners( - eventFilter?: TypedEventFilter - ): Array>; - listeners(eventName?: string): Array; - removeAllListeners( - eventFilter: TypedEventFilter - ): this; - removeAllListeners(eventName?: string): this; - off: OnEvent; - on: OnEvent; - once: OnEvent; - removeListener: OnEvent; - - functions: { - ACROSS_INPUT_TOKEN(overrides?: CallOverrides): Promise<[string]>; - - EXCHANGE(overrides?: CallOverrides): Promise<[string]>; - - SPOKE_POOL(overrides?: CallOverrides): Promise<[string]>; - - SWAP_TOKEN(overrides?: CallOverrides): Promise<[string]>; - - allowedSelectors( - arg0: BytesLike, - overrides?: CallOverrides - ): Promise<[boolean]>; - - multicall( - data: BytesLike[], - overrides?: Overrides & { from?: string } - ): Promise; - - swapAndBridge( - routerCalldata: BytesLike, - swapTokenAmount: BigNumberish, - minExpectedInputTokenAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, - overrides?: Overrides & { from?: string } - ): Promise; - }; - - ACROSS_INPUT_TOKEN(overrides?: CallOverrides): Promise; - - EXCHANGE(overrides?: CallOverrides): Promise; - - SPOKE_POOL(overrides?: CallOverrides): Promise; - - SWAP_TOKEN(overrides?: CallOverrides): Promise; - - allowedSelectors( - arg0: BytesLike, - overrides?: CallOverrides - ): Promise; - - multicall( - data: BytesLike[], - overrides?: Overrides & { from?: string } - ): Promise; - - swapAndBridge( - routerCalldata: BytesLike, - swapTokenAmount: BigNumberish, - minExpectedInputTokenAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, - overrides?: Overrides & { from?: string } - ): Promise; - - callStatic: { - ACROSS_INPUT_TOKEN(overrides?: CallOverrides): Promise; - - EXCHANGE(overrides?: CallOverrides): Promise; - - SPOKE_POOL(overrides?: CallOverrides): Promise; - - SWAP_TOKEN(overrides?: CallOverrides): Promise; - - allowedSelectors( - arg0: BytesLike, - overrides?: CallOverrides - ): Promise; - - multicall(data: BytesLike[], overrides?: CallOverrides): Promise; - - swapAndBridge( - routerCalldata: BytesLike, - swapTokenAmount: BigNumberish, - minExpectedInputTokenAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, - overrides?: CallOverrides - ): Promise; - }; - - filters: { - "SwapBeforeBridge(address,address,address,uint256,uint256,address,uint256)"( - exchange?: null, - swapToken?: string | null, - acrossInputToken?: string | null, - swapTokenAmount?: null, - acrossInputAmount?: null, - acrossOutputToken?: string | null, - acrossOutputAmount?: null - ): SwapBeforeBridgeEventFilter; - SwapBeforeBridge( - exchange?: null, - swapToken?: string | null, - acrossInputToken?: string | null, - swapTokenAmount?: null, - acrossInputAmount?: null, - acrossOutputToken?: string | null, - acrossOutputAmount?: null - ): SwapBeforeBridgeEventFilter; - }; - - estimateGas: { - ACROSS_INPUT_TOKEN(overrides?: CallOverrides): Promise; - - EXCHANGE(overrides?: CallOverrides): Promise; - - SPOKE_POOL(overrides?: CallOverrides): Promise; - - SWAP_TOKEN(overrides?: CallOverrides): Promise; - - allowedSelectors( - arg0: BytesLike, - overrides?: CallOverrides - ): Promise; - - multicall( - data: BytesLike[], - overrides?: Overrides & { from?: string } - ): Promise; - - swapAndBridge( - routerCalldata: BytesLike, - swapTokenAmount: BigNumberish, - minExpectedInputTokenAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, - overrides?: Overrides & { from?: string } - ): Promise; - }; - - populateTransaction: { - ACROSS_INPUT_TOKEN( - overrides?: CallOverrides - ): Promise; - - EXCHANGE(overrides?: CallOverrides): Promise; - - SPOKE_POOL(overrides?: CallOverrides): Promise; - - SWAP_TOKEN(overrides?: CallOverrides): Promise; - - allowedSelectors( - arg0: BytesLike, - overrides?: CallOverrides - ): Promise; - - multicall( - data: BytesLike[], - overrides?: Overrides & { from?: string } - ): Promise; - - swapAndBridge( - routerCalldata: BytesLike, - swapTokenAmount: BigNumberish, - minExpectedInputTokenAmount: BigNumberish, - depositData: SwapAndBridgeBase.DepositDataStruct, - overrides?: Overrides & { from?: string } - ): Promise; - }; -} diff --git a/api/_typechain/SwapAndBridge.sol/SwapAndBridgeBase.ts b/api/_typechain/SwapAndBridge.sol/SwapAndBridgeBase.ts deleted file mode 100644 index bbbdf9bdb..000000000 --- a/api/_typechain/SwapAndBridge.sol/SwapAndBridgeBase.ts +++ /dev/null @@ -1,211 +0,0 @@ -/* Autogenerated file. Do not edit manually. */ -/* tslint:disable */ - -import type { - BaseContract, - BigNumber, - BytesLike, - CallOverrides, - ContractTransaction, - Overrides, - PopulatedTransaction, - Signer, - utils, -} from "ethers"; -import type { - FunctionFragment, - Result, - EventFragment, -} from "@ethersproject/abi"; -import type { Listener, Provider } from "@ethersproject/providers"; -import type { - TypedEventFilter, - TypedEvent, - TypedListener, - OnEvent, -} from "@across-protocol/contracts/dist/typechain/common"; - -export interface SwapAndBridgeBaseInterface extends utils.Interface { - functions: { - "EXCHANGE()": FunctionFragment; - "SPOKE_POOL()": FunctionFragment; - "allowedSelectors(bytes4)": FunctionFragment; - "multicall(bytes[])": FunctionFragment; - }; - - getFunction( - nameOrSignatureOrTopic: - | "EXCHANGE" - | "SPOKE_POOL" - | "allowedSelectors" - | "multicall" - ): FunctionFragment; - - encodeFunctionData(functionFragment: "EXCHANGE", values?: undefined): string; - encodeFunctionData( - functionFragment: "SPOKE_POOL", - values?: undefined - ): string; - encodeFunctionData( - functionFragment: "allowedSelectors", - values: [BytesLike] - ): string; - encodeFunctionData( - functionFragment: "multicall", - values: [BytesLike[]] - ): string; - - decodeFunctionResult(functionFragment: "EXCHANGE", data: BytesLike): Result; - decodeFunctionResult(functionFragment: "SPOKE_POOL", data: BytesLike): Result; - decodeFunctionResult( - functionFragment: "allowedSelectors", - data: BytesLike - ): Result; - decodeFunctionResult(functionFragment: "multicall", data: BytesLike): Result; - - events: { - "SwapBeforeBridge(address,address,address,uint256,uint256,address,uint256)": EventFragment; - }; - - getEvent(nameOrSignatureOrTopic: "SwapBeforeBridge"): EventFragment; -} - -export interface SwapBeforeBridgeEventObject { - exchange: string; - swapToken: string; - acrossInputToken: string; - swapTokenAmount: BigNumber; - acrossInputAmount: BigNumber; - acrossOutputToken: string; - acrossOutputAmount: BigNumber; -} -export type SwapBeforeBridgeEvent = TypedEvent< - [string, string, string, BigNumber, BigNumber, string, BigNumber], - SwapBeforeBridgeEventObject ->; - -export type SwapBeforeBridgeEventFilter = - TypedEventFilter; - -export interface SwapAndBridgeBase extends BaseContract { - connect(signerOrProvider: Signer | Provider | string): this; - attach(addressOrName: string): this; - deployed(): Promise; - - interface: SwapAndBridgeBaseInterface; - - queryFilter( - event: TypedEventFilter, - fromBlockOrBlockhash?: string | number | undefined, - toBlock?: string | number | undefined - ): Promise>; - - listeners( - eventFilter?: TypedEventFilter - ): Array>; - listeners(eventName?: string): Array; - removeAllListeners( - eventFilter: TypedEventFilter - ): this; - removeAllListeners(eventName?: string): this; - off: OnEvent; - on: OnEvent; - once: OnEvent; - removeListener: OnEvent; - - functions: { - EXCHANGE(overrides?: CallOverrides): Promise<[string]>; - - SPOKE_POOL(overrides?: CallOverrides): Promise<[string]>; - - allowedSelectors( - arg0: BytesLike, - overrides?: CallOverrides - ): Promise<[boolean]>; - - multicall( - data: BytesLike[], - overrides?: Overrides & { from?: string } - ): Promise; - }; - - EXCHANGE(overrides?: CallOverrides): Promise; - - SPOKE_POOL(overrides?: CallOverrides): Promise; - - allowedSelectors( - arg0: BytesLike, - overrides?: CallOverrides - ): Promise; - - multicall( - data: BytesLike[], - overrides?: Overrides & { from?: string } - ): Promise; - - callStatic: { - EXCHANGE(overrides?: CallOverrides): Promise; - - SPOKE_POOL(overrides?: CallOverrides): Promise; - - allowedSelectors( - arg0: BytesLike, - overrides?: CallOverrides - ): Promise; - - multicall(data: BytesLike[], overrides?: CallOverrides): Promise; - }; - - filters: { - "SwapBeforeBridge(address,address,address,uint256,uint256,address,uint256)"( - exchange?: null, - swapToken?: string | null, - acrossInputToken?: string | null, - swapTokenAmount?: null, - acrossInputAmount?: null, - acrossOutputToken?: string | null, - acrossOutputAmount?: null - ): SwapBeforeBridgeEventFilter; - SwapBeforeBridge( - exchange?: null, - swapToken?: string | null, - acrossInputToken?: string | null, - swapTokenAmount?: null, - acrossInputAmount?: null, - acrossOutputToken?: string | null, - acrossOutputAmount?: null - ): SwapBeforeBridgeEventFilter; - }; - - estimateGas: { - EXCHANGE(overrides?: CallOverrides): Promise; - - SPOKE_POOL(overrides?: CallOverrides): Promise; - - allowedSelectors( - arg0: BytesLike, - overrides?: CallOverrides - ): Promise; - - multicall( - data: BytesLike[], - overrides?: Overrides & { from?: string } - ): Promise; - }; - - populateTransaction: { - EXCHANGE(overrides?: CallOverrides): Promise; - - SPOKE_POOL(overrides?: CallOverrides): Promise; - - allowedSelectors( - arg0: BytesLike, - overrides?: CallOverrides - ): Promise; - - multicall( - data: BytesLike[], - overrides?: Overrides & { from?: string } - ): Promise; - }; -} diff --git a/api/_typechain/SwapAndBridge.sol/index.ts b/api/_typechain/SwapAndBridge.sol/index.ts deleted file mode 100644 index c2a19dda2..000000000 --- a/api/_typechain/SwapAndBridge.sol/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* Autogenerated file. Do not edit manually. */ -/* tslint:disable */ - -export type { SwapAndBridge } from "./SwapAndBridge"; -export type { SwapAndBridgeBase } from "./SwapAndBridgeBase"; -export type { UniversalSwapAndBridge } from "./UniversalSwapAndBridge"; diff --git a/api/_typechain/factories/SpokePoolV3Periphery.ts b/api/_typechain/factories/SpokePoolV3Periphery.ts new file mode 100644 index 000000000..4b85fa045 --- /dev/null +++ b/api/_typechain/factories/SpokePoolV3Periphery.ts @@ -0,0 +1,838 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ + +import { + Signer, + utils, + Contract, + ContractFactory, + BytesLike, + Overrides, +} from "ethers"; +import type { Provider, TransactionRequest } from "@ethersproject/providers"; +import type { + SpokePoolV3Periphery, + SpokePoolV3PeripheryInterface, +} from "../SpokePoolV3Periphery"; + +const _abi = [ + { + inputs: [ + { + internalType: "bytes4[]", + name: "_allowedSelectors", + type: "bytes4[]", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "ContractInitialized", + type: "error", + }, + { + inputs: [], + name: "InvalidFunctionSelector", + type: "error", + }, + { + inputs: [], + name: "InvalidMsgValue", + type: "error", + }, + { + inputs: [], + name: "InvalidSpokePool", + type: "error", + }, + { + inputs: [], + name: "InvalidSwapToken", + type: "error", + }, + { + inputs: [], + name: "LeftoverSrcTokens", + type: "error", + }, + { + inputs: [], + name: "MinimumExpectedInputAmount", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "exchange", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "swapToken", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "acrossInputToken", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "swapTokenAmount", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "acrossInputAmount", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "acrossOutputToken", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "acrossOutputAmount", + type: "uint256", + }, + ], + name: "SwapBeforeBridge", + type: "event", + }, + { + inputs: [ + { + internalType: "bytes4", + name: "", + type: "bytes4", + }, + ], + name: "allowedSelectors", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "address", + name: "inputToken", + type: "address", + }, + { + internalType: "uint256", + name: "inputAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "destinationChainId", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityParameter", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + name: "deposit", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "contract IERC20Auth", + name: "acrossInputToken", + type: "address", + }, + { + internalType: "uint256", + name: "acrossInputAmount", + type: "uint256", + }, + { + components: [ + { + internalType: "address", + name: "outputToken", + type: "address", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "address", + name: "depositor", + type: "address", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "destinationChainid", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityParameter", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + internalType: "struct SpokePoolV3Periphery.DepositData", + name: "depositData", + type: "tuple", + }, + { + internalType: "uint256", + name: "validAfter", + type: "uint256", + }, + { + internalType: "uint256", + name: "validBefore", + type: "uint256", + }, + { + internalType: "bytes32", + name: "nonce", + type: "bytes32", + }, + { + internalType: "uint8", + name: "v", + type: "uint8", + }, + { + internalType: "bytes32", + name: "r", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "s", + type: "bytes32", + }, + ], + name: "depositWithAuthorization", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "contract IERC20Permit", + name: "acrossInputToken", + type: "address", + }, + { + internalType: "uint256", + name: "acrossInputAmount", + type: "uint256", + }, + { + components: [ + { + internalType: "address", + name: "outputToken", + type: "address", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "address", + name: "depositor", + type: "address", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "destinationChainid", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityParameter", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + internalType: "struct SpokePoolV3Periphery.DepositData", + name: "depositData", + type: "tuple", + }, + { + internalType: "uint256", + name: "deadline", + type: "uint256", + }, + { + internalType: "uint8", + name: "v", + type: "uint8", + }, + { + internalType: "bytes32", + name: "r", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "s", + type: "bytes32", + }, + ], + name: "depositWithPermit", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "exchange", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "contract V3SpokePoolInterface", + name: "_spokePool", + type: "address", + }, + { + internalType: "contract WETH9Interface", + name: "_wrappedNativeToken", + type: "address", + }, + { + internalType: "address", + name: "_exchange", + type: "address", + }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes[]", + name: "data", + type: "bytes[]", + }, + ], + name: "multicall", + outputs: [ + { + internalType: "bytes[]", + name: "results", + type: "bytes[]", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "spokePool", + outputs: [ + { + internalType: "contract V3SpokePoolInterface", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "contract IERC20", + name: "swapToken", + type: "address", + }, + { + internalType: "contract IERC20", + name: "acrossInputToken", + type: "address", + }, + { + internalType: "bytes", + name: "routerCalldata", + type: "bytes", + }, + { + internalType: "uint256", + name: "swapTokenAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "minExpectedInputTokenAmount", + type: "uint256", + }, + { + components: [ + { + internalType: "address", + name: "outputToken", + type: "address", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "address", + name: "depositor", + type: "address", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "destinationChainid", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityParameter", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + internalType: "struct SpokePoolV3Periphery.DepositData", + name: "depositData", + type: "tuple", + }, + ], + name: "swapAndBridge", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "contract IERC20Auth", + name: "swapToken", + type: "address", + }, + { + internalType: "contract IERC20", + name: "acrossInputToken", + type: "address", + }, + { + internalType: "bytes", + name: "routerCalldata", + type: "bytes", + }, + { + internalType: "uint256", + name: "swapTokenAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "minExpectedInputTokenAmount", + type: "uint256", + }, + { + components: [ + { + internalType: "address", + name: "outputToken", + type: "address", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "address", + name: "depositor", + type: "address", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "destinationChainid", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityParameter", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + internalType: "struct SpokePoolV3Periphery.DepositData", + name: "depositData", + type: "tuple", + }, + { + internalType: "uint256", + name: "validAfter", + type: "uint256", + }, + { + internalType: "uint256", + name: "validBefore", + type: "uint256", + }, + { + internalType: "bytes32", + name: "nonce", + type: "bytes32", + }, + { + internalType: "uint8", + name: "v", + type: "uint8", + }, + { + internalType: "bytes32", + name: "r", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "s", + type: "bytes32", + }, + ], + name: "swapAndBridgeWithAuthorization", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "contract IERC20Permit", + name: "swapToken", + type: "address", + }, + { + internalType: "contract IERC20", + name: "acrossInputToken", + type: "address", + }, + { + internalType: "bytes", + name: "routerCalldata", + type: "bytes", + }, + { + internalType: "uint256", + name: "swapTokenAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "minExpectedInputTokenAmount", + type: "uint256", + }, + { + components: [ + { + internalType: "address", + name: "outputToken", + type: "address", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "address", + name: "depositor", + type: "address", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "destinationChainid", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityParameter", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + internalType: "struct SpokePoolV3Periphery.DepositData", + name: "depositData", + type: "tuple", + }, + { + internalType: "uint256", + name: "deadline", + type: "uint256", + }, + { + internalType: "uint8", + name: "v", + type: "uint8", + }, + { + internalType: "bytes32", + name: "r", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "s", + type: "bytes32", + }, + ], + name: "swapAndBridgeWithPermit", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; + +const _bytecode = + "0x60406080604052346200011c5762001e1e803803806200001f8162000134565b928339810160209081838203126200011c5782516001600160401b03938482116200011c57019080601f830112156200011c57815193841162000120576005918460051b9084806200007381850162000134565b8098815201928201019283116200011c578401905b828210620000fa57505050600193849360ff19936001855f5416175f555f955b620000be575b604051611cc390816200015b8239f35b8151861015620000f45786809663ffffffff60e01b8382881b86010151165f52818352845f2082888254161790550195620000a8565b620000ae565b81516001600160e01b0319811681036200011c57815290840190840162000088565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b6040519190601f01601f191682016001600160401b03811183821017620001205760405256fe60806040526004361015610011575f80fd5b5f3560e01c8063038f12ea146100c4578063277deffe146100bf57806327e98fbd146100ba57806385f168eb146100b5578063ac9650d8146100b0578063afdac3d6146100ab578063bdf52ad3146100a6578063c0c53b8b146100a1578063c51e5eb91461009c578063d2f7265a146100975763fdf152d314610092575f80fd5b610b13565b610ac2565b6108a0565b61074d565b6106c0565b61066f565b6105dd565b610481565b61039b565b610224565b610175565b73ffffffffffffffffffffffffffffffffffffffff8116036100e757565b5f80fd5b600435906100f8826100c9565b565b602435906100f8826100c9565b9181601f840112156100e75782359167ffffffffffffffff83116100e757602083818601950101116100e757565b90816101409103126100e75790565b610124359060ff821682036100e757565b60e4359060ff821682036100e757565b6084359060ff821682036100e757565b346100e7576101807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e7576101ad6100eb565b6101b56100fa565b67ffffffffffffffff91906044358381116100e7576101d8903690600401610107565b9260a4359485116100e7576101f4610222953690600401610135565b936101fd610144565b9261016435956101443595610104359460e4359460c435946084359360643593610c95565b005b346100e7576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e757600435610260816100c9565b6102686100fa565b67ffffffffffffffff91906044358381116100e75761028b903690600401610107565b919060a4359485116100e7576102a8610222953690600401610135565b6102b0610155565b926101243595610104359560c435946084359360643593610dc0565b63ffffffff8116036100e757565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b67ffffffffffffffff811161031b57604052565b6102da565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761031b57604052565b67ffffffffffffffff811161031b57601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b6101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e7576004356103d2816100c9565b6024356103de816100c9565b60a4356103ea816100c9565b60c4356103f6816102cc565b60e43590610403826102cc565b6101043592610411846102cc565b610124359567ffffffffffffffff87116100e757366023880112156100e75786600401359561043f87610361565b9661044d6040519889610320565b808852366024828b0101116100e7576020815f9260246102229c01838c013789010152608435916064359160443591610ea0565b346100e75760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e7576004357fffffffff0000000000000000000000000000000000000000000000000000000081168091036100e7575f526001602052602060ff60405f2054166040519015158152f35b5f5b8381106105095750505f910152565b81810151838201526020016104fa565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f602093610555815180928187528780880191016104f8565b0116010190565b6020808201906020835283518092526040830192602060408460051b8301019501935f915b8483106105915750505050505090565b90919293949584806105cd837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc086600196030187528a51610519565b9801930193019194939290610581565b346100e75760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e75767ffffffffffffffff6004358181116100e757366023820112156100e75780600401359182116100e7573660248360051b830101116100e75761066191602461065592016111e4565b6040519182918261055c565b0390f35b5f9103126100e757565b346100e7575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e757602073ffffffffffffffffffffffffffffffffffffffff60025416604051908152f35b346100e7576101207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e7576004356106fc816100c9565b60443567ffffffffffffffff81116100e75761071c903690600401610135565b9060c4359160ff831683036100e75761022292610104359260e4359260a435916084359160643591602435906112cb565b346100e75760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e757600435610788816100c9565b60243590610795826100c9565b604435906107a2826100c9565b60045460ff8160a01c16610876577fffffffffffffffffffffff00000000000000000000000000000000000000000074010000000000000000000000000000000000000000926102229573ffffffffffffffffffffffffffffffffffffffff8092167fffffffffffffffffffffffff00000000000000000000000000000000000000006002541617600255169116171760045573ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000006003541617600355565b60046040517f9f4eefba000000000000000000000000000000000000000000000000000000008152fd5b60c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e757600480356108d7816100c9565b602435906108e4826100c9565b67ffffffffffffffff6044358181116100e7576109049036908601610107565b916064359060a4359081116100e7576109209036908801610135565b926109296113c2565b6109547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b3415610aac57813403610a835773ffffffffffffffffffffffffffffffffffffffff6109b0610997895473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff1690565b9080821690871603610a5a57803b156100e7575f90604051988980927fd0e30db000000000000000000000000000000000000000000000000000000000825234905af1968715610a5557610a0e97610a3c575b505b60843592611421565b61022260017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f5416175f55565b80610a49610a4f92610307565b80610665565b5f610a03565b610db5565b876040517f3539a701000000000000000000000000000000000000000000000000000000008152fd5b866040517f1841b4e1000000000000000000000000000000000000000000000000000000008152fd5b610a0e9650610abd82303388611606565b610a05565b346100e7575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e757602073ffffffffffffffffffffffffffffffffffffffff60035416604051908152f35b346100e7575f60e07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e75760043590610b50826100c9565b60243560443567ffffffffffffffff81116100e757610b73903690600401610135565b9073ffffffffffffffffffffffffffffffffffffffff610b91610165565b94610b9a6113c2565b610bc57fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b1693843b156100e7576040517fd505accf00000000000000000000000000000000000000000000000000000000815233600482015230602482015260448101839052606480359082015260ff91909116608482015260a480359082015260c48035908201525f8160e48183895af1610c80575b50610c4f9293610c4a82303384611606565b611765565b610c7d60017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f5416175f55565b80f35b610c4f9350610c8e90610307565b5f92610c38565b73ffffffffffffffffffffffffffffffffffffffff909c9a919b94979295989396999c610cc06113c2565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f5516998a3b156100e7576040517fef55bec6000000000000000000000000000000000000000000000000000000008152336004820152306024820152604481018990526064810193909352608483019390935260a482019b909b5260ff909a1660c48b015260e48a01919091526101048901525f8861012481838a5af1978815610a5557610d7898610da6575b50611421565b6100f860017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f5416175f55565b610daf90610307565b5f610d72565b6040513d5f823e3d90fd5b73ffffffffffffffffffffffffffffffffffffffff909a91999293949596979a610de86113c2565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f551697883b156100e7576040517fd505accf00000000000000000000000000000000000000000000000000000000815233600482015230602482015260448101879052606481019b909b5260ff1660848b015260a48a019190915260c4890152610d78975f8160e481838b5af1610e91575b50610e8c83303389611606565b611421565b610e9a90610307565b5f610e7f565b939298919697909497610eb16113c2565b610edc7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b873403610fce57610f0561099760025473ffffffffffffffffffffffffffffffffffffffff1690565b96873b15610fa457873b156100e7575f99610f51956040519c8d9b8c9a8b9a7f7b939232000000000000000000000000000000000000000000000000000000008c523360048d01610ff8565b039134905af18015610a5557610f91575b506100f860017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f5416175f55565b80610a49610f9e92610307565b5f610f62565b60046040517fb474246c000000000000000000000000000000000000000000000000000000008152fd5b60046040517f1841b4e1000000000000000000000000000000000000000000000000000000008152fd5b9794909361107c9b9a969294999793996101809a73ffffffffffffffffffffffffffffffffffffffff8097818094168d521660208c01521660408a01525f60608a0152608089015260a088015260c08701521660e085015263ffffffff92838092166101008601521661012084015216610140820152816101608201520190610519565b90565b67ffffffffffffffff811161031b5760051b60200190565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1813603018212156100e7570180359067ffffffffffffffff82116100e7576020019181360383136100e757565b908210156111305761112c9160051b8101906110c4565b9091565b611097565b908092918237015f815290565b3d1561116c573d9061115382610361565b916111616040519384610320565b82523d5f602084013e565b606090565b6020818303126100e75780519067ffffffffffffffff82116100e7570181601f820112156100e75780516111a481610361565b926111b26040519485610320565b818452602082840101116100e75761107c91602080850191016104f8565b80518210156111305760209160051b010190565b9190916111f08361107f565b9060406112006040519384610320565b8483527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe061122d8661107f565b015f5b8181106112ba57505082945f5b81811061124b575050505050565b5f80611258838588611115565b90611267875180938193611135565b0390305af4611274611142565b901561129a579060019161128882886111d0565b5261129381876111d0565b500161123d565b60448151106100e7578060046100e7920151602480918301019101611171565b806060602080938801015201611230565b73ffffffffffffffffffffffffffffffffffffffff9098959897949392979691966112f46113c2565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f551694853b156100e7576040517fef55bec6000000000000000000000000000000000000000000000000000000008152336004820152306024820152604481018890526064810193909352608483019390935260a482019790975260ff90961660c487015260e48601919091526101048501525f846101248183855af1938415610a5557610d78946113ac575b50611765565b6113b590610307565b5f6113a6565b156100e757565b60ff5f5416156100e757565b7fffffffff00000000000000000000000000000000000000000000000000000000903581811693926004811061140357505050565b60040360031b82901b16169150565b908160209103126100e7575190565b95949392919061147561147161146a61143a848b6113ce565b7fffffffff00000000000000000000000000000000000000000000000000000000165f52600160205260405f2090565b5460ff1690565b1590565b6115dc576040517f70a082310000000000000000000000000000000000000000000000000000000080825230600483015260209873ffffffffffffffffffffffffffffffffffffffff94919391929091908a816024818c8a165afa948515610a55578b915f966115bd575b5060405190815230600482015295869060249082908d165afa948515610a55576100f89a5f96611584575b50505f61157f92819261153d8661153760035473ffffffffffffffffffffffffffffffffffffffff1690565b8d6118cf565b8261155d60035473ffffffffffffffffffffffffffffffffffffffff1690565b9261156d60405180948193611135565b03925af1611579611142565b506113bb565b6119d9565b5f929650926115ad83928561157f96903d106115b6575b6115a58183610320565b810190611412565b9692509261150b565b503d61159b565b6115d5919650823d84116115b6576115a58183610320565b945f6114e0565b60046040517f42868c9b000000000000000000000000000000000000000000000000000000008152fd5b9290604051927f23b872dd00000000000000000000000000000000000000000000000000000000602085015273ffffffffffffffffffffffffffffffffffffffff809216602485015216604483015260648201526064815260a081019181831067ffffffffffffffff84111761031b576100f892604052611bb0565b3561107c816100c9565b3561107c816102cc565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe093818652868601375f8582860101520116010190565b9895909461107c9d9b97926020956117579a959c976101809d8d73ffffffffffffffffffffffffffffffffffffffff998a8096818096168452169101521660408d01521660608b015260808a015260a089015260c08801521660e086015263ffffffff8092166101008601521661012084015261014083019063ffffffff169052565b816101608201520191611696565b9190916117948361178e61099760025473ffffffffffffffffffffffffffffffffffffffff1690565b836118cf565b6117b661099760025473ffffffffffffffffffffffffffffffffffffffff1690565b916117c360408201611682565b6117cf60608301611682565b916117d981611682565b936117e660a08301611682565b6117f260c0840161168c565b6117fe60e0850161168c565b9061180c610100860161168c565b9261181b6101208701876110c4565b9690958b3b156100e7576040519c8d9b8c809c7f7b939232000000000000000000000000000000000000000000000000000000008252608086013595602001359473ffffffffffffffffffffffffffffffffffffffff16916004019c6118809d6116d4565b03815a5f948591f18015610a55576118955750565b80610a496100f892610307565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b60449192602073ffffffffffffffffffffffffffffffffffffffff604051948580927fdd62ed3e000000000000000000000000000000000000000000000000000000008252306004830152808916602483015286165afa928315610a55575f936119ab575b5082018092116119a6576040517f095ea7b300000000000000000000000000000000000000000000000000000000602082015273ffffffffffffffffffffffffffffffffffffffff9390931660248401526044808401929092529082526100f891906119a1606483610320565b611bb0565b6118a2565b6119c591935060203d6020116115b6576115a58183610320565b915f611934565b919082039182116119a657565b6040517f70a082310000000000000000000000000000000000000000000000000000000080825230600483015273ffffffffffffffffffffffffffffffffffffffff989697959695602095878b16959294939087826024818a5afa8015610a5557611a4b925f91611b93575b506119cc565b978810611b69576040519384523060048501528916928581602481875afa8015610a55578392611a81925f92611b4a57506119cc565b03611b20576100f8977f646284e396b68ff4b4f34e0aa97bcdb9c100f5b44a20da5c475f62703985384191611b18611ace60035473ffffffffffffffffffffffffffffffffffffffff1690565b89611ad88c611682565b9360405195869516998d013592859094939260609273ffffffffffffffffffffffffffffffffffffffff6080840197168352602083015260408201520152565b0390a4611765565b60046040517fd6cf42f0000000000000000000000000000000000000000000000000000000008152fd5b611b62919250883d8a116115b6576115a58183610320565b905f611a45565b60046040517f0492ff87000000000000000000000000000000000000000000000000000000008152fd5b611baa9150893d8b116115b6576115a58183610320565b5f611a45565b73ffffffffffffffffffffffffffffffffffffffff166040516040810181811067ffffffffffffffff82111761031b57611c2b937f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c656460205f948594604052818152015260208151910182855af1611c25611142565b91611c64565b8051908115918215611c41575b5050156100e757565b81925090602091810103126100e7576020015180151581036100e7575f80611c38565b9015611c7e57815115611c75575090565b3b156100e75790565b5080519081156100e757602001fdfea2646970667358221220273204c702747fdff0ffe95c6319ba2f475d4a21c4beb991049bd8c21271b20064736f6c63430008170033"; + +type SpokePoolV3PeripheryConstructorParams = + | [signer?: Signer] + | ConstructorParameters; + +const isSuperArgs = ( + xs: SpokePoolV3PeripheryConstructorParams +): xs is ConstructorParameters => xs.length > 1; + +export class SpokePoolV3Periphery__factory extends ContractFactory { + constructor(...args: SpokePoolV3PeripheryConstructorParams) { + if (isSuperArgs(args)) { + super(...args); + } else { + super(_abi, _bytecode, args[0]); + } + } + + override deploy( + _allowedSelectors: BytesLike[], + overrides?: Overrides & { from?: string } + ): Promise { + return super.deploy( + _allowedSelectors, + overrides || {} + ) as Promise; + } + override getDeployTransaction( + _allowedSelectors: BytesLike[], + overrides?: Overrides & { from?: string } + ): TransactionRequest { + return super.getDeployTransaction(_allowedSelectors, overrides || {}); + } + override attach(address: string): SpokePoolV3Periphery { + return super.attach(address) as SpokePoolV3Periphery; + } + override connect(signer: Signer): SpokePoolV3Periphery__factory { + return super.connect(signer) as SpokePoolV3Periphery__factory; + } + + static readonly bytecode = _bytecode; + static readonly abi = _abi; + static createInterface(): SpokePoolV3PeripheryInterface { + return new utils.Interface(_abi) as SpokePoolV3PeripheryInterface; + } + static connect( + address: string, + signerOrProvider: Signer | Provider + ): SpokePoolV3Periphery { + return new Contract( + address, + _abi, + signerOrProvider + ) as SpokePoolV3Periphery; + } +} diff --git a/api/_typechain/factories/SwapAndBridge.sol/SwapAndBridgeBase__factory.ts b/api/_typechain/factories/SwapAndBridge.sol/SwapAndBridgeBase__factory.ts deleted file mode 100644 index 39c7908d8..000000000 --- a/api/_typechain/factories/SwapAndBridge.sol/SwapAndBridgeBase__factory.ts +++ /dev/null @@ -1,153 +0,0 @@ -/* Autogenerated file. Do not edit manually. */ -/* tslint:disable */ - -import { Contract, Signer, utils } from "ethers"; -import type { Provider } from "@ethersproject/providers"; -import type { - SwapAndBridgeBase, - SwapAndBridgeBaseInterface, -} from "../../SwapAndBridge.sol/SwapAndBridgeBase"; - -const _abi = [ - { - inputs: [], - name: "InvalidFunctionSelector", - type: "error", - }, - { - inputs: [], - name: "LeftoverSrcTokens", - type: "error", - }, - { - inputs: [], - name: "MinimumExpectedInputAmount", - type: "error", - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "address", - name: "exchange", - type: "address", - }, - { - indexed: true, - internalType: "address", - name: "swapToken", - type: "address", - }, - { - indexed: true, - internalType: "address", - name: "acrossInputToken", - type: "address", - }, - { - indexed: false, - internalType: "uint256", - name: "swapTokenAmount", - type: "uint256", - }, - { - indexed: false, - internalType: "uint256", - name: "acrossInputAmount", - type: "uint256", - }, - { - indexed: true, - internalType: "address", - name: "acrossOutputToken", - type: "address", - }, - { - indexed: false, - internalType: "uint256", - name: "acrossOutputAmount", - type: "uint256", - }, - ], - name: "SwapBeforeBridge", - type: "event", - }, - { - inputs: [], - name: "EXCHANGE", - outputs: [ - { - internalType: "address", - name: "", - type: "address", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "SPOKE_POOL", - outputs: [ - { - internalType: "contract V3SpokePoolInterface", - name: "", - type: "address", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "bytes4", - name: "", - type: "bytes4", - }, - ], - name: "allowedSelectors", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "bytes[]", - name: "data", - type: "bytes[]", - }, - ], - name: "multicall", - outputs: [ - { - internalType: "bytes[]", - name: "results", - type: "bytes[]", - }, - ], - stateMutability: "nonpayable", - type: "function", - }, -] as const; - -export class SwapAndBridgeBase__factory { - static readonly abi = _abi; - static createInterface(): SwapAndBridgeBaseInterface { - return new utils.Interface(_abi) as SwapAndBridgeBaseInterface; - } - static connect( - address: string, - signerOrProvider: Signer | Provider - ): SwapAndBridgeBase { - return new Contract(address, _abi, signerOrProvider) as SwapAndBridgeBase; - } -} diff --git a/api/_typechain/factories/SwapAndBridge.sol/SwapAndBridge__factory.ts b/api/_typechain/factories/SwapAndBridge.sol/SwapAndBridge__factory.ts deleted file mode 100644 index 770f17c74..000000000 --- a/api/_typechain/factories/SwapAndBridge.sol/SwapAndBridge__factory.ts +++ /dev/null @@ -1,367 +0,0 @@ -/* Autogenerated file. Do not edit manually. */ -/* tslint:disable */ - -import { - Signer, - utils, - Contract, - ContractFactory, - BytesLike, - Overrides, -} from "ethers"; -import type { Provider, TransactionRequest } from "@ethersproject/providers"; -import type { - SwapAndBridge, - SwapAndBridgeInterface, -} from "../../SwapAndBridge.sol/SwapAndBridge"; - -const _abi = [ - { - inputs: [ - { - internalType: "contract V3SpokePoolInterface", - name: "_spokePool", - type: "address", - }, - { - internalType: "contract WETH9Interface", - name: "_wrappedNativeToken", - type: "address", - }, - { - internalType: "address", - name: "_exchange", - type: "address", - }, - { - internalType: "bytes4[]", - name: "_allowedSelectors", - type: "bytes4[]", - }, - { - internalType: "contract IERC20", - name: "_swapToken", - type: "address", - }, - { - internalType: "contract IERC20", - name: "_acrossInputToken", - type: "address", - }, - ], - stateMutability: "nonpayable", - type: "constructor", - }, - { - inputs: [], - name: "InvalidFunctionSelector", - type: "error", - }, - { - inputs: [], - name: "LeftoverSrcTokens", - type: "error", - }, - { - inputs: [], - name: "MinimumExpectedInputAmount", - type: "error", - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "address", - name: "exchange", - type: "address", - }, - { - indexed: true, - internalType: "address", - name: "swapToken", - type: "address", - }, - { - indexed: true, - internalType: "address", - name: "acrossInputToken", - type: "address", - }, - { - indexed: false, - internalType: "uint256", - name: "swapTokenAmount", - type: "uint256", - }, - { - indexed: false, - internalType: "uint256", - name: "acrossInputAmount", - type: "uint256", - }, - { - indexed: true, - internalType: "address", - name: "acrossOutputToken", - type: "address", - }, - { - indexed: false, - internalType: "uint256", - name: "acrossOutputAmount", - type: "uint256", - }, - ], - name: "SwapBeforeBridge", - type: "event", - }, - { - inputs: [], - name: "ACROSS_INPUT_TOKEN", - outputs: [ - { - internalType: "contract IERC20", - name: "", - type: "address", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "EXCHANGE", - outputs: [ - { - internalType: "address", - name: "", - type: "address", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "SPOKE_POOL", - outputs: [ - { - internalType: "contract V3SpokePoolInterface", - name: "", - type: "address", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "SWAP_TOKEN", - outputs: [ - { - internalType: "contract IERC20", - name: "", - type: "address", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "bytes4", - name: "", - type: "bytes4", - }, - ], - name: "allowedSelectors", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "bytes[]", - name: "data", - type: "bytes[]", - }, - ], - name: "multicall", - outputs: [ - { - internalType: "bytes[]", - name: "results", - type: "bytes[]", - }, - ], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "bytes", - name: "routerCalldata", - type: "bytes", - }, - { - internalType: "uint256", - name: "swapTokenAmount", - type: "uint256", - }, - { - internalType: "uint256", - name: "minExpectedInputTokenAmount", - type: "uint256", - }, - { - components: [ - { - internalType: "address", - name: "outputToken", - type: "address", - }, - { - internalType: "uint256", - name: "outputAmount", - type: "uint256", - }, - { - internalType: "address", - name: "depositor", - type: "address", - }, - { - internalType: "address", - name: "recipient", - type: "address", - }, - { - internalType: "uint256", - name: "destinationChainid", - type: "uint256", - }, - { - internalType: "address", - name: "exclusiveRelayer", - type: "address", - }, - { - internalType: "uint32", - name: "quoteTimestamp", - type: "uint32", - }, - { - internalType: "uint32", - name: "fillDeadline", - type: "uint32", - }, - { - internalType: "uint32", - name: "exclusivityDeadline", - type: "uint32", - }, - { - internalType: "bytes", - name: "message", - type: "bytes", - }, - ], - internalType: "struct SwapAndBridgeBase.DepositData", - name: "depositData", - type: "tuple", - }, - ], - name: "swapAndBridge", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, -] as const; - -const _bytecode = - "0x604061012060405234620001d2576200133d803803806200002081620001ea565b92833981019160c082840312620001d25781516001600160a01b03908181168103620001d25760209283850151938385168503620001d25760408601519384168403620001d25760608601516001600160401b039790888111620001d25787019080601f83011215620001d2578151988911620001d6576005918960051b908480620000ae818501620001ea565b809d81520192820101928311620001d2578401905b828210620001b057505050620000ea60a0620000e260808a0162000210565b980162000210565b97600196879660ff19966001885f5416175f5560805260a05260c0525f955b62000174575b888860e05261010090815260405161111791826200022683396080518281816101da0152610faf015260a05182818161041c015281816109600152610d18015260c05182505060e05182818160c9015261082c01525181818161016c015261084e0152f35b8051861015620001aa5786809663ffffffff60e01b8582861b85010151165f52818552855f208288825416179055019562000109565b6200010f565b81516001600160e01b031981168103620001d2578152908401908401620000c3565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b6040519190601f01601f191682016001600160401b03811183821017620001d657604052565b51906001600160a01b0382168203620001d25756fe60806040526004361015610011575f80fd5b5f3560e01c8063393bb94314610084578063652fa4b91461007f5780638021fef71461007a57806385f168eb14610075578063ac9650d814610070578063b50e44b81461006b5763e65ae3ae14610066575f80fd5b61044f565b6103d2565b61034a565b6101fe565b610190565b610122565b34610114575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101145773ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000166080527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8060a0016080f35b5f80fd5b5f91031261011457565b34610114575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011457602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b34610114575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011457602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b346101145760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610114576004357fffffffff000000000000000000000000000000000000000000000000000000008116809103610114575f526001602052602060ff60405f2054166040519015158152f35b5f5b8381106102865750505f910152565b8181015183820152602001610277565b602080820190808352835180925260408301928160408460051b8301019501935f915b8483106102c95750505050505090565b909192939495848080837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc086600196030187527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8c5161033681518092818752878088019101610275565b0116010198019301930191949392906102b9565b346101145760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101145767ffffffffffffffff6004358181116101145736602382011215610114578060040135918211610114573660248360051b83010111610114576103ce9160246103c2920161070a565b60405191829182610296565b0390f35b34610114575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261011457602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b90816101409103126101145790565b346101145760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101145767ffffffffffffffff600435818111610114573660238201121561011457806004013582811161011457366024828401011161011457606435928311610114576104cf6104df933690600401610440565b91604435916024803592016107f1565b005b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b67ffffffffffffffff811161052257604052565b6104e1565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761052257604052565b67ffffffffffffffff81116105225760051b60200190565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215610114570180359067ffffffffffffffff82116101145760200191813603831361011457565b90821015610619576106159160051b8101906105ad565b9091565b610580565b908092918237015f815290565b67ffffffffffffffff811161052257601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b3d1561068f573d906106768261062b565b916106846040519384610527565b82523d5f602084013e565b606090565b6020818303126101145780519067ffffffffffffffff8211610114570181601f820112156101145780516106c78161062b565b926106d56040519485610527565b81845260208284010111610114576106f39160208085019101610275565b90565b80518210156106195760209160051b010190565b91909161071683610568565b9060406107266040519384610527565b8483527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe061075386610568565b015f5b8181106107e057505082945f5b818110610771575050505050565b5f8061077e8385886105fe565b9061078d87518093819361061e565b0390305af461079a610665565b90156107c057906001916107ae82886106f6565b526107b981876106f6565b5001610763565b604481511061011457806004610114920151602480918301019101610694565b806060602080938801015201610756565b9193909260ff5f5416156101145761082a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b7f0000000000000000000000000000000000000000000000000000000000000000927f0000000000000000000000000000000000000000000000000000000000000000946108bc6108b86108b16108818486610a73565b7fffffffff00000000000000000000000000000000000000000000000000000000165f52600160205260405f2090565b5460ff1690565b1590565b610a42576040517f70a082310000000000000000000000000000000000000000000000000000000080825230600483015273ffffffffffffffffffffffffffffffffffffffff98919260209182856024818c8f165afa948515610a1c575f95610a21575b506040519081523060048201529982908b9060249082908d165afa948515610a1c576109ac9a89935f976109dc575b50506109a7925f92838093610986887f00000000000000000000000000000000000000000000000000000000000000008096610afe565b6109956040518094819361061e565b03925af16109a1610665565b50610a6c565b610c29565b6109da60017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f5416175f55565b565b5f809491819499506109a7969381610a0892903d10610a15575b610a008183610527565b810190610ab7565b989350509281945061094f565b503d6109f6565b610ac6565b83919550610a3b90823d8411610a1557610a008183610527565b9490610920565b60046040517f42868c9b000000000000000000000000000000000000000000000000000000008152fd5b1561011457565b7fffffffff000000000000000000000000000000000000000000000000000000009035818116939260048110610aa857505050565b60040360031b82901b16169150565b90816020910312610114575190565b6040513d5f823e3d90fd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b60449192602073ffffffffffffffffffffffffffffffffffffffff604051948580927fdd62ed3e000000000000000000000000000000000000000000000000000000008252306004830152808916602483015286165afa928315610a1c575f93610bda575b508201809211610bd5576040517f095ea7b300000000000000000000000000000000000000000000000000000000602082015273ffffffffffffffffffffffffffffffffffffffff9390931660248401526044808401929092529082526109da9190610bd0606483610527565b610e02565b610ad1565b610bf491935060203d602011610a1557610a008183610527565b915f610b63565b91908203918211610bd557565b3573ffffffffffffffffffffffffffffffffffffffff811681036101145790565b6040517f70a082310000000000000000000000000000000000000000000000000000000080825230600483015273ffffffffffffffffffffffffffffffffffffffff989697959695602095878b16959294939087826024818a5afa8015610a1c57610c9b925f91610de5575b50610bfb565b978810610dbb576040519384523060048501528916928581602481875afa8015610a1c578392610cd1925f92610d9c5750610bfb565b03610d72576109da977f646284e396b68ff4b4f34e0aa97bcdb9c100f5b44a20da5c475f62703985384191610d6a610d088a610c08565b918960405194859416988c0135917f0000000000000000000000000000000000000000000000000000000000000000859094939260609273ffffffffffffffffffffffffffffffffffffffff6080840197168352602083015260408201520152565b0390a4610f96565b60046040517fd6cf42f0000000000000000000000000000000000000000000000000000000008152fd5b610db4919250883d8a11610a1557610a008183610527565b905f610c95565b60046040517f0492ff87000000000000000000000000000000000000000000000000000000008152fd5b610dfc9150893d8b11610a1557610a008183610527565b5f610c95565b73ffffffffffffffffffffffffffffffffffffffff166040516040810181811067ffffffffffffffff82111761052257610e7d937f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c656460205f948594604052818152015260208151910182855af1610e77610665565b916110b8565b8051908115918215610e93575b50501561011457565b819250906020918101031261011457602001518015158103610114575f80610e8a565b3563ffffffff811681036101145790565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe093818652868601375f8582860101520116010190565b989590946106f39d9b9792602095610f889a959c976101809d8d73ffffffffffffffffffffffffffffffffffffffff998a8096818096168452169101521660408d01521660608b015260808a015260a089015260c08801521660e086015263ffffffff8092166101008601521661012084015261014083019063ffffffff169052565b816101608201520191610ec7565b9073ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001692610fdb838583610afe565b610fe760408601610c08565b90610ff460608701610c08565b93610ffe87610c08565b9661100b60a08201610c08565b61101760c08301610eb6565b61102360e08401610eb6565b916110316101008501610eb6565b936110406101208201826105ad565b9790968c3b15610114576040519d8e809d819d7f7b939232000000000000000000000000000000000000000000000000000000008352608087013596602001359516916004019c6110909d610f05565b03815a5f948591f18015610a1c576110a55750565b806110b26109da9261050e565b80610118565b90156110d2578151156110c9575090565b3b156101145790565b50805190811561011457602001fdfea2646970667358221220026a7409d965bcdd82516120169821b365afc3acc0d52608453d28ca5160c46464736f6c63430008170033"; - -type SwapAndBridgeConstructorParams = - | [signer?: Signer] - | ConstructorParameters; - -const isSuperArgs = ( - xs: SwapAndBridgeConstructorParams -): xs is ConstructorParameters => xs.length > 1; - -export class SwapAndBridge__factory extends ContractFactory { - constructor(...args: SwapAndBridgeConstructorParams) { - if (isSuperArgs(args)) { - super(...args); - } else { - super(_abi, _bytecode, args[0]); - } - } - - override deploy( - _spokePool: string, - _wrappedNativeToken: string, - _exchange: string, - _allowedSelectors: BytesLike[], - _swapToken: string, - _acrossInputToken: string, - overrides?: Overrides & { from?: string } - ): Promise { - return super.deploy( - _spokePool, - _wrappedNativeToken, - _exchange, - _allowedSelectors, - _swapToken, - _acrossInputToken, - overrides || {} - ) as Promise; - } - override getDeployTransaction( - _spokePool: string, - _wrappedNativeToken: string, - _exchange: string, - _allowedSelectors: BytesLike[], - _swapToken: string, - _acrossInputToken: string, - overrides?: Overrides & { from?: string } - ): TransactionRequest { - return super.getDeployTransaction( - _spokePool, - _wrappedNativeToken, - _exchange, - _allowedSelectors, - _swapToken, - _acrossInputToken, - overrides || {} - ); - } - override attach(address: string): SwapAndBridge { - return super.attach(address) as SwapAndBridge; - } - override connect(signer: Signer): SwapAndBridge__factory { - return super.connect(signer) as SwapAndBridge__factory; - } - - static readonly bytecode = _bytecode; - static readonly abi = _abi; - static createInterface(): SwapAndBridgeInterface { - return new utils.Interface(_abi) as SwapAndBridgeInterface; - } - static connect( - address: string, - signerOrProvider: Signer | Provider - ): SwapAndBridge { - return new Contract(address, _abi, signerOrProvider) as SwapAndBridge; - } -} diff --git a/api/_typechain/factories/SwapAndBridge.sol/UniversalSwapAndBridge__factory.ts b/api/_typechain/factories/SwapAndBridge.sol/UniversalSwapAndBridge__factory.ts deleted file mode 100644 index 740fd1bcf..000000000 --- a/api/_typechain/factories/SwapAndBridge.sol/UniversalSwapAndBridge__factory.ts +++ /dev/null @@ -1,777 +0,0 @@ -/* Autogenerated file. Do not edit manually. */ -/* tslint:disable */ - -import { - Signer, - utils, - Contract, - ContractFactory, - BytesLike, - Overrides, -} from "ethers"; -import type { Provider, TransactionRequest } from "@ethersproject/providers"; -import type { - UniversalSwapAndBridge, - UniversalSwapAndBridgeInterface, -} from "../../SwapAndBridge.sol/UniversalSwapAndBridge"; - -const _abi = [ - { - inputs: [ - { - internalType: "contract V3SpokePoolInterface", - name: "_spokePool", - type: "address", - }, - { - internalType: "contract WETH9Interface", - name: "_wrappedNativeToken", - type: "address", - }, - { - internalType: "address", - name: "_exchange", - type: "address", - }, - { - internalType: "bytes4[]", - name: "_allowedSelectors", - type: "bytes4[]", - }, - ], - stateMutability: "nonpayable", - type: "constructor", - }, - { - inputs: [], - name: "InsufficientSwapValue", - type: "error", - }, - { - inputs: [], - name: "InvalidFunctionSelector", - type: "error", - }, - { - inputs: [], - name: "InvalidSwapToken", - type: "error", - }, - { - inputs: [], - name: "LeftoverSrcTokens", - type: "error", - }, - { - inputs: [], - name: "MinimumExpectedInputAmount", - type: "error", - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "address", - name: "exchange", - type: "address", - }, - { - indexed: true, - internalType: "address", - name: "swapToken", - type: "address", - }, - { - indexed: true, - internalType: "address", - name: "acrossInputToken", - type: "address", - }, - { - indexed: false, - internalType: "uint256", - name: "swapTokenAmount", - type: "uint256", - }, - { - indexed: false, - internalType: "uint256", - name: "acrossInputAmount", - type: "uint256", - }, - { - indexed: true, - internalType: "address", - name: "acrossOutputToken", - type: "address", - }, - { - indexed: false, - internalType: "uint256", - name: "acrossOutputAmount", - type: "uint256", - }, - ], - name: "SwapBeforeBridge", - type: "event", - }, - { - inputs: [], - name: "EXCHANGE", - outputs: [ - { - internalType: "address", - name: "", - type: "address", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "SPOKE_POOL", - outputs: [ - { - internalType: "contract V3SpokePoolInterface", - name: "", - type: "address", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "bytes4", - name: "", - type: "bytes4", - }, - ], - name: "allowedSelectors", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "contract IERC20Auth", - name: "acrossInputToken", - type: "address", - }, - { - internalType: "uint256", - name: "acrossInputAmount", - type: "uint256", - }, - { - components: [ - { - internalType: "address", - name: "outputToken", - type: "address", - }, - { - internalType: "uint256", - name: "outputAmount", - type: "uint256", - }, - { - internalType: "address", - name: "depositor", - type: "address", - }, - { - internalType: "address", - name: "recipient", - type: "address", - }, - { - internalType: "uint256", - name: "destinationChainid", - type: "uint256", - }, - { - internalType: "address", - name: "exclusiveRelayer", - type: "address", - }, - { - internalType: "uint32", - name: "quoteTimestamp", - type: "uint32", - }, - { - internalType: "uint32", - name: "fillDeadline", - type: "uint32", - }, - { - internalType: "uint32", - name: "exclusivityDeadline", - type: "uint32", - }, - { - internalType: "bytes", - name: "message", - type: "bytes", - }, - ], - internalType: "struct SwapAndBridgeBase.DepositData", - name: "depositData", - type: "tuple", - }, - { - internalType: "uint256", - name: "validAfter", - type: "uint256", - }, - { - internalType: "uint256", - name: "validBefore", - type: "uint256", - }, - { - internalType: "bytes32", - name: "nonce", - type: "bytes32", - }, - { - internalType: "uint8", - name: "v", - type: "uint8", - }, - { - internalType: "bytes32", - name: "r", - type: "bytes32", - }, - { - internalType: "bytes32", - name: "s", - type: "bytes32", - }, - ], - name: "depositWithAuthorization", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "contract IERC20Permit", - name: "acrossInputToken", - type: "address", - }, - { - internalType: "uint256", - name: "acrossInputAmount", - type: "uint256", - }, - { - components: [ - { - internalType: "address", - name: "outputToken", - type: "address", - }, - { - internalType: "uint256", - name: "outputAmount", - type: "uint256", - }, - { - internalType: "address", - name: "depositor", - type: "address", - }, - { - internalType: "address", - name: "recipient", - type: "address", - }, - { - internalType: "uint256", - name: "destinationChainid", - type: "uint256", - }, - { - internalType: "address", - name: "exclusiveRelayer", - type: "address", - }, - { - internalType: "uint32", - name: "quoteTimestamp", - type: "uint32", - }, - { - internalType: "uint32", - name: "fillDeadline", - type: "uint32", - }, - { - internalType: "uint32", - name: "exclusivityDeadline", - type: "uint32", - }, - { - internalType: "bytes", - name: "message", - type: "bytes", - }, - ], - internalType: "struct SwapAndBridgeBase.DepositData", - name: "depositData", - type: "tuple", - }, - { - internalType: "uint256", - name: "deadline", - type: "uint256", - }, - { - internalType: "uint8", - name: "v", - type: "uint8", - }, - { - internalType: "bytes32", - name: "r", - type: "bytes32", - }, - { - internalType: "bytes32", - name: "s", - type: "bytes32", - }, - ], - name: "depositWithPermit", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "bytes[]", - name: "data", - type: "bytes[]", - }, - ], - name: "multicall", - outputs: [ - { - internalType: "bytes[]", - name: "results", - type: "bytes[]", - }, - ], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "contract IERC20", - name: "swapToken", - type: "address", - }, - { - internalType: "contract IERC20", - name: "acrossInputToken", - type: "address", - }, - { - internalType: "bytes", - name: "routerCalldata", - type: "bytes", - }, - { - internalType: "uint256", - name: "swapTokenAmount", - type: "uint256", - }, - { - internalType: "uint256", - name: "minExpectedInputTokenAmount", - type: "uint256", - }, - { - components: [ - { - internalType: "address", - name: "outputToken", - type: "address", - }, - { - internalType: "uint256", - name: "outputAmount", - type: "uint256", - }, - { - internalType: "address", - name: "depositor", - type: "address", - }, - { - internalType: "address", - name: "recipient", - type: "address", - }, - { - internalType: "uint256", - name: "destinationChainid", - type: "uint256", - }, - { - internalType: "address", - name: "exclusiveRelayer", - type: "address", - }, - { - internalType: "uint32", - name: "quoteTimestamp", - type: "uint32", - }, - { - internalType: "uint32", - name: "fillDeadline", - type: "uint32", - }, - { - internalType: "uint32", - name: "exclusivityDeadline", - type: "uint32", - }, - { - internalType: "bytes", - name: "message", - type: "bytes", - }, - ], - internalType: "struct SwapAndBridgeBase.DepositData", - name: "depositData", - type: "tuple", - }, - ], - name: "swapAndBridge", - outputs: [], - stateMutability: "payable", - type: "function", - }, - { - inputs: [ - { - internalType: "contract IERC20Auth", - name: "swapToken", - type: "address", - }, - { - internalType: "contract IERC20", - name: "acrossInputToken", - type: "address", - }, - { - internalType: "bytes", - name: "routerCalldata", - type: "bytes", - }, - { - internalType: "uint256", - name: "swapTokenAmount", - type: "uint256", - }, - { - internalType: "uint256", - name: "minExpectedInputTokenAmount", - type: "uint256", - }, - { - components: [ - { - internalType: "address", - name: "outputToken", - type: "address", - }, - { - internalType: "uint256", - name: "outputAmount", - type: "uint256", - }, - { - internalType: "address", - name: "depositor", - type: "address", - }, - { - internalType: "address", - name: "recipient", - type: "address", - }, - { - internalType: "uint256", - name: "destinationChainid", - type: "uint256", - }, - { - internalType: "address", - name: "exclusiveRelayer", - type: "address", - }, - { - internalType: "uint32", - name: "quoteTimestamp", - type: "uint32", - }, - { - internalType: "uint32", - name: "fillDeadline", - type: "uint32", - }, - { - internalType: "uint32", - name: "exclusivityDeadline", - type: "uint32", - }, - { - internalType: "bytes", - name: "message", - type: "bytes", - }, - ], - internalType: "struct SwapAndBridgeBase.DepositData", - name: "depositData", - type: "tuple", - }, - { - internalType: "uint256", - name: "validAfter", - type: "uint256", - }, - { - internalType: "uint256", - name: "validBefore", - type: "uint256", - }, - { - internalType: "bytes32", - name: "nonce", - type: "bytes32", - }, - { - internalType: "uint8", - name: "v", - type: "uint8", - }, - { - internalType: "bytes32", - name: "r", - type: "bytes32", - }, - { - internalType: "bytes32", - name: "s", - type: "bytes32", - }, - ], - name: "swapAndBridgeWithAuthorization", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "contract IERC20Permit", - name: "swapToken", - type: "address", - }, - { - internalType: "contract IERC20", - name: "acrossInputToken", - type: "address", - }, - { - internalType: "bytes", - name: "routerCalldata", - type: "bytes", - }, - { - internalType: "uint256", - name: "swapTokenAmount", - type: "uint256", - }, - { - internalType: "uint256", - name: "minExpectedInputTokenAmount", - type: "uint256", - }, - { - components: [ - { - internalType: "address", - name: "outputToken", - type: "address", - }, - { - internalType: "uint256", - name: "outputAmount", - type: "uint256", - }, - { - internalType: "address", - name: "depositor", - type: "address", - }, - { - internalType: "address", - name: "recipient", - type: "address", - }, - { - internalType: "uint256", - name: "destinationChainid", - type: "uint256", - }, - { - internalType: "address", - name: "exclusiveRelayer", - type: "address", - }, - { - internalType: "uint32", - name: "quoteTimestamp", - type: "uint32", - }, - { - internalType: "uint32", - name: "fillDeadline", - type: "uint32", - }, - { - internalType: "uint32", - name: "exclusivityDeadline", - type: "uint32", - }, - { - internalType: "bytes", - name: "message", - type: "bytes", - }, - ], - internalType: "struct SwapAndBridgeBase.DepositData", - name: "depositData", - type: "tuple", - }, - { - internalType: "uint256", - name: "deadline", - type: "uint256", - }, - { - internalType: "uint8", - name: "v", - type: "uint8", - }, - { - internalType: "bytes32", - name: "r", - type: "bytes32", - }, - { - internalType: "bytes32", - name: "s", - type: "bytes32", - }, - ], - name: "swapAndBridgeWithPermit", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, -] as const; - -const _bytecode = - "0x604060e060405234620001905762001b2390813803806200002081620001a8565b9384398201608083820312620001905782516001600160a01b03929091908383168303620001905760209485810151958587168703620001905760408201519586168603620001905760608201516001600160401b03928382116200019057019380601f860112156200019057845192831162000194576005948360051b908380620000ae818501620001a8565b80978152019282010192831162000190578301905b8282106200016e57505050600196879660ff19966001885f5416175f5560805260a05260c0525f955b62000132575b6040516119549081620001cf82396080518181816102fb0152611418015260a05181818161053d015281816111d20152611757015260c051816106c70152f35b8151861015620001685786809663ffffffff60e01b8382881b86010151165f52818352845f2082888254161790550195620000ec565b620000f2565b81516001600160e01b03198116810362000190578152908301908301620000c3565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b6040519190601f01601f191682016001600160401b03811183821017620001945760405256fe60806040526004361015610011575f80fd5b5f3560e01c8063038f12ea146100a4578063277deffe1461009f5780638021fef71461009a57806385f168eb14610095578063ac9650d814610090578063b50e44b81461008b578063bdf52ad314610086578063c51e5eb9146100815763fdf152d31461007c575f80fd5b6107f6565b6105ee565b610561565b6104f3565b61046b565b61031f565b6102b1565b6101ff565b34610153576101807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610153576100dc610175565b6100e4610184565b67ffffffffffffffff919060443583811161015357610107903690600401610191565b9260a435948511610153576101236101519536906004016101bf565b9361012c6101ce565b9261016435956101443595610104359460e4359460c4359460843593606435936109d4565b005b5f80fd5b73ffffffffffffffffffffffffffffffffffffffff81160361015357565b6004359061018282610157565b565b6024359061018282610157565b9181601f840112156101535782359167ffffffffffffffff8311610153576020838186019501011161015357565b90816101409103126101535790565b610124359060ff8216820361015357565b60e4359060ff8216820361015357565b6084359060ff8216820361015357565b34610153576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101535760043561023b81610157565b610243610184565b67ffffffffffffffff919060443583811161015357610266903690600401610191565b919060a435948511610153576102836101519536906004016101bf565b61028b6101df565b926101243595610104359560c435946084359360643593610b86565b5f91031261015357565b34610153575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015357602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b346101535760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610153576004357fffffffff000000000000000000000000000000000000000000000000000000008116809103610153575f526001602052602060ff60405f2054166040519015158152f35b5f5b8381106103a75750505f910152565b8181015183820152602001610398565b602080820190808352835180925260408301928160408460051b8301019501935f915b8483106103ea5750505050505090565b909192939495848080837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc086600196030187527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8c5161045781518092818752878088019101610396565b0116010198019301930191949392906103da565b346101535760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101535767ffffffffffffffff6004358181116101535736602382011215610153578060040135918211610153573660248360051b83010111610153576104ef9160246104e39201610eab565b604051918291826103b7565b0390f35b34610153575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261015357602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b34610153576101207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101535760043561059d81610157565b60443567ffffffffffffffff8111610153576105bd9036906004016101bf565b9060c4359160ff831683036101535761015192610104359260e4359260a43591608435916064359160243590610f92565b60c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610153576004803561062581610157565b6024359061063282610157565b67ffffffffffffffff604435818111610153576106529036908601610191565b916064359060a4359081116101535761066e90369088016101bf565b92610677611089565b6106a27fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b34156107e0578134036107b75773ffffffffffffffffffffffffffffffffffffffff807f000000000000000000000000000000000000000000000000000000000000000016809187160361078e57803b15610153575f90604051988980927fd0e30db000000000000000000000000000000000000000000000000000000000825234905af19687156107895761074297610770575b505b608435926110d9565b61015160017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f5416175f55565b8061077d61078392610b21565b806102a7565b5f610737565b610b7b565b876040517f3539a701000000000000000000000000000000000000000000000000000000008152fd5b866040517ff143e0da000000000000000000000000000000000000000000000000000000008152fd5b61074296506107f182303388611299565b610739565b346101535760e07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101535760043561083181610157565b60243560443567ffffffffffffffff8111610153576108549036906004016101bf565b9073ffffffffffffffffffffffffffffffffffffffff6108726101ef565b9361087b611089565b6108a67fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b6040517fdd62ed3e000000000000000000000000000000000000000000000000000000008152336004820152306024820152911690602081604481855afa80156107895783915f916109a5575b5010610910575b610742935061090b82303384611299565b6113ff565b803b15610153576040517fd505accf00000000000000000000000000000000000000000000000000000000815233600482015230602482015260448101839052606480359082015260ff94909416608485015260a480359085015260c48035908501525f8460e48183855af19384156107895761074294610992575b506108fa565b8061077d61099f92610b21565b5f61098c565b6109c7915060203d6020116109cd575b6109bf8183610b3a565b810190610cfa565b5f6108f3565b503d6109b5565b73ffffffffffffffffffffffffffffffffffffffff909c9a919b94979295989396999c6109ff611089565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f5516998a3b15610153576040517fef55bec6000000000000000000000000000000000000000000000000000000008152336004820152306024820152604481018990526064810193909352608483019390935260a482019b909b5260ff909a1660c48b015260e48a01919091526101048901525f8861012481838a5af197881561078957610ab798610ae5575b506110d9565b61018260017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f5416175f55565b610aee90610b21565b5f610ab1565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b67ffffffffffffffff8111610b3557604052565b610af4565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117610b3557604052565b6040513d5f823e3d90fd5b73ffffffffffffffffffffffffffffffffffffffff909a9293949596979891999a610baf611089565b610bda7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b6040517fdd62ed3e000000000000000000000000000000000000000000000000000000008152336004820152306024820152911698906020816044818d5afa80156107895787915f91610cdb575b5010610c48575b505050610ab79750610c4383303389611299565b6110d9565b883b15610153576040517fd505accf00000000000000000000000000000000000000000000000000000000815233600482015230602482015260448101879052606481019190915260ff91909116608482015260a481019990995260c48901525f8860e481838a5af197881561078957610ab798610cc8575b8080610c2f565b8061077d610cd592610b21565b5f610cc1565b610cf4915060203d6020116109cd576109bf8183610b3a565b5f610c28565b90816020910312610153575190565b67ffffffffffffffff8111610b355760051b60200190565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215610153570180359067ffffffffffffffff82116101535760200191813603831361015357565b90821015610dba57610db69160051b810190610d4e565b9091565b610d21565b908092918237015f815290565b67ffffffffffffffff8111610b3557601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b3d15610e30573d90610e1782610dcc565b91610e256040519384610b3a565b82523d5f602084013e565b606090565b6020818303126101535780519067ffffffffffffffff8211610153570181601f82011215610153578051610e6881610dcc565b92610e766040519485610b3a565b8184526020828401011161015357610e949160208085019101610396565b90565b8051821015610dba5760209160051b010190565b919091610eb783610d09565b906040610ec76040519384610b3a565b8483527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0610ef486610d09565b015f5b818110610f8157505082945f5b818110610f12575050505050565b5f80610f1f838588610d9f565b90610f2e875180938193610dbf565b0390305af4610f3b610e06565b9015610f615790600191610f4f8288610e97565b52610f5a8187610e97565b5001610f04565b604481511061015357806004610153920151602480918301019101610e35565b806060602080938801015201610ef7565b73ffffffffffffffffffffffffffffffffffffffff909895989794939297969196610fbb611089565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f551694853b15610153576040517fef55bec6000000000000000000000000000000000000000000000000000000008152336004820152306024820152604481018890526064810193909352608483019390935260a482019790975260ff90961660c487015260e48601919091526101048501525f846101248183855af193841561078957610ab794611073575b506113ff565b61107c90610b21565b5f61106d565b1561015357565b60ff5f54161561015357565b7fffffffff0000000000000000000000000000000000000000000000000000000090358181169392600481106110ca57505050565b60040360031b82901b16169150565b9095949392919561112e61112a6111236110f38486611095565b7fffffffff00000000000000000000000000000000000000000000000000000000165f52600160205260405f2090565b5460ff1690565b1590565b61126f576040517f70a082310000000000000000000000000000000000000000000000000000000080825230600483015273ffffffffffffffffffffffffffffffffffffffff98919260209182856024818c8f165afa948515610789575f9561124e575b506040519081523060048201529982908b9060249082908d165afa948515610789576101829a89935f9761121e575b5050611219925f928380936111f8887f00000000000000000000000000000000000000000000000000000000000000008096611548565b61120760405180948193610dbf565b03925af1611213610e06565b50611082565b611668565b5f8094918194995061121996938161124192903d106109cd576109bf8183610b3a565b98935050928194506111c1565b8391955061126890823d84116109cd576109bf8183610b3a565b9490611192565b60046040517f42868c9b000000000000000000000000000000000000000000000000000000008152fd5b9290604051927f23b872dd00000000000000000000000000000000000000000000000000000000602085015273ffffffffffffffffffffffffffffffffffffffff809216602485015216604483015260648201526064815260a081019181831067ffffffffffffffff841117610b355761018292604052611841565b35610e9481610157565b3563ffffffff811681036101535790565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe093818652868601375f8582860101520116010190565b98959094610e949d9b97926020956113f19a959c976101809d8d73ffffffffffffffffffffffffffffffffffffffff998a8096818096168452169101521660408d01521660608b015260808a015260a089015260c08801521660e086015263ffffffff8092166101008601521661012084015261014083019063ffffffff169052565b816101608201520191611330565b9073ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001692611444838583611548565b61145060408601611315565b9061145d60608701611315565b9361146787611315565b9661147460a08201611315565b61148060c0830161131f565b61148c60e0840161131f565b9161149a610100850161131f565b936114a9610120820182610d4e565b9790968c3b15610153576040519d8e809d819d7f7b939232000000000000000000000000000000000000000000000000000000008352608087013596602001359516916004019c6114f99d61136e565b03815a5f948591f180156107895761150e5750565b8061077d61018292610b21565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b6040517fdd62ed3e00000000000000000000000000000000000000000000000000000000815230600482015273ffffffffffffffffffffffffffffffffffffffff8316602482015291926020838060448101038173ffffffffffffffffffffffffffffffffffffffff86165afa928315610789575f9361163a575b508201809211611635576040517f095ea7b300000000000000000000000000000000000000000000000000000000602082015273ffffffffffffffffffffffffffffffffffffffff9390931660248401526044808401929092529082526101829190611630606483610b3a565b611841565b61151b565b61165491935060203d6020116109cd576109bf8183610b3a565b915f6115c3565b9190820391821161163557565b6040517f70a082310000000000000000000000000000000000000000000000000000000080825230600483015273ffffffffffffffffffffffffffffffffffffffff989697959695602095878b16959294939087826024818a5afa8015610789576116da925f91611824575b5061165b565b9788106117fa576040519384523060048501528916928581602481875afa8015610789578392611710925f926117db575061165b565b036117b157610182977f646284e396b68ff4b4f34e0aa97bcdb9c100f5b44a20da5c475f627039853841916117a96117478a611315565b918960405194859416988c0135917f0000000000000000000000000000000000000000000000000000000000000000859094939260609273ffffffffffffffffffffffffffffffffffffffff6080840197168352602083015260408201520152565b0390a46113ff565b60046040517fd6cf42f0000000000000000000000000000000000000000000000000000000008152fd5b6117f3919250883d8a116109cd576109bf8183610b3a565b905f6116d4565b60046040517f0492ff87000000000000000000000000000000000000000000000000000000008152fd5b61183b9150893d8b116109cd576109bf8183610b3a565b5f6116d4565b73ffffffffffffffffffffffffffffffffffffffff166040516040810181811067ffffffffffffffff821117610b35576118bc937f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c656460205f948594604052818152015260208151910182855af16118b6610e06565b916118f5565b80519081159182156118d2575b50501561015357565b819250906020918101031261015357602001518015158103610153575f806118c9565b901561190f57815115611906575090565b3b156101535790565b50805190811561015357602001fdfea264697066735822122066b7197a155308805fbedee5c26980493cce360e5c829be686db3534152c74bf64736f6c63430008170033"; - -type UniversalSwapAndBridgeConstructorParams = - | [signer?: Signer] - | ConstructorParameters; - -const isSuperArgs = ( - xs: UniversalSwapAndBridgeConstructorParams -): xs is ConstructorParameters => xs.length > 1; - -export class UniversalSwapAndBridge__factory extends ContractFactory { - constructor(...args: UniversalSwapAndBridgeConstructorParams) { - if (isSuperArgs(args)) { - super(...args); - } else { - super(_abi, _bytecode, args[0]); - } - } - - override deploy( - _spokePool: string, - _wrappedNativeToken: string, - _exchange: string, - _allowedSelectors: BytesLike[], - overrides?: Overrides & { from?: string } - ): Promise { - return super.deploy( - _spokePool, - _wrappedNativeToken, - _exchange, - _allowedSelectors, - overrides || {} - ) as Promise; - } - override getDeployTransaction( - _spokePool: string, - _wrappedNativeToken: string, - _exchange: string, - _allowedSelectors: BytesLike[], - overrides?: Overrides & { from?: string } - ): TransactionRequest { - return super.getDeployTransaction( - _spokePool, - _wrappedNativeToken, - _exchange, - _allowedSelectors, - overrides || {} - ); - } - override attach(address: string): UniversalSwapAndBridge { - return super.attach(address) as UniversalSwapAndBridge; - } - override connect(signer: Signer): UniversalSwapAndBridge__factory { - return super.connect(signer) as UniversalSwapAndBridge__factory; - } - - static readonly bytecode = _bytecode; - static readonly abi = _abi; - static createInterface(): UniversalSwapAndBridgeInterface { - return new utils.Interface(_abi) as UniversalSwapAndBridgeInterface; - } - static connect( - address: string, - signerOrProvider: Signer | Provider - ): UniversalSwapAndBridge { - return new Contract( - address, - _abi, - signerOrProvider - ) as UniversalSwapAndBridge; - } -} diff --git a/api/_typechain/factories/SwapAndBridge.sol/index.ts b/api/_typechain/factories/SwapAndBridge.sol/index.ts deleted file mode 100644 index 74ba6aa0d..000000000 --- a/api/_typechain/factories/SwapAndBridge.sol/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* Autogenerated file. Do not edit manually. */ -/* tslint:disable */ - -export { SwapAndBridge__factory } from "./SwapAndBridge__factory"; -export { SwapAndBridgeBase__factory } from "./SwapAndBridgeBase__factory"; -export { UniversalSwapAndBridge__factory } from "./UniversalSwapAndBridge__factory"; diff --git a/api/swap-quote.ts b/api/swap-quote.ts index 56c54d472..073340abb 100644 --- a/api/swap-quote.ts +++ b/api/swap-quote.ts @@ -13,7 +13,7 @@ import { validateChainAndTokenParams, isSwapRouteEnabled, } from "./_utils"; -import { getUniswapQuoteForOriginSwapExactInput } from "./_dexes/uniswap"; +import { getUniswapQuoteWithSwapQuoter } from "./_dexes/uniswap/swap-quoter"; import { get1inchQuoteForOriginSwapExactInput } from "./_dexes/1inch"; import { InvalidParamError } from "./_errors"; import { AMOUNT_TYPE } from "./_dexes/cross-swap"; @@ -110,7 +110,7 @@ const handler = async ( } as const; const quoteResults = await Promise.allSettled([ - getUniswapQuoteForOriginSwapExactInput(swap), + getUniswapQuoteWithSwapQuoter(swap), get1inchQuoteForOriginSwapExactInput(swap), ]); diff --git a/scripts/generate-routes.ts b/scripts/generate-routes.ts index 86276288b..4bc4790d2 100644 --- a/scripts/generate-routes.ts +++ b/scripts/generate-routes.ts @@ -93,11 +93,18 @@ const enabledRoutes = { }, swapAndBridgeAddresses: { "1inch": { - [CHAIN_IDs.POLYGON]: "0xF9735e425A36d22636EF4cb75c7a6c63378290CA", - [CHAIN_IDs.OPTIMISM]: "0x7631eA29479Ee265241F13FB48555A2C886d3Bf8", - [CHAIN_IDs.ARBITRUM]: "0x81C7601ac0c5825e89F967f9222B977CCD78aD77", - [CHAIN_IDs.BASE]: "0x98285D11B9F7aFec2d475805E5255f26B4490167", + [CHAIN_IDs.POLYGON]: "0xaBa0F11D55C5dDC52cD0Cb2cd052B621d45159d5", + [CHAIN_IDs.OPTIMISM]: "0x3E7448657409278C9d6E192b92F2b69B234FCc42", + [CHAIN_IDs.ARBITRUM]: "0xC456398D5eE3B93828252e48beDEDbc39e03368E", + [CHAIN_IDs.BASE]: "0x7CFaBF2eA327009B39f40078011B0Fb714b65926", }, + uniswap: { + [CHAIN_IDs.POLYGON]: "0x9220Fa27ae680E4e8D9733932128FA73362E0393", + [CHAIN_IDs.OPTIMISM]: "0x6f4A733c7889f038D77D4f540182Dda17423CcbF", + [CHAIN_IDs.ARBITRUM]: "0xF633b72A4C2Fb73b77A379bf72864A825aD35b6D", + }, + }, + spokePoolPeripheryAddresses: { uniswap: { [CHAIN_IDs.POLYGON]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", [CHAIN_IDs.OPTIMISM]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", @@ -137,6 +144,9 @@ const enabledRoutes = { "0x17496824Ba574A4e9De80110A91207c4c63e552a", // Mocked }, }, + spokePoolPeripheryAddresses: { + uniswap: {}, + }, routes: transformChainConfigs(enabledSepoliaChainConfigs), }, } as const; @@ -374,6 +384,21 @@ async function generateRoutes(hubPoolChainId = 1) { }), {} ), + spokePoolPeripheryAddresses: Object.entries( + config.spokePoolPeripheryAddresses + ).reduce( + (acc, [key, value]) => ({ + ...acc, + [key]: Object.entries(value).reduce( + (acc, [chainId, address]) => ({ + ...acc, + [chainId]: utils.getAddress(address as string), + }), + {} + ), + }), + {} + ), routes: config.routes.flatMap((route) => transformBridgeRoute(route, config.hubPoolChain) ), diff --git a/src/data/routes_11155111_0x14224e63716afAcE30C9a417E0542281869f7d9e.json b/src/data/routes_11155111_0x14224e63716afAcE30C9a417E0542281869f7d9e.json index 2bf848111..5d7264fa4 100644 --- a/src/data/routes_11155111_0x14224e63716afAcE30C9a417E0542281869f7d9e.json +++ b/src/data/routes_11155111_0x14224e63716afAcE30C9a417E0542281869f7d9e.json @@ -13,6 +13,9 @@ "11155420": "0x17496824Ba574A4e9De80110A91207c4c63e552a" } }, + "spokePoolPeripheryAddresses": { + "uniswap": {} + }, "routes": [ { "fromChain": 11155111, diff --git a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json index ad0fbfe36..ffd9eafe3 100644 --- a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json +++ b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json @@ -9,11 +9,18 @@ "claimAndStakeAddress": "0x985e8A89Dd6Af8896Ef075c8dd93512433dc5829", "swapAndBridgeAddresses": { "1inch": { - "10": "0x7631eA29479Ee265241F13FB48555A2C886d3Bf8", - "137": "0xF9735e425A36d22636EF4cb75c7a6c63378290CA", - "8453": "0x98285D11B9F7aFec2d475805E5255f26B4490167", - "42161": "0x81C7601ac0c5825e89F967f9222B977CCD78aD77" + "10": "0x3E7448657409278C9d6E192b92F2b69B234FCc42", + "137": "0xaBa0F11D55C5dDC52cD0Cb2cd052B621d45159d5", + "8453": "0x7CFaBF2eA327009B39f40078011B0Fb714b65926", + "42161": "0xC456398D5eE3B93828252e48beDEDbc39e03368E" }, + "uniswap": { + "10": "0x6f4A733c7889f038D77D4f540182Dda17423CcbF", + "137": "0x9220Fa27ae680E4e8D9733932128FA73362E0393", + "42161": "0xF633b72A4C2Fb73b77A379bf72864A825aD35b6D" + } + }, + "spokePoolPeripheryAddresses": { "uniswap": { "1": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", "10": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", diff --git a/src/utils/bridge.ts b/src/utils/bridge.ts index a79114e6a..136c71af9 100644 --- a/src/utils/bridge.ts +++ b/src/utils/bridge.ts @@ -338,8 +338,6 @@ export async function sendSwapAndBridgeTx( fillDeadline ??= await getFillDeadline(spokePool); const tx = await swapAndBridge.populateTransaction.swapAndBridge( - swapTokenAddress, - inputTokenAddress, swapQuote.routerCalldata, swapTokenAmount, swapQuote.minExpectedInputTokenAmount, diff --git a/src/utils/config.ts b/src/utils/config.ts index 1952f1c0a..ba1b0449c 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -18,8 +18,8 @@ import { AcceleratingDistributor__factory, ClaimAndStake, ClaimAndStake__factory, - UniversalSwapAndBridge, - UniversalSwapAndBridge__factory, + SwapAndBridge, + SwapAndBridge__factory, } from "utils/typechain"; import { SupportedDex } from "./serverless-api/prod/swap-quote"; @@ -158,7 +158,7 @@ export class ConfigClient { chainId: constants.ChainId, dexKey: SupportedDex, signer?: Signer - ): UniversalSwapAndBridge | undefined { + ): SwapAndBridge | undefined { const address = this.getSwapAndBridgeAddress(chainId, dexKey); if (!address) { @@ -166,7 +166,7 @@ export class ConfigClient { } const provider = signer ?? providerUtils.getProvider(chainId); - return UniversalSwapAndBridge__factory.connect(address, provider); + return SwapAndBridge__factory.connect(address, provider); } getHubPoolChainId(): constants.ChainId { return this.config.hubPoolChain; From 7167488843481542b485ab382e355844aae91893 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Tue, 26 Nov 2024 14:26:02 +0700 Subject: [PATCH 24/47] fix: origin swap recipient address --- api/_dexes/uniswap/swap-router-02.ts | 73 ++++++++++++---------------- 1 file changed, 31 insertions(+), 42 deletions(-) diff --git a/api/_dexes/uniswap/swap-router-02.ts b/api/_dexes/uniswap/swap-router-02.ts index 580c672b7..ce7080840 100644 --- a/api/_dexes/uniswap/swap-router-02.ts +++ b/api/_dexes/uniswap/swap-router-02.ts @@ -15,6 +15,7 @@ import { getBridgeQuoteForMinOutput, getRoutesByChainIds, getRouteByOutputTokenAndOriginChain, + getLogger, } from "../../_utils"; import { buildMulticallHandlerMessage, @@ -29,10 +30,10 @@ import { buildExactOutputBridgeTokenMessage, buildMinOutputBridgeTokenMessage, getFallbackRecipient, - getSwapAndBridgeAddress, NoSwapRouteError, } from "../utils"; import { AMOUNT_TYPE } from "../cross-swap"; +import { getSpokePoolPeripheryAddress } from "../../_spoke-pool-periphery"; // Taken from here: https://docs.uniswap.org/contracts/v3/reference/deployments/ export const SWAP_ROUTER_02_ADDRESS = { @@ -109,10 +110,7 @@ export async function getUniswapCrossSwapQuotesForOutputB2A( destinationSwapQuote = await getUniswapQuoteWithSwapRouter02( { ...destinationSwap, - amount: addSlippageToAmount( - destinationSwapQuote.maximumAmountIn, - crossSwap.slippageTolerance.toString() - ), + amount: addBufferToAmount(destinationSwapQuote.maximumAmountIn), }, TradeType.EXACT_INPUT ); @@ -201,7 +199,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( chainId: originSwapChainId, tokenIn: crossSwap.inputToken, tokenOut: bridgeableInputToken, - recipient: getSwapAndBridgeAddress("uniswap", originSwapChainId), + recipient: getSpokePoolPeripheryAddress("uniswap", originSwapChainId), slippageTolerance: crossSwap.slippageTolerance, }; // 2.1. Get origin swap quote for any input token -> bridgeable input token @@ -217,29 +215,14 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( let adjOriginSwapQuote = await getUniswapQuoteWithSwapRouter02( { ...originSwap, - amount: originSwapQuote.maximumAmountIn.toString(), + amount: addBufferToAmount(originSwapQuote.maximumAmountIn), }, TradeType.EXACT_INPUT ); - - if (adjOriginSwapQuote.minAmountOut.lt(bridgeQuote.inputAmount)) { - adjOriginSwapQuote = await getUniswapQuoteWithSwapRouter02( - { - ...originSwap, - amount: addSlippageToAmount( - adjOriginSwapQuote.maximumAmountIn, - crossSwap.slippageTolerance.toString() - ), - }, - TradeType.EXACT_INPUT - ); - if (adjOriginSwapQuote.minAmountOut.lt(bridgeQuote.inputAmount)) { - throw new Error( - `Origin swap quote min. output amount ${adjOriginSwapQuote.minAmountOut.toString()} ` + - `is less than required bridge input amount ${bridgeQuote.inputAmount.toString()}` - ); - } - } + assertMinOutputAmount( + adjOriginSwapQuote.minAmountOut, + bridgeQuote.inputAmount + ); return { crossSwap, @@ -362,7 +345,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( chainId: originSwapChainId, tokenIn: crossSwap.inputToken, tokenOut: bridgeableInputToken, - recipient: getSwapAndBridgeAddress("uniswap", originSwapChainId), + recipient: getSpokePoolPeripheryAddress("uniswap", originSwapChainId), slippageTolerance: crossSwap.slippageTolerance, }; const destinationSwap = { @@ -388,10 +371,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( destinationSwapQuote = await getUniswapQuoteWithSwapRouter02( { ...destinationSwap, - amount: addSlippageToAmount( - destinationSwapQuote.maximumAmountIn, - crossSwap.slippageTolerance.toString() - ), + amount: addBufferToAmount(destinationSwapQuote.maximumAmountIn), }, TradeType.EXACT_INPUT ); @@ -424,10 +404,14 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( const adjOriginSwapQuote = await getUniswapQuoteWithSwapRouter02( { ...originSwap, - amount: originSwapQuote.maximumAmountIn.toString(), + amount: addBufferToAmount(originSwapQuote.maximumAmountIn), }, TradeType.EXACT_INPUT ); + assertMinOutputAmount( + adjOriginSwapQuote.minAmountOut, + bridgeQuote.inputAmount + ); return { crossSwap, @@ -504,7 +488,9 @@ export async function getUniswapQuoteWithSwapRouter02( }, }; - console.log("swapQuote", { + getLogger().debug({ + at: "uniswap/getUniswapQuoteWithSwapRouter02", + message: "Swap quote", type: tradeType === TradeType.EXACT_INPUT ? "EXACT_INPUT" : "EXACT_OUTPUT", tokenIn: swapQuote.tokenIn.symbol, tokenOut: swapQuote.tokenOut.symbol, @@ -523,11 +509,7 @@ function getSwapRouter02AndOptions(params: { recipient: string; slippageTolerance: number; }) { - const provider = getProvider(params.chainId); - const router = new AlphaRouter({ - chainId: params.chainId, - provider, - }); + const router = getSwapRouter02(params.chainId); const options: SwapOptionsSwapRouter02 = { recipient: params.recipient, deadline: utils.getCurrentTime() + 30 * 60, // 30 minutes from now @@ -540,6 +522,15 @@ function getSwapRouter02AndOptions(params: { }; } +const swapRouterCache = new Map(); +function getSwapRouter02(chainId: number) { + const provider = getProvider(chainId); + if (!swapRouterCache.has(chainId)) { + swapRouterCache.set(chainId, new AlphaRouter({ chainId, provider })); + } + return swapRouterCache.get(chainId)!; +} + function floatToPercent(value: number) { return new Percent( // max. slippage decimals is 2 @@ -661,11 +652,9 @@ function assertMinOutputAmount( } } -function addSlippageToAmount(amount: BigNumber, slippageTolerance: string) { +function addBufferToAmount(amount: BigNumber, buffer = 0.01) { return amount - .mul( - ethers.utils.parseEther((1 + Number(slippageTolerance) / 100).toString()) - ) + .mul(ethers.utils.parseEther((1 + Number(buffer) / 100).toString())) .div(utils.fixedPointAdjustment) .toString(); } From c02281890fa0f7f6bfaf9d7fb1991da9596bc843 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Tue, 26 Nov 2024 14:39:50 +0700 Subject: [PATCH 25/47] fix: adjust buffer --- api/_dexes/uniswap/swap-router-02.ts | 2 +- scripts/tests/swap-allowance.ts | 32 +++++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/api/_dexes/uniswap/swap-router-02.ts b/api/_dexes/uniswap/swap-router-02.ts index ce7080840..0421dd6eb 100644 --- a/api/_dexes/uniswap/swap-router-02.ts +++ b/api/_dexes/uniswap/swap-router-02.ts @@ -654,7 +654,7 @@ function assertMinOutputAmount( function addBufferToAmount(amount: BigNumber, buffer = 0.01) { return amount - .mul(ethers.utils.parseEther((1 + Number(buffer) / 100).toString())) + .mul(ethers.utils.parseEther((1 + Number(buffer)).toString())) .div(utils.fixedPointAdjustment) .toString(); } diff --git a/scripts/tests/swap-allowance.ts b/scripts/tests/swap-allowance.ts index c3f022ab2..7f0b539bc 100644 --- a/scripts/tests/swap-allowance.ts +++ b/scripts/tests/swap-allowance.ts @@ -70,7 +70,7 @@ const MIN_OUTPUT_CASES = [ { labels: ["B2A", "MIN_OUTPUT", "USDC - APE"], params: { - amount: ethers.utils.parseUnits("1", 18).toString(), + amount: ethers.utils.parseUnits("3", 18).toString(), tradeType: "minOutput", inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[originChainId], originChainId, @@ -106,6 +106,30 @@ const MIN_OUTPUT_CASES = [ depositor, }, }, + { + labels: ["A2B", "MIN_OUTPUT", "USDC - WETH"], + params: { + amount: ethers.utils.parseUnits("0.01", 18).toString(), + tradeType: "minOutput", + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[originChainId], + originChainId, + outputToken: TOKEN_SYMBOLS_MAP.WETH.addresses[destinationChainId], + destinationChainId, + depositor, + }, + }, + { + labels: ["A2B", "MIN_OUTPUT", "WETH - USDC"], + params: { + amount: ethers.utils.parseUnits("1", 6).toString(), + tradeType: "minOutput", + inputToken: TOKEN_SYMBOLS_MAP.WETH.addresses[originChainId], + originChainId, + outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[destinationChainId], + destinationChainId, + depositor, + }, + }, // A2A { labels: ["A2A", "MIN_OUTPUT", "bridged USDC - APE"], @@ -115,7 +139,7 @@ const MIN_OUTPUT_CASES = [ inputToken: TOKEN_SYMBOLS_MAP["USDC.e"].addresses[originChainId] || TOKEN_SYMBOLS_MAP.USDbC.addresses[originChainId], - originChainId: CHAIN_IDs.BASE, + originChainId, outputToken: anyDestinationOutputTokens[destinationChainId], // APE Coin destinationChainId, depositor, @@ -144,7 +168,9 @@ async function swap() { }); for (const testCase of filteredTestCases) { console.log("\nTest case:", testCase.labels.join(" ")); + console.log("Params:", testCase.params); const response = await axios.get( + // `https://preview.across.to/api/swap/allowance`, `http://localhost:3000/api/swap/allowance`, { params: testCase.params, @@ -152,7 +178,7 @@ async function swap() { ); console.log(response.data); - if (process.env.DEV_WALLET_PK) { + if (!process.env.DEV_WALLET_PK) { const wallet = new Wallet(process.env.DEV_WALLET_PK!).connect( getProvider(testCase.params.originChainId) ); From e5a03539c514b90e884949c6f9a0354b5e51b3b1 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp <51655063+gsteenkamp89@users.noreply.github.com> Date: Wed, 27 Nov 2024 11:27:46 +0200 Subject: [PATCH 26/47] Gerhard/acx 3321 calculate fee components (#1285) Co-authored-by: Dong-Ha Kim --- api/_dexes/types.ts | 11 +++++ api/_utils.ts | 10 ++++- api/coingecko.ts | 2 +- api/swap/_utils.ts | 99 ++++++++++++++++++++++++++++++++++++++++++- api/swap/allowance.ts | 1 + 5 files changed, 118 insertions(+), 5 deletions(-) diff --git a/api/_dexes/types.ts b/api/_dexes/types.ts index d8205279a..03ae61669 100644 --- a/api/_dexes/types.ts +++ b/api/_dexes/types.ts @@ -78,3 +78,14 @@ export type CrossSwapQuotes = { destinationSwapQuote?: SwapQuote; originSwapQuote?: SwapQuote; }; + +export type CrossSwapQuotesWithFees = CrossSwapQuotes & { + fees: CrossSwapFees; +}; + +// { currency => amount } +export type CrossSwapFees = { + bridgeFees: Record; + originSwapFees?: Record; + destinationSwapFees?: Record; +}; diff --git a/api/_utils.ts b/api/_utils.ts index 6e4038fdd..076f39a0c 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -775,12 +775,18 @@ export const buildDepositForSimulation = (depositArgs: { export const getCachedTokenPrice = async ( l1Token: string, baseCurrency: string = "eth", - historicalDateISO?: string + historicalDateISO?: string, + chainId?: number ): Promise => { return Number( ( await axios(`${resolveVercelEndpoint()}/api/coingecko`, { - params: { l1Token, baseCurrency, date: historicalDateISO }, + params: { + l1Token, + chainId, + baseCurrency, + date: historicalDateISO, + }, }) ).data.price ); diff --git a/api/coingecko.ts b/api/coingecko.ts index 42f8a3512..4e4539189 100644 --- a/api/coingecko.ts +++ b/api/coingecko.ts @@ -144,7 +144,7 @@ const handler = async ( address, modifiedBaseCurrency ) - : await coingeckoClient.getCurrentPriceByContract( + : await coingeckoClient.getCurrentPriceById( address, modifiedBaseCurrency, chainId diff --git a/api/swap/_utils.ts b/api/swap/_utils.ts index 36476a031..c1f57e13d 100644 --- a/api/swap/_utils.ts +++ b/api/swap/_utils.ts @@ -9,6 +9,7 @@ import { boolStr, getCachedTokenInfo, getWrappedNativeTokenAddress, + getCachedTokenPrice, } from "../_utils"; import { AMOUNT_TYPE, @@ -17,7 +18,13 @@ import { } from "../_dexes/cross-swap"; import { InvalidParamError } from "../_errors"; import { isValidIntegratorId } from "../_integrator-id"; -import { Token } from "../_dexes/types"; +import { + CrossSwapFees, + CrossSwapQuotes, + SwapQuote, + Token, +} from "../_dexes/types"; +import { formatUnits } from "ethers/lib/utils"; import { encodeApproveCalldata } from "../_multicall-handler"; export const BaseSwapQueryParamsSchema = type({ @@ -123,8 +130,14 @@ export async function handleBaseSwapQueryParams({ isOutputNative, }); + // 3. Calculate fees based for full route + const fees = await calculateCrossSwapFees(crossSwapQuotes); + return { - crossSwapQuotes, + crossSwapQuotes: { + ...crossSwapQuotes, + fees, + }, integratorId, skipOriginTxEstimation, isInputNative, @@ -158,3 +171,85 @@ export function getApprovalTxns(params: { }); return approvalTxns; } + +async function calculateSwapFee( + swapQuote: SwapQuote, + baseCurrency: string +): Promise> { + const { tokenIn, tokenOut, expectedAmountOut, expectedAmountIn } = swapQuote; + const [inputTokenPriceBase, outputTokenPriceBase] = await Promise.all([ + getCachedTokenPrice( + tokenIn.address, + baseCurrency, + undefined, + tokenIn.chainId + ), + getCachedTokenPrice( + tokenOut.address, + baseCurrency, + undefined, + tokenOut.chainId + ), + ]); + + const normalizedIn = + parseFloat(formatUnits(expectedAmountIn, tokenIn.decimals)) * + inputTokenPriceBase; + const normalizedOut = + parseFloat(formatUnits(expectedAmountOut, tokenOut.decimals)) * + outputTokenPriceBase; + return { + [baseCurrency]: normalizedIn - normalizedOut, + }; +} + +async function calculateBridgeFee( + bridgeQuote: CrossSwapQuotes["bridgeQuote"], + baseCurrency: string +): Promise> { + const { inputToken, suggestedFees } = bridgeQuote; + const inputTokenPriceBase = await getCachedTokenPrice( + inputToken.address, + baseCurrency, + undefined, + inputToken.chainId + ); + const normalizedFee = + parseFloat( + formatUnits(suggestedFees.totalRelayFee.total, inputToken.decimals) + ) * inputTokenPriceBase; + + return { + [baseCurrency]: normalizedFee, + }; +} + +export async function calculateCrossSwapFees( + crossSwapQuote: CrossSwapQuotes, + baseCurrency = "usd" +): Promise { + const bridgeFeePromise = calculateBridgeFee( + crossSwapQuote.bridgeQuote, + baseCurrency + ); + + const originSwapFeePromise = crossSwapQuote?.originSwapQuote + ? calculateSwapFee(crossSwapQuote.originSwapQuote, baseCurrency) + : Promise.resolve(undefined); + + const destinationSwapFeePromise = crossSwapQuote?.destinationSwapQuote + ? calculateSwapFee(crossSwapQuote.destinationSwapQuote, baseCurrency) + : Promise.resolve(undefined); + + const [bridgeFees, originSwapFees, destinationSwapFees] = await Promise.all([ + bridgeFeePromise, + originSwapFeePromise, + destinationSwapFeePromise, + ]); + + return { + bridgeFees, + originSwapFees, + destinationSwapFees, + }; +} diff --git a/api/swap/allowance.ts b/api/swap/allowance.ts index 3978777c3..2e593a741 100644 --- a/api/swap/allowance.ts +++ b/api/swap/allowance.ts @@ -96,6 +96,7 @@ const handler = async ( : crossSwapQuotes.bridgeQuote.outputToken; const responseJson = { + fees: crossSwapQuotes.fees, checks: { allowance: { token: inputTokenAddress, From 129aadd5f8c45e77bf7eedc6b28d64c5bc4bdb96 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Fri, 29 Nov 2024 11:46:31 +0100 Subject: [PATCH 27/47] feat: use trading api + support universal router (#1294) --- api/_dexes/cross-swap.ts | 55 +- api/_dexes/types.ts | 4 +- api/_dexes/uniswap/adapter.ts | 252 ++++++ api/_dexes/uniswap/quote-resolver.ts | 539 +++++++++++++ api/_dexes/uniswap/swap-router-02.ts | 752 +++--------------- api/_dexes/uniswap/trading-api.ts | 139 ++++ api/_dexes/uniswap/universal-router.ts | 159 ++++ api/_dexes/uniswap/utils.ts | 92 ++- api/_errors.ts | 3 +- api/_spoke-pool-periphery.ts | 4 +- api/_utils.ts | 2 +- api/swap/_utils.ts | 4 +- api/swap/allowance.ts | 7 +- scripts/generate-routes.ts | 6 +- scripts/tests/swap-allowance.ts | 5 +- ...6fA914353c44b2E33eBE05f21846F1048bEda.json | 6 +- 16 files changed, 1373 insertions(+), 656 deletions(-) create mode 100644 api/_dexes/uniswap/adapter.ts create mode 100644 api/_dexes/uniswap/quote-resolver.ts create mode 100644 api/_dexes/uniswap/trading-api.ts create mode 100644 api/_dexes/uniswap/universal-router.ts diff --git a/api/_dexes/cross-swap.ts b/api/_dexes/cross-swap.ts index ede9bcfcb..2a4a28b7c 100644 --- a/api/_dexes/cross-swap.ts +++ b/api/_dexes/cross-swap.ts @@ -10,10 +10,12 @@ import { getSpokePool, } from "../_utils"; import { - getUniswapCrossSwapQuotesForOutputB2A, - getUniswapCrossSwapQuotesForOutputA2B, getBestUniswapCrossSwapQuotesForOutputA2A, -} from "./uniswap/swap-router-02"; + getUniswapCrossSwapQuotesForOutputA2B, + getUniswapCrossSwapQuotesForOutputB2A, +} from "./uniswap/quote-resolver"; +import { getSwapRouter02Strategy } from "./uniswap/swap-router-02"; +import { UniswapQuoteFetchStrategy } from "./uniswap/utils"; import { CrossSwap, CrossSwapQuotes } from "./types"; import { buildExactOutputBridgeTokenMessage, @@ -22,6 +24,7 @@ import { import { getSpokePoolPeriphery } from "../_spoke-pool-periphery"; import { tagIntegratorId } from "../_integrator-id"; import { getMultiCallHandlerAddress } from "../_multicall-handler"; +import { CHAIN_IDs } from "../_constants"; export type CrossSwapType = (typeof CROSS_SWAP_TYPE)[keyof typeof CROSS_SWAP_TYPE]; @@ -48,7 +51,13 @@ export const LEFTOVER_TYPE = { BRIDGEABLE_TOKEN: "bridgeableToken", } as const; -export const PREFERRED_BRIDGE_TOKENS = ["WETH", "USDC"]; +export const PREFERRED_BRIDGE_TOKENS = ["WETH"]; + +const defaultQuoteFetchStrategy: UniswapQuoteFetchStrategy = + getSwapRouter02Strategy(); +const strategyOverrides = { + [CHAIN_IDs.BLAST]: defaultQuoteFetchStrategy, +}; export async function getCrossSwapQuotes( crossSwap: CrossSwap @@ -128,21 +137,35 @@ export async function getCrossSwapQuotesForOutputB2B(crossSwap: CrossSwap) { } export async function getCrossSwapQuotesForOutputB2A(crossSwap: CrossSwap) { - // @TODO: Add support for other DEXes / aggregators - return getUniswapCrossSwapQuotesForOutputB2A(crossSwap); + return getUniswapCrossSwapQuotesForOutputB2A( + crossSwap, + // Destination swap requires destination chain's quote fetch strategy + getQuoteFetchStrategy(crossSwap.outputToken.chainId) + ); } export async function getCrossSwapQuotesForOutputA2B(crossSwap: CrossSwap) { - // @TODO: Add support for other DEXes / aggregators - return getUniswapCrossSwapQuotesForOutputA2B(crossSwap); + return getUniswapCrossSwapQuotesForOutputA2B( + crossSwap, + // Origin swap requires origin chain's quote fetch strategy + getQuoteFetchStrategy(crossSwap.inputToken.chainId) + ); } export async function getCrossSwapQuotesForOutputA2A(crossSwap: CrossSwap) { - // @TODO: Add support for other DEXes / aggregators - return getBestUniswapCrossSwapQuotesForOutputA2A(crossSwap, { - preferredBridgeTokens: PREFERRED_BRIDGE_TOKENS, - bridgeRoutesLimit: 2, - }); + return getBestUniswapCrossSwapQuotesForOutputA2A( + crossSwap, + getQuoteFetchStrategy(crossSwap.inputToken.chainId), + getQuoteFetchStrategy(crossSwap.outputToken.chainId), + { + preferredBridgeTokens: PREFERRED_BRIDGE_TOKENS, + bridgeRoutesLimit: 1, + } + ); +} + +function getQuoteFetchStrategy(chainId: number) { + return strategyOverrides[chainId] ?? defaultQuoteFetchStrategy; } export function getCrossSwapType(params: { @@ -179,13 +202,17 @@ export async function buildCrossSwapTxForAllowanceHolder( ) { const originChainId = crossSwapQuotes.crossSwap.inputToken.chainId; const spokePool = getSpokePool(originChainId); - const spokePoolPeriphery = getSpokePoolPeriphery("uniswap", originChainId); + const deposit = await extractDepositDataStruct(crossSwapQuotes); let tx: PopulatedTransaction; let toAddress: string; if (crossSwapQuotes.originSwapQuote) { + const spokePoolPeriphery = getSpokePoolPeriphery( + crossSwapQuotes.originSwapQuote.peripheryAddress, + originChainId + ); tx = await spokePoolPeriphery.populateTransaction.swapAndBridge( crossSwapQuotes.originSwapQuote.tokenIn.address, crossSwapQuotes.originSwapQuote.tokenOut.address, diff --git a/api/_dexes/types.ts b/api/_dexes/types.ts index 03ae61669..8eab02695 100644 --- a/api/_dexes/types.ts +++ b/api/_dexes/types.ts @@ -76,7 +76,9 @@ export type CrossSwapQuotes = { suggestedFees: Awaited>; }; destinationSwapQuote?: SwapQuote; - originSwapQuote?: SwapQuote; + originSwapQuote?: SwapQuote & { + peripheryAddress: string; + }; }; export type CrossSwapQuotesWithFees = CrossSwapQuotes & { diff --git a/api/_dexes/uniswap/adapter.ts b/api/_dexes/uniswap/adapter.ts new file mode 100644 index 000000000..3b686ad84 --- /dev/null +++ b/api/_dexes/uniswap/adapter.ts @@ -0,0 +1,252 @@ +import { MixedRouteSDK, Trade as RouterTrade } from "@uniswap/router-sdk"; +import { + Currency, + CurrencyAmount, + Ether, + Token, + TradeType, +} from "@uniswap/sdk-core"; +import { Pair, Route as V2Route } from "@uniswap/v2-sdk"; +import { Pool, Route as V3Route, FeeAmount } from "@uniswap/v3-sdk"; +import { BigNumber } from "ethers"; + +export type TokenInRoute = { + address: string; + chainId: number; + symbol: string; + decimals: string; + name?: string; + buyFeeBps?: string; + sellFeeBps?: string; +}; + +export enum PoolType { + V2Pool = "v2-pool", + V3Pool = "v3-pool", + V4Pool = "v4-pool", +} + +export type V2Reserve = { + token: TokenInRoute; + quotient: string; +}; + +export type V2PoolInRoute = { + type: PoolType.V2Pool; + address?: string; + tokenIn: TokenInRoute; + tokenOut: TokenInRoute; + reserve0: V2Reserve; + reserve1: V2Reserve; + amountIn?: string; + amountOut?: string; +}; + +export type V3PoolInRoute = { + type: PoolType.V3Pool; + address?: string; + tokenIn: TokenInRoute; + tokenOut: TokenInRoute; + sqrtRatioX96: string; + liquidity: string; + tickCurrent: string; + fee: string; + amountIn?: string; + amountOut?: string; +}; + +export type PartialClassicQuote = { + // We need tokenIn/Out to support native currency + tokenIn: string; + tokenOut: string; + tradeType: TradeType; + route: Array<(V3PoolInRoute | V2PoolInRoute)[]>; +}; + +interface RouteResult { + routev3: V3Route | null; + routev2: V2Route | null; + mixedRoute: MixedRouteSDK | null; + inputAmount: CurrencyAmount; + outputAmount: CurrencyAmount; +} + +// Helper class to convert routing-specific quote entities to RouterTrade entities +// the returned RouterTrade can then be used to build the UniswapTrade entity in this package +export class RouterTradeAdapter { + // Generate a RouterTrade using fields from a classic quote response + static fromClassicQuote(quote: PartialClassicQuote) { + const { route } = quote; + + if (!route) throw new Error("Expected route to be present"); + if (!route.length) + throw new Error("Expected there to be at least one route"); + if (route.some((r) => !r.length)) + throw new Error("Expected all routes to have at least one pool"); + const firstRoute = route[0]; + + const tokenInData = firstRoute[0].tokenIn; + const tokenOutData = firstRoute[firstRoute.length - 1].tokenOut; + + if (!tokenInData || !tokenOutData) + throw new Error("Expected both tokenIn and tokenOut to be present"); + if (tokenInData.chainId !== tokenOutData.chainId) + throw new Error("Expected tokenIn and tokenOut to be have same chainId"); + + const parsedCurrencyIn = RouterTradeAdapter.toCurrency(false, tokenInData); + const parsedCurrencyOut = RouterTradeAdapter.toCurrency( + false, + tokenOutData + ); + + const typedRoutes: RouteResult[] = route.map((subRoute) => { + const rawAmountIn = subRoute[0].amountIn; + const rawAmountOut = subRoute[subRoute.length - 1].amountOut; + + if (!rawAmountIn || !rawAmountOut) { + throw new Error( + "Expected both raw amountIn and raw amountOut to be present" + ); + } + + const inputAmount = CurrencyAmount.fromRawAmount( + parsedCurrencyIn, + rawAmountIn + ); + const outputAmount = CurrencyAmount.fromRawAmount( + parsedCurrencyOut, + rawAmountOut + ); + + const isOnlyV2 = RouterTradeAdapter.isVersionedRoute( + PoolType.V2Pool, + subRoute + ); + const isOnlyV3 = RouterTradeAdapter.isVersionedRoute( + PoolType.V3Pool, + subRoute + ); + + return { + routev3: isOnlyV3 + ? new V3Route( + (subRoute as V3PoolInRoute[]).map(RouterTradeAdapter.toPool), + parsedCurrencyIn, + parsedCurrencyOut + ) + : null, + routev2: isOnlyV2 + ? new V2Route( + (subRoute as V2PoolInRoute[]).map(RouterTradeAdapter.toPair), + parsedCurrencyIn, + parsedCurrencyOut + ) + : null, + mixedRoute: + !isOnlyV3 && !isOnlyV2 + ? new MixedRouteSDK( + subRoute.map(RouterTradeAdapter.toPoolOrPair), + parsedCurrencyIn, + parsedCurrencyOut + ) + : null, + inputAmount, + outputAmount, + }; + }); + + return new RouterTrade({ + v2Routes: typedRoutes + .filter((route) => route.routev2) + .map((route) => ({ + routev2: route.routev2 as V2Route, + inputAmount: route.inputAmount, + outputAmount: route.outputAmount, + })), + v3Routes: typedRoutes + .filter((route) => route.routev3) + .map((route) => ({ + routev3: route.routev3 as V3Route, + inputAmount: route.inputAmount, + outputAmount: route.outputAmount, + })), + // TODO: ROUTE-219 - Support v4 trade in universal-router sdk + v4Routes: [], + mixedRoutes: typedRoutes + .filter((route) => route.mixedRoute) + .map((route) => ({ + mixedRoute: route.mixedRoute as MixedRouteSDK, + inputAmount: route.inputAmount, + outputAmount: route.outputAmount, + })), + tradeType: quote.tradeType, + }); + } + + private static toCurrency(isNative: boolean, token: TokenInRoute): Currency { + if (isNative) { + return Ether.onChain(token.chainId); + } + return this.toToken(token); + } + + private static toPoolOrPair = ( + pool: V3PoolInRoute | V2PoolInRoute + ): Pool | Pair => { + return pool.type === PoolType.V3Pool + ? RouterTradeAdapter.toPool(pool) + : RouterTradeAdapter.toPair(pool); + }; + + private static toToken(token: TokenInRoute): Token { + const { chainId, address, decimals, symbol, buyFeeBps, sellFeeBps } = token; + return new Token( + chainId, + address, + parseInt(decimals.toString()), + symbol, + /* name */ undefined, + false, + buyFeeBps ? BigNumber.from(buyFeeBps) : undefined, + sellFeeBps ? BigNumber.from(sellFeeBps) : undefined + ); + } + + private static toPool({ + fee, + sqrtRatioX96, + liquidity, + tickCurrent, + tokenIn, + tokenOut, + }: V3PoolInRoute): Pool { + return new Pool( + RouterTradeAdapter.toToken(tokenIn), + RouterTradeAdapter.toToken(tokenOut), + parseInt(fee) as FeeAmount, + sqrtRatioX96, + liquidity, + parseInt(tickCurrent) + ); + } + + private static toPair = ({ reserve0, reserve1 }: V2PoolInRoute): Pair => { + return new Pair( + CurrencyAmount.fromRawAmount( + RouterTradeAdapter.toToken(reserve0.token), + reserve0.quotient + ), + CurrencyAmount.fromRawAmount( + RouterTradeAdapter.toToken(reserve1.token), + reserve1.quotient + ) + ); + }; + + private static isVersionedRoute( + type: PoolType, + route: (V3PoolInRoute | V2PoolInRoute)[] + ): route is T[] { + return route.every((pool) => pool.type === type); + } +} diff --git a/api/_dexes/uniswap/quote-resolver.ts b/api/_dexes/uniswap/quote-resolver.ts new file mode 100644 index 000000000..b4506573e --- /dev/null +++ b/api/_dexes/uniswap/quote-resolver.ts @@ -0,0 +1,539 @@ +import { BigNumber } from "ethers"; +import { TradeType } from "@uniswap/sdk-core"; + +import { + getRouteByInputTokenAndDestinationChain, + getTokenByAddress, + getBridgeQuoteForMinOutput, + getRoutesByChainIds, + getRouteByOutputTokenAndOriginChain, +} from "../../_utils"; +import { + buildMulticallHandlerMessage, + encodeApproveCalldata, + encodeDrainCalldata, + encodeTransferCalldata, + encodeWethWithdrawCalldata, + getMultiCallHandlerAddress, +} from "../../_multicall-handler"; +import { + Token as AcrossToken, + CrossSwap, + CrossSwapQuotes, + SwapQuote, +} from "../types"; +import { + buildExactOutputBridgeTokenMessage, + buildMinOutputBridgeTokenMessage, + getFallbackRecipient, +} from "../utils"; +import { AMOUNT_TYPE } from "../cross-swap"; +import { UniswapQuoteFetchStrategy, addMarkupToAmount } from "./utils"; + +const indicativeQuoteBuffer = 0.005; // 0.5% buffer for indicative quotes + +/** + * Returns Uniswap v3 quote for a swap with min. output amount for route + * BRIDGEABLE input token -> ANY output token, e.g. USDC -> ARB. Required steps: + * 1. Get destination swap quote for bridgeable output token -> any token + * 2. Get bridge quote for bridgeable input token -> bridgeable output token + */ +export async function getUniswapCrossSwapQuotesForOutputB2A( + crossSwap: CrossSwap, + strategy: UniswapQuoteFetchStrategy +): Promise { + const destinationSwapChainId = crossSwap.outputToken.chainId; + const bridgeRoute = getRouteByInputTokenAndDestinationChain( + crossSwap.inputToken.address, + destinationSwapChainId + ); + + if (!bridgeRoute) { + throw new Error( + `No bridge route found for input token ${crossSwap.inputToken.symbol} ` + + `${crossSwap.inputToken.chainId} -> ${destinationSwapChainId}` + ); + } + + const _bridgeableOutputToken = getTokenByAddress( + bridgeRoute.toTokenAddress, + bridgeRoute.toChain + ); + + if (!_bridgeableOutputToken) { + throw new Error( + `No bridgeable output token found for ${bridgeRoute.toTokenAddress} on chain ${bridgeRoute.toChain}` + ); + } + + const bridgeableOutputToken = { + address: bridgeRoute.toTokenAddress, + decimals: _bridgeableOutputToken.decimals, + symbol: _bridgeableOutputToken.symbol, + chainId: destinationSwapChainId, + }; + + const destinationSwap = { + chainId: destinationSwapChainId, + tokenIn: bridgeableOutputToken, + tokenOut: crossSwap.outputToken, + recipient: getMultiCallHandlerAddress(destinationSwapChainId), + slippageTolerance: crossSwap.slippageTolerance, + type: crossSwap.type, + }; + // 1. Get destination swap quote for bridgeable output token -> any token + // with exact output amount. + let destinationSwapQuote = await strategy.fetchFn( + { + ...destinationSwap, + amount: crossSwap.amount.toString(), + }, + TradeType.EXACT_OUTPUT + ); + + // 2. Get bridge quote for bridgeable input token -> bridgeable output token + const bridgeQuote = await getBridgeQuoteForMinOutput({ + inputToken: crossSwap.inputToken, + outputToken: bridgeableOutputToken, + minOutputAmount: destinationSwapQuote.maximumAmountIn, + recipient: getMultiCallHandlerAddress(destinationSwapChainId), + message: buildDestinationSwapCrossChainMessage({ + crossSwap, + destinationSwapQuote, + bridgeableOutputToken, + routerAddress: strategy.getRouterAddress(destinationSwapChainId), + }), + }); + + return { + crossSwap, + bridgeQuote, + destinationSwapQuote, + originSwapQuote: undefined, + }; +} + +/** + * Returns Uniswap v3 quote for a swap with min. output amount for route + * ANY input token -> BRIDGEABLE output token, e.g. ARB -> USDC. Required steps: + * 1. Get bridge quote for bridgeable input token -> bridgeable output token + * 2. Get origin swap quote for any input token -> bridgeable input token + */ +export async function getUniswapCrossSwapQuotesForOutputA2B( + crossSwap: CrossSwap, + strategy: UniswapQuoteFetchStrategy +): Promise { + const originSwapChainId = crossSwap.inputToken.chainId; + const destinationChainId = crossSwap.outputToken.chainId; + const bridgeRoute = getRouteByOutputTokenAndOriginChain( + crossSwap.outputToken.address, + originSwapChainId + ); + + if (!bridgeRoute) { + throw new Error( + `No bridge route found for output token ${crossSwap.outputToken.symbol} ` + + `${originSwapChainId} -> ${crossSwap.outputToken.chainId}` + ); + } + + const _bridgeableInputToken = getTokenByAddress( + bridgeRoute.fromTokenAddress, + bridgeRoute.fromChain + ); + + if (!_bridgeableInputToken) { + throw new Error( + `No bridgeable input token found for ${bridgeRoute.toTokenAddress} on chain ${bridgeRoute.toChain}` + ); + } + + const bridgeableInputToken = { + address: bridgeRoute.fromTokenAddress, + decimals: _bridgeableInputToken.decimals, + symbol: _bridgeableInputToken.symbol, + chainId: bridgeRoute.fromChain, + }; + + // 1. Get bridge quote for bridgeable input token -> bridgeable output token + const bridgeQuote = await getBridgeQuoteForMinOutput({ + inputToken: bridgeableInputToken, + outputToken: crossSwap.outputToken, + minOutputAmount: crossSwap.amount, + recipient: getMultiCallHandlerAddress(destinationChainId), + message: buildExactOutputBridgeTokenMessage(crossSwap), + }); + // 1.1. Update bridge quote message for min. output amount + if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT && crossSwap.isOutputNative) { + bridgeQuote.message = buildMinOutputBridgeTokenMessage( + crossSwap, + bridgeQuote.outputAmount + ); + } + + const spokePoolPeripheryAddress = + strategy.getPeripheryAddress(originSwapChainId); + const originSwap = { + chainId: originSwapChainId, + tokenIn: crossSwap.inputToken, + tokenOut: bridgeableInputToken, + recipient: spokePoolPeripheryAddress, + slippageTolerance: crossSwap.slippageTolerance, + type: crossSwap.type, + }; + // 2.1. Get origin swap quote for any input token -> bridgeable input token + const originSwapQuote = await strategy.fetchFn( + { + ...originSwap, + amount: bridgeQuote.inputAmount.toString(), + }, + TradeType.EXACT_OUTPUT, + { + useIndicativeQuote: true, + } + ); + // 2.2. Re-fetch origin swap quote with updated input amount and EXACT_INPUT type. + // This prevents leftover tokens in the SwapAndBridge contract. + let adjOriginSwapQuote = await strategy.fetchFn( + { + ...originSwap, + amount: addMarkupToAmount( + originSwapQuote.maximumAmountIn, + indicativeQuoteBuffer + ).toString(), + }, + TradeType.EXACT_INPUT + ); + assertMinOutputAmount( + adjOriginSwapQuote.minAmountOut, + bridgeQuote.inputAmount + ); + + return { + crossSwap, + bridgeQuote, + destinationSwapQuote: undefined, + originSwapQuote: { + ...adjOriginSwapQuote, + peripheryAddress: spokePoolPeripheryAddress, + }, + }; +} + +/** + * Returns Uniswap v3 quote for a swap with min. output amount for route + * ANY input token -> ANY output token, e.g. ARB -> OP. We compare quotes from + * different bridge routes and return the best one. In this iteration, we only + * consider a hardcoded list of high-liquid bridge routes. + * @param crossSwap + * @param opts + */ +export async function getBestUniswapCrossSwapQuotesForOutputA2A( + crossSwap: CrossSwap, + originStrategy: UniswapQuoteFetchStrategy, + destinationStrategy: UniswapQuoteFetchStrategy, + opts: { + preferredBridgeTokens: string[]; + bridgeRoutesLimit: number; + } +): Promise { + const originSwapChainId = crossSwap.inputToken.chainId; + const destinationSwapChainId = crossSwap.outputToken.chainId; + const allBridgeRoutes = getRoutesByChainIds( + originSwapChainId, + destinationSwapChainId + ); + + if (allBridgeRoutes.length === 0) { + throw new Error( + `No bridge routes found for ${originSwapChainId} -> ${destinationSwapChainId}` + ); + } + + const preferredBridgeRoutes = allBridgeRoutes.filter(({ toTokenSymbol }) => + opts.preferredBridgeTokens.includes(toTokenSymbol) + ); + const bridgeRoutesToCompare = ( + preferredBridgeRoutes.length > 0 ? preferredBridgeRoutes : allBridgeRoutes + ).slice(0, opts.bridgeRoutesLimit); + + if (bridgeRoutesToCompare.length === 0) { + throw new Error( + `No bridge routes to compare for ${originSwapChainId} -> ${destinationSwapChainId}` + ); + } + + const crossSwapQuotes = await Promise.all( + bridgeRoutesToCompare.map((bridgeRoute) => + getUniswapCrossSwapQuotesForOutputA2A( + crossSwap, + bridgeRoute, + originStrategy, + destinationStrategy + ) + ) + ); + + // Compare quotes by lowest input amount + const bestCrossSwapQuote = crossSwapQuotes.reduce((prev, curr) => + prev.originSwapQuote!.maximumAmountIn.lt( + curr.originSwapQuote!.maximumAmountIn + ) + ? prev + : curr + ); + return bestCrossSwapQuote; +} + +/** + * Returns Uniswap v3 quote for a swap with min. output amount for route + * ANY input token -> ANY output token, e.g. ARB -> OP, using a specific bridge route. + * @param crossSwap + * @param bridgeRoute + */ +export async function getUniswapCrossSwapQuotesForOutputA2A( + crossSwap: CrossSwap, + bridgeRoute: { + fromTokenAddress: string; + fromChain: number; + toTokenAddress: string; + toChain: number; + }, + originStrategy: UniswapQuoteFetchStrategy, + destinationStrategy: UniswapQuoteFetchStrategy +): Promise { + const originSwapChainId = crossSwap.inputToken.chainId; + const destinationSwapChainId = crossSwap.outputToken.chainId; + + const _bridgeableInputToken = getTokenByAddress( + bridgeRoute.fromTokenAddress, + bridgeRoute.fromChain + ); + const _bridgeableOutputToken = getTokenByAddress( + bridgeRoute.toTokenAddress, + bridgeRoute.toChain + ); + + if (!_bridgeableInputToken) { + throw new Error( + `No bridgeable input token found for ${bridgeRoute.fromTokenAddress} on chain ${bridgeRoute.fromChain}` + ); + } + + if (!_bridgeableOutputToken) { + throw new Error( + `No bridgeable output token found for ${bridgeRoute.toTokenAddress} on chain ${bridgeRoute.toChain}` + ); + } + + const bridgeableInputToken = { + address: bridgeRoute.fromTokenAddress, + decimals: _bridgeableInputToken.decimals, + symbol: _bridgeableInputToken.symbol, + chainId: bridgeRoute.fromChain, + }; + const bridgeableOutputToken = { + address: bridgeRoute.toTokenAddress, + decimals: _bridgeableOutputToken.decimals, + symbol: _bridgeableOutputToken.symbol, + chainId: bridgeRoute.toChain, + }; + const multiCallHandlerAddress = getMultiCallHandlerAddress( + destinationSwapChainId + ); + const originSwap = { + chainId: originSwapChainId, + tokenIn: crossSwap.inputToken, + tokenOut: bridgeableInputToken, + recipient: originStrategy.getPeripheryAddress(originSwapChainId), + slippageTolerance: crossSwap.slippageTolerance, + type: crossSwap.type, + }; + const destinationSwap = { + chainId: destinationSwapChainId, + tokenIn: bridgeableOutputToken, + tokenOut: crossSwap.outputToken, + recipient: multiCallHandlerAddress, + slippageTolerance: crossSwap.slippageTolerance, + type: crossSwap.type, + }; + + // 1. Get destination swap quote for bridgeable output token -> any token + // with exact output amount + let destinationSwapQuote = await destinationStrategy.fetchFn( + { + ...destinationSwap, + amount: crossSwap.amount.toString(), + }, + TradeType.EXACT_OUTPUT + ); + + // 2. Get bridge quote for bridgeable input token -> bridgeable output token + const bridgeQuote = await getBridgeQuoteForMinOutput({ + inputToken: bridgeableInputToken, + outputToken: bridgeableOutputToken, + minOutputAmount: destinationSwapQuote.maximumAmountIn, + recipient: getMultiCallHandlerAddress(destinationSwapChainId), + message: buildDestinationSwapCrossChainMessage({ + crossSwap, + destinationSwapQuote, + bridgeableOutputToken, + routerAddress: destinationStrategy.getRouterAddress( + destinationSwapChainId + ), + }), + }); + + // 3.1. Get origin swap quote for any input token -> bridgeable input token + const originSwapQuote = await originStrategy.fetchFn( + { + ...originSwap, + amount: bridgeQuote.inputAmount.toString(), + }, + TradeType.EXACT_OUTPUT, + { + useIndicativeQuote: true, + } + ); + // 3.2. Re-fetch origin swap quote with updated input amount and EXACT_INPUT type. + // This prevents leftover tokens in the SwapAndBridge contract. + let adjOriginSwapQuote = await originStrategy.fetchFn( + { + ...originSwap, + amount: addMarkupToAmount( + originSwapQuote.maximumAmountIn, + indicativeQuoteBuffer + ).toString(), + }, + TradeType.EXACT_INPUT + ); + assertMinOutputAmount( + adjOriginSwapQuote.minAmountOut, + bridgeQuote.inputAmount + ); + + return { + crossSwap, + destinationSwapQuote, + bridgeQuote, + originSwapQuote: { + ...adjOriginSwapQuote, + peripheryAddress: originStrategy.getPeripheryAddress(originSwapChainId), + }, + }; +} + +function buildDestinationSwapCrossChainMessage({ + crossSwap, + destinationSwapQuote, + bridgeableOutputToken, + routerAddress, +}: { + crossSwap: CrossSwap; + bridgeableOutputToken: AcrossToken; + destinationSwapQuote: SwapQuote; + routerAddress: string; +}) { + const destinationSwapChainId = destinationSwapQuote.tokenOut.chainId; + + let transferActions: { + target: string; + callData: string; + value: string; + }[] = []; + + // If output token is native, we need to unwrap WETH before sending it to the + // recipient. This is because we only handle WETH in the destination swap. + if (crossSwap.isOutputNative) { + transferActions = [ + { + target: crossSwap.outputToken.address, + callData: encodeWethWithdrawCalldata(crossSwap.amount), + value: "0", + }, + { + target: crossSwap.recipient, + callData: "0x", + value: crossSwap.amount.toString(), + }, + ]; + } + // If output token is an ERC-20 token and amount type is EXACT_OUTPUT, we need + // to transfer the EXACT output amount to the recipient. The refundAddress / depositor + // will receive any leftovers. + else if (crossSwap.type === AMOUNT_TYPE.EXACT_OUTPUT) { + transferActions = [ + { + target: crossSwap.outputToken.address, + callData: encodeTransferCalldata(crossSwap.recipient, crossSwap.amount), + value: "0", + }, + { + target: getMultiCallHandlerAddress(destinationSwapChainId), + callData: encodeDrainCalldata( + crossSwap.outputToken.address, + crossSwap.refundAddress ?? crossSwap.depositor + ), + value: "0", + }, + ]; + } + // If output token is an ERC-20 token and amount type is MIN_OUTPUT, we need + // to transfer all realized output tokens to the recipient. + else if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT) { + transferActions = [ + { + target: getMultiCallHandlerAddress(destinationSwapChainId), + callData: encodeDrainCalldata( + crossSwap.outputToken.address, + crossSwap.recipient + ), + value: "0", + }, + ]; + } + + return buildMulticallHandlerMessage({ + fallbackRecipient: getFallbackRecipient(crossSwap), + actions: [ + // approve bridgeable output token + { + target: bridgeableOutputToken.address, + callData: encodeApproveCalldata( + routerAddress, + destinationSwapQuote.maximumAmountIn + ), + value: "0", + }, + // swap bridgeable output token -> cross swap output token + { + target: destinationSwapQuote.swapTx.to, + callData: destinationSwapQuote.swapTx.data, + value: destinationSwapQuote.swapTx.value, + }, + // transfer output tokens to recipient + ...transferActions, + // drain remaining bridgeable output tokens from MultiCallHandler contract + { + target: getMultiCallHandlerAddress(destinationSwapChainId), + callData: encodeDrainCalldata( + bridgeableOutputToken.address, + crossSwap.refundAddress ?? crossSwap.depositor + ), + value: "0", + }, + ], + }); +} + +function assertMinOutputAmount( + amountOut: BigNumber, + expectedMinAmountOut: BigNumber +) { + if (amountOut.lt(expectedMinAmountOut)) { + throw new Error( + `Swap quote output amount ${amountOut.toString()} ` + + `is less than required min. output amount ${expectedMinAmountOut.toString()}` + ); + } +} diff --git a/api/_dexes/uniswap/swap-router-02.ts b/api/_dexes/uniswap/swap-router-02.ts index 0421dd6eb..91482ac1a 100644 --- a/api/_dexes/uniswap/swap-router-02.ts +++ b/api/_dexes/uniswap/swap-router-02.ts @@ -1,39 +1,23 @@ -import { BigNumber, ethers } from "ethers"; -import { CurrencyAmount, Percent, Token, TradeType } from "@uniswap/sdk-core"; -import { - AlphaRouter, - SwapOptionsSwapRouter02, - SwapType, -} from "@uniswap/smart-order-router"; +import { BigNumber } from "ethers"; +import { TradeType } from "@uniswap/sdk-core"; +import { SwapRouter } from "@uniswap/router-sdk"; + import { CHAIN_IDs } from "@across-protocol/constants"; -import { utils } from "@across-protocol/sdk"; +import { getLogger } from "../../_utils"; +import { Swap, SwapQuote } from "../types"; +import { getSpokePoolPeripheryAddress } from "../../_spoke-pool-periphery"; import { - getProvider, - getRouteByInputTokenAndDestinationChain, - getTokenByAddress, - getBridgeQuoteForMinOutput, - getRoutesByChainIds, - getRouteByOutputTokenAndOriginChain, - getLogger, -} from "../../_utils"; -import { - buildMulticallHandlerMessage, - encodeApproveCalldata, - encodeDrainCalldata, - encodeTransferCalldata, - encodeWethWithdrawCalldata, - getMultiCallHandlerAddress, -} from "../../_multicall-handler"; -import { Token as AcrossToken, Swap, CrossSwap, SwapQuote } from "../types"; + addMarkupToAmount, + floatToPercent, + UniswapQuoteFetchStrategy, +} from "./utils"; import { - buildExactOutputBridgeTokenMessage, - buildMinOutputBridgeTokenMessage, - getFallbackRecipient, - NoSwapRouteError, -} from "../utils"; -import { AMOUNT_TYPE } from "../cross-swap"; -import { getSpokePoolPeripheryAddress } from "../../_spoke-pool-periphery"; + getUniswapClassicQuoteFromApi, + getUniswapClassicIndicativeQuoteFromApi, + UniswapClassicQuoteFromApi, +} from "./trading-api"; +import { RouterTradeAdapter } from "./adapter"; // Taken from here: https://docs.uniswap.org/contracts/v3/reference/deployments/ export const SWAP_ROUTER_02_ADDRESS = { @@ -48,613 +32,129 @@ export const SWAP_ROUTER_02_ADDRESS = { [CHAIN_IDs.ZORA]: "0x7De04c96BE5159c3b5CeffC82aa176dc81281557", }; -/** - * Returns Uniswap v3 quote for a swap with min. output amount for route - * BRIDGEABLE input token -> ANY output token, e.g. USDC -> ARB. Required steps: - * 1. Get destination swap quote for bridgeable output token -> any token - * 2. Get bridge quote for bridgeable input token -> bridgeable output token - */ -export async function getUniswapCrossSwapQuotesForOutputB2A( - crossSwap: CrossSwap -) { - const destinationSwapChainId = crossSwap.outputToken.chainId; - const bridgeRoute = getRouteByInputTokenAndDestinationChain( - crossSwap.inputToken.address, - destinationSwapChainId - ); - - if (!bridgeRoute) { - throw new Error( - `No bridge route found for input token ${crossSwap.inputToken.symbol} ` + - `${crossSwap.inputToken.chainId} -> ${destinationSwapChainId}` - ); - } - - const _bridgeableOutputToken = getTokenByAddress( - bridgeRoute.toTokenAddress, - bridgeRoute.toChain - ); - - if (!_bridgeableOutputToken) { - throw new Error( - `No bridgeable output token found for ${bridgeRoute.toTokenAddress} on chain ${bridgeRoute.toChain}` - ); - } - - const bridgeableOutputToken = { - address: bridgeRoute.toTokenAddress, - decimals: _bridgeableOutputToken.decimals, - symbol: _bridgeableOutputToken.symbol, - chainId: destinationSwapChainId, - }; +export function getSwapRouter02Strategy(): UniswapQuoteFetchStrategy { + const getRouterAddress = (chainId: number) => SWAP_ROUTER_02_ADDRESS[chainId]; + const getPeripheryAddress = (chainId: number) => + getSpokePoolPeripheryAddress("uniswap-swapRouter02", chainId); + + const fetchFn = async ( + swap: Swap, + tradeType: TradeType, + opts: Partial<{ + useIndicativeQuote: boolean; + }> = { + useIndicativeQuote: false, + } + ) => { + let swapQuote: SwapQuote; + if (!opts.useIndicativeQuote) { + const { quote } = await getUniswapClassicQuoteFromApi( + { ...swap, swapper: swap.recipient }, + tradeType + ); + const swapTx = buildSwapRouterSwapTx(swap, tradeType, quote); + + const expectedAmountIn = BigNumber.from(quote.input.amount); + const maxAmountIn = + tradeType === TradeType.EXACT_INPUT + ? expectedAmountIn + : addMarkupToAmount(expectedAmountIn, quote.slippage / 100); + const expectedAmountOut = BigNumber.from(quote.output.amount); + const minAmountOut = + tradeType === TradeType.EXACT_OUTPUT + ? expectedAmountOut + : addMarkupToAmount(expectedAmountOut, -quote.slippage / 100); + + swapQuote = { + tokenIn: swap.tokenIn, + tokenOut: swap.tokenOut, + maximumAmountIn: maxAmountIn, + minAmountOut, + expectedAmountOut, + expectedAmountIn, + slippageTolerance: quote.slippage, + swapTx, + }; + } else { + const { input, output } = await getUniswapClassicIndicativeQuoteFromApi( + { ...swap, swapper: swap.recipient }, + tradeType + ); + + const expectedAmountIn = BigNumber.from(input.amount); + const maxAmountIn = + tradeType === TradeType.EXACT_INPUT + ? expectedAmountIn + : addMarkupToAmount(expectedAmountIn, swap.slippageTolerance / 100); + const expectedAmountOut = BigNumber.from(output.amount); + const minAmountOut = + tradeType === TradeType.EXACT_OUTPUT + ? expectedAmountOut + : addMarkupToAmount(expectedAmountOut, -swap.slippageTolerance / 100); + + swapQuote = { + tokenIn: swap.tokenIn, + tokenOut: swap.tokenOut, + maximumAmountIn: maxAmountIn, + minAmountOut, + expectedAmountOut, + expectedAmountIn, + slippageTolerance: swap.slippageTolerance, + swapTx: { + to: "0x", + data: "0x", + value: "0x", + }, + }; + } + + getLogger().debug({ + at: "uniswap/swap-router-02/fetchFn", + message: "Swap quote", + type: + tradeType === TradeType.EXACT_INPUT ? "EXACT_INPUT" : "EXACT_OUTPUT", + tokenIn: swapQuote.tokenIn.symbol, + tokenOut: swapQuote.tokenOut.symbol, + chainId: swap.chainId, + maximumAmountIn: swapQuote.maximumAmountIn.toString(), + minAmountOut: swapQuote.minAmountOut.toString(), + expectedAmountOut: swapQuote.expectedAmountOut.toString(), + expectedAmountIn: swapQuote.expectedAmountIn.toString(), + }); - const destinationSwap = { - chainId: destinationSwapChainId, - tokenIn: bridgeableOutputToken, - tokenOut: crossSwap.outputToken, - recipient: getMultiCallHandlerAddress(destinationSwapChainId), - slippageTolerance: crossSwap.slippageTolerance, + return swapQuote; }; - // 1.1. Get destination swap quote for bridgeable output token -> any token - // with exact output amount. - let destinationSwapQuote = await getUniswapQuoteWithSwapRouter02( - { - ...destinationSwap, - amount: crossSwap.amount.toString(), - }, - TradeType.EXACT_OUTPUT - ); - // 1.2. Re-fetch destination swap quote with exact input amount if leftover tokens - // should be sent to receiver. - if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT) { - destinationSwapQuote = await getUniswapQuoteWithSwapRouter02( - { - ...destinationSwap, - amount: addBufferToAmount(destinationSwapQuote.maximumAmountIn), - }, - TradeType.EXACT_INPUT - ); - assertMinOutputAmount(destinationSwapQuote.minAmountOut, crossSwap.amount); - } - - // 2. Get bridge quote for bridgeable input token -> bridgeable output token - const bridgeQuote = await getBridgeQuoteForMinOutput({ - inputToken: crossSwap.inputToken, - outputToken: bridgeableOutputToken, - minOutputAmount: destinationSwapQuote.maximumAmountIn, - recipient: getMultiCallHandlerAddress(destinationSwapChainId), - message: buildDestinationSwapCrossChainMessage({ - crossSwap, - destinationSwapQuote, - bridgeableOutputToken, - }), - }); return { - crossSwap, - bridgeQuote, - destinationSwapQuote, - originSwapQuote: undefined, + getRouterAddress, + getPeripheryAddress, + fetchFn, }; } -/** - * Returns Uniswap v3 quote for a swap with min. output amount for route - * ANY input token -> BRIDGEABLE output token, e.g. ARB -> USDC. Required steps: - * 1. Get bridge quote for bridgeable input token -> bridgeable output token - * 2. Get origin swap quote for any input token -> bridgeable input token - */ -export async function getUniswapCrossSwapQuotesForOutputA2B( - crossSwap: CrossSwap +export function buildSwapRouterSwapTx( + swap: Swap, + tradeType: TradeType, + quote: UniswapClassicQuoteFromApi ) { - const originSwapChainId = crossSwap.inputToken.chainId; - const destinationChainId = crossSwap.outputToken.chainId; - const bridgeRoute = getRouteByOutputTokenAndOriginChain( - crossSwap.outputToken.address, - originSwapChainId - ); - - if (!bridgeRoute) { - throw new Error( - `No bridge route found for output token ${crossSwap.outputToken.symbol} ` + - `${originSwapChainId} -> ${crossSwap.outputToken.chainId}` - ); - } - - const _bridgeableInputToken = getTokenByAddress( - bridgeRoute.fromTokenAddress, - bridgeRoute.fromChain - ); - - if (!_bridgeableInputToken) { - throw new Error( - `No bridgeable input token found for ${bridgeRoute.toTokenAddress} on chain ${bridgeRoute.toChain}` - ); - } - - const bridgeableInputToken = { - address: bridgeRoute.fromTokenAddress, - decimals: _bridgeableInputToken.decimals, - symbol: _bridgeableInputToken.symbol, - chainId: bridgeRoute.fromChain, - }; - - // 1. Get bridge quote for bridgeable input token -> bridgeable output token - const bridgeQuote = await getBridgeQuoteForMinOutput({ - inputToken: bridgeableInputToken, - outputToken: crossSwap.outputToken, - minOutputAmount: crossSwap.amount, - recipient: getMultiCallHandlerAddress(destinationChainId), - message: buildExactOutputBridgeTokenMessage(crossSwap), - }); - // 1.1. Update bridge quote message for min. output amount - if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT && crossSwap.isOutputNative) { - bridgeQuote.message = buildMinOutputBridgeTokenMessage( - crossSwap, - bridgeQuote.outputAmount - ); - } - - const originSwap = { - chainId: originSwapChainId, - tokenIn: crossSwap.inputToken, - tokenOut: bridgeableInputToken, - recipient: getSpokePoolPeripheryAddress("uniswap", originSwapChainId), - slippageTolerance: crossSwap.slippageTolerance, + const options = { + recipient: swap.recipient, + slippageTolerance: floatToPercent(swap.slippageTolerance), }; - // 2.1. Get origin swap quote for any input token -> bridgeable input token - const originSwapQuote = await getUniswapQuoteWithSwapRouter02( - { - ...originSwap, - amount: bridgeQuote.inputAmount.toString(), - }, - TradeType.EXACT_OUTPUT - ); - // 2.2. Re-fetch origin swap quote with updated input amount and EXACT_INPUT type. - // This prevents leftover tokens in the SwapAndBridge contract. - let adjOriginSwapQuote = await getUniswapQuoteWithSwapRouter02( - { - ...originSwap, - amount: addBufferToAmount(originSwapQuote.maximumAmountIn), - }, - TradeType.EXACT_INPUT - ); - assertMinOutputAmount( - adjOriginSwapQuote.minAmountOut, - bridgeQuote.inputAmount - ); - - return { - crossSwap, - bridgeQuote, - destinationSwapQuote: undefined, - originSwapQuote: adjOriginSwapQuote, - }; -} - -/** - * Returns Uniswap v3 quote for a swap with min. output amount for route - * ANY input token -> ANY output token, e.g. ARB -> OP. We compare quotes from - * different bridge routes and return the best one. In this iteration, we only - * consider a hardcoded list of high-liquid bridge routes. - * @param crossSwap - * @param opts - */ -export async function getBestUniswapCrossSwapQuotesForOutputA2A( - crossSwap: CrossSwap, - opts: { - preferredBridgeTokens: string[]; - bridgeRoutesLimit: number; - } -) { - const originSwapChainId = crossSwap.inputToken.chainId; - const destinationSwapChainId = crossSwap.outputToken.chainId; - const allBridgeRoutes = getRoutesByChainIds( - originSwapChainId, - destinationSwapChainId - ); - if (allBridgeRoutes.length === 0) { - throw new Error( - `No bridge routes found for ${originSwapChainId} -> ${destinationSwapChainId}` - ); - } - - const preferredBridgeRoutes = allBridgeRoutes.filter(({ toTokenSymbol }) => - opts.preferredBridgeTokens.includes(toTokenSymbol) - ); - const bridgeRoutesToCompare = ( - preferredBridgeRoutes.length > 0 ? preferredBridgeRoutes : allBridgeRoutes - ).slice(0, opts.bridgeRoutesLimit); - - if (bridgeRoutesToCompare.length === 0) { - throw new Error( - `No bridge routes to compare for ${originSwapChainId} -> ${destinationSwapChainId}` - ); - } - - const crossSwapQuotes = await Promise.all( - bridgeRoutesToCompare.map((bridgeRoute) => - getUniswapCrossSwapQuotesForOutputA2A(crossSwap, bridgeRoute) - ) - ); - - // Compare quotes by lowest input amount - const bestCrossSwapQuote = crossSwapQuotes.reduce((prev, curr) => - prev.originSwapQuote!.maximumAmountIn.lt( - curr.originSwapQuote!.maximumAmountIn - ) - ? prev - : curr - ); - return bestCrossSwapQuote; -} - -/** - * Returns Uniswap v3 quote for a swap with min. output amount for route - * ANY input token -> ANY output token, e.g. ARB -> OP, using a specific bridge route. - * @param crossSwap - * @param bridgeRoute - */ -export async function getUniswapCrossSwapQuotesForOutputA2A( - crossSwap: CrossSwap, - bridgeRoute: { - fromTokenAddress: string; - fromChain: number; - toTokenAddress: string; - toChain: number; - } -) { - const originSwapChainId = crossSwap.inputToken.chainId; - const destinationSwapChainId = crossSwap.outputToken.chainId; - - const _bridgeableInputToken = getTokenByAddress( - bridgeRoute.fromTokenAddress, - bridgeRoute.fromChain - ); - const _bridgeableOutputToken = getTokenByAddress( - bridgeRoute.toTokenAddress, - bridgeRoute.toChain - ); - - if (!_bridgeableInputToken) { - throw new Error( - `No bridgeable input token found for ${bridgeRoute.fromTokenAddress} on chain ${bridgeRoute.fromChain}` - ); - } - - if (!_bridgeableOutputToken) { - throw new Error( - `No bridgeable output token found for ${bridgeRoute.toTokenAddress} on chain ${bridgeRoute.toChain}` - ); - } - - const bridgeableInputToken = { - address: bridgeRoute.fromTokenAddress, - decimals: _bridgeableInputToken.decimals, - symbol: _bridgeableInputToken.symbol, - chainId: bridgeRoute.fromChain, - }; - const bridgeableOutputToken = { - address: bridgeRoute.toTokenAddress, - decimals: _bridgeableOutputToken.decimals, - symbol: _bridgeableOutputToken.symbol, - chainId: bridgeRoute.toChain, - }; - const originSwap = { - chainId: originSwapChainId, - tokenIn: crossSwap.inputToken, - tokenOut: bridgeableInputToken, - recipient: getSpokePoolPeripheryAddress("uniswap", originSwapChainId), - slippageTolerance: crossSwap.slippageTolerance, - }; - const destinationSwap = { - chainId: destinationSwapChainId, - tokenIn: bridgeableOutputToken, - tokenOut: crossSwap.outputToken, - recipient: getMultiCallHandlerAddress(destinationSwapChainId), - slippageTolerance: crossSwap.slippageTolerance, - }; - - // 1.1. Get destination swap quote for bridgeable output token -> any token - // with exact output amount - let destinationSwapQuote = await getUniswapQuoteWithSwapRouter02( - { - ...destinationSwap, - amount: crossSwap.amount.toString(), - }, - TradeType.EXACT_OUTPUT - ); - // 1.2. Re-fetch destination swap quote with exact input amount if leftover tokens - // should be sent to receiver. - if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT) { - destinationSwapQuote = await getUniswapQuoteWithSwapRouter02( - { - ...destinationSwap, - amount: addBufferToAmount(destinationSwapQuote.maximumAmountIn), - }, - TradeType.EXACT_INPUT - ); - assertMinOutputAmount(destinationSwapQuote.minAmountOut, crossSwap.amount); - } - - // 2. Get bridge quote for bridgeable input token -> bridgeable output token - const bridgeQuote = await getBridgeQuoteForMinOutput({ - inputToken: bridgeableInputToken, - outputToken: bridgeableOutputToken, - minOutputAmount: destinationSwapQuote.expectedAmountIn, - recipient: getMultiCallHandlerAddress(destinationSwapChainId), - message: buildDestinationSwapCrossChainMessage({ - crossSwap, - destinationSwapQuote, - bridgeableOutputToken, - }), - }); - - // 3.1. Get origin swap quote for any input token -> bridgeable input token - const originSwapQuote = await getUniswapQuoteWithSwapRouter02( - { - ...originSwap, - amount: bridgeQuote.inputAmount.toString(), - }, - TradeType.EXACT_OUTPUT - ); - // 3.2. Re-fetch origin swap quote with updated input amount and EXACT_INPUT type. - // This prevents leftover tokens in the SwapAndBridge contract. - const adjOriginSwapQuote = await getUniswapQuoteWithSwapRouter02( - { - ...originSwap, - amount: addBufferToAmount(originSwapQuote.maximumAmountIn), - }, - TradeType.EXACT_INPUT - ); - assertMinOutputAmount( - adjOriginSwapQuote.minAmountOut, - bridgeQuote.inputAmount - ); - - return { - crossSwap, - destinationSwapQuote, - bridgeQuote, - originSwapQuote: adjOriginSwapQuote, - }; -} - -export async function getUniswapQuoteWithSwapRouter02( - swap: Omit, - tradeType: TradeType -): Promise { - const { router, options } = getSwapRouter02AndOptions(swap); - - const amountCurrency = - tradeType === TradeType.EXACT_INPUT ? swap.tokenIn : swap.tokenOut; - const quoteCurrency = - tradeType === TradeType.EXACT_INPUT ? swap.tokenOut : swap.tokenIn; - - const route = await router.route( - CurrencyAmount.fromRawAmount( - new Token( - amountCurrency.chainId, - amountCurrency.address, - amountCurrency.decimals - ), - swap.amount - ), - new Token( - quoteCurrency.chainId, - quoteCurrency.address, - quoteCurrency.decimals - ), + const routerTrade = RouterTradeAdapter.fromClassicQuote({ + tokenIn: quote.input.token, + tokenOut: quote.output.token, tradeType, + route: quote.route, + }); + const { calldata, value } = SwapRouter.swapCallParameters( + routerTrade, options ); - - if (!route || !route.methodParameters) { - throw new NoSwapRouteError({ - dex: "uniswap", - tokenInSymbol: swap.tokenIn.symbol, - tokenOutSymbol: swap.tokenOut.symbol, - chainId: swap.chainId, - swapType: - tradeType === TradeType.EXACT_INPUT ? "EXACT_INPUT" : "EXACT_OUTPUT", - }); - } - - const swapQuote = { - tokenIn: swap.tokenIn, - tokenOut: swap.tokenOut, - maximumAmountIn: ethers.utils.parseUnits( - route.trade.maximumAmountIn(options.slippageTolerance).toExact(), - swap.tokenIn.decimals - ), - minAmountOut: ethers.utils.parseUnits( - route.trade.minimumAmountOut(options.slippageTolerance).toExact(), - swap.tokenOut.decimals - ), - expectedAmountOut: ethers.utils.parseUnits( - route.trade.outputAmount.toExact(), - swap.tokenOut.decimals - ), - expectedAmountIn: ethers.utils.parseUnits( - route.trade.inputAmount.toExact(), - swap.tokenIn.decimals - ), - slippageTolerance: swap.slippageTolerance, - swapTx: { - to: route.methodParameters.to, - data: route.methodParameters.calldata, - value: route.methodParameters.value, - }, - }; - - getLogger().debug({ - at: "uniswap/getUniswapQuoteWithSwapRouter02", - message: "Swap quote", - type: tradeType === TradeType.EXACT_INPUT ? "EXACT_INPUT" : "EXACT_OUTPUT", - tokenIn: swapQuote.tokenIn.symbol, - tokenOut: swapQuote.tokenOut.symbol, - chainId: swap.chainId, - maximumAmountIn: swapQuote.maximumAmountIn.toString(), - minAmountOut: swapQuote.minAmountOut.toString(), - expectedAmountOut: swapQuote.expectedAmountOut.toString(), - expectedAmountIn: swapQuote.expectedAmountIn.toString(), - }); - - return swapQuote; -} - -function getSwapRouter02AndOptions(params: { - chainId: number; - recipient: string; - slippageTolerance: number; -}) { - const router = getSwapRouter02(params.chainId); - const options: SwapOptionsSwapRouter02 = { - recipient: params.recipient, - deadline: utils.getCurrentTime() + 30 * 60, // 30 minutes from now - type: SwapType.SWAP_ROUTER_02, - slippageTolerance: floatToPercent(params.slippageTolerance), - }; return { - router, - options, + data: calldata, + value, + to: SWAP_ROUTER_02_ADDRESS[swap.chainId], }; } - -const swapRouterCache = new Map(); -function getSwapRouter02(chainId: number) { - const provider = getProvider(chainId); - if (!swapRouterCache.has(chainId)) { - swapRouterCache.set(chainId, new AlphaRouter({ chainId, provider })); - } - return swapRouterCache.get(chainId)!; -} - -function floatToPercent(value: number) { - return new Percent( - // max. slippage decimals is 2 - Number(value.toFixed(2)) * 100, - 10_000 - ); -} - -function buildDestinationSwapCrossChainMessage({ - crossSwap, - destinationSwapQuote, - bridgeableOutputToken, -}: { - crossSwap: CrossSwap; - bridgeableOutputToken: AcrossToken; - destinationSwapQuote: SwapQuote; -}) { - const destinationSwapChainId = destinationSwapQuote.tokenOut.chainId; - - let transferActions: { - target: string; - callData: string; - value: string; - }[] = []; - - // If output token is native, we need to unwrap WETH before sending it to the - // recipient. This is because we only handle WETH in the destination swap. - if (crossSwap.isOutputNative) { - transferActions = [ - { - target: crossSwap.outputToken.address, - callData: encodeWethWithdrawCalldata(crossSwap.amount), - value: "0", - }, - { - target: crossSwap.recipient, - callData: "0x", - value: crossSwap.amount.toString(), - }, - ]; - } - // If output token is an ERC-20 token and amount type is EXACT_OUTPUT, we need - // to transfer the EXACT output amount to the recipient. The refundAddress / depositor - // will receive any leftovers. - else if (crossSwap.type === AMOUNT_TYPE.EXACT_OUTPUT) { - transferActions = [ - { - target: crossSwap.outputToken.address, - callData: encodeTransferCalldata(crossSwap.recipient, crossSwap.amount), - value: "0", - }, - { - target: getMultiCallHandlerAddress(destinationSwapChainId), - callData: encodeDrainCalldata( - crossSwap.outputToken.address, - crossSwap.refundAddress ?? crossSwap.depositor - ), - value: "0", - }, - ]; - } - // If output token is an ERC-20 token and amount type is MIN_OUTPUT, we need - // to transfer all realized output tokens to the recipient. - else if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT) { - transferActions = [ - { - target: getMultiCallHandlerAddress(destinationSwapChainId), - callData: encodeDrainCalldata( - crossSwap.outputToken.address, - crossSwap.recipient - ), - value: "0", - }, - ]; - } - - return buildMulticallHandlerMessage({ - fallbackRecipient: getFallbackRecipient(crossSwap), - actions: [ - // approve bridgeable output token - { - target: bridgeableOutputToken.address, - callData: encodeApproveCalldata( - SWAP_ROUTER_02_ADDRESS[destinationSwapChainId], - destinationSwapQuote.maximumAmountIn - ), - value: "0", - }, - // swap bridgeable output token -> cross swap output token - { - target: destinationSwapQuote.swapTx.to, - callData: destinationSwapQuote.swapTx.data, - value: destinationSwapQuote.swapTx.value, - }, - // transfer output tokens to recipient - ...transferActions, - // drain remaining bridgeable output tokens from MultiCallHandler contract - { - target: getMultiCallHandlerAddress(destinationSwapChainId), - callData: encodeDrainCalldata( - bridgeableOutputToken.address, - crossSwap.refundAddress ?? crossSwap.depositor - ), - value: "0", - }, - ], - }); -} - -function assertMinOutputAmount( - amountOut: BigNumber, - expectedMinAmountOut: BigNumber -) { - if (amountOut.lt(expectedMinAmountOut)) { - throw new Error( - `Swap quote output amount ${amountOut.toString()} ` + - `is less than required min. output amount ${expectedMinAmountOut.toString()}` - ); - } -} - -function addBufferToAmount(amount: BigNumber, buffer = 0.01) { - return amount - .mul(ethers.utils.parseEther((1 + Number(buffer)).toString())) - .div(utils.fixedPointAdjustment) - .toString(); -} diff --git a/api/_dexes/uniswap/trading-api.ts b/api/_dexes/uniswap/trading-api.ts new file mode 100644 index 000000000..53a640580 --- /dev/null +++ b/api/_dexes/uniswap/trading-api.ts @@ -0,0 +1,139 @@ +import { TradeType } from "@uniswap/sdk-core"; +import axios from "axios"; + +import { Swap } from "../types"; +import { V2PoolInRoute, V3PoolInRoute } from "./adapter"; + +export type UniswapClassicQuoteFromApi = { + chainId: number; + input: { + amount: string; + token: string; + }; + output: { + amount: string; + token: string; + recipient: string; + }; + swapper: string; + route: Array<(V3PoolInRoute | V2PoolInRoute)[]>; + slippage: number; + tradeType: "EXACT_OUTPUT" | "EXACT_INPUT"; + quoteId: string; +}; + +export type UniswapParamForApi = Omit & { + swapper: string; + slippageTolerance?: number; +}; + +export const UNISWAP_TRADING_API_BASE_URL = + process.env.UNISWAP_TRADING_API_BASE_URL || + "https://trading-api-labs.interface.gateway.uniswap.org/v1"; + +export const UNISWAP_API_KEY = + process.env.UNISWAP_API_KEY || "JoyCGj29tT4pymvhaGciK4r1aIPvqW6W53xT1fwo"; + +/** + * Based on https://uniswap-docs.readme.io/reference/aggregator_quote-1 + */ +export async function getUniswapClassicQuoteFromApi( + swap: UniswapParamForApi, + tradeType: TradeType +) { + const response = await axios.post<{ + requestId: string; + routing: "CLASSIC"; + quote: UniswapClassicQuoteFromApi; + }>( + `${UNISWAP_TRADING_API_BASE_URL}/quote`, + { + type: + tradeType === TradeType.EXACT_INPUT ? "EXACT_INPUT" : "EXACT_OUTPUT", + tokenInChainId: swap.tokenIn.chainId, + tokenOutChainId: swap.tokenOut.chainId, + tokenIn: swap.tokenIn.address, + tokenOut: swap.tokenOut.address, + swapper: swap.swapper, + slippageTolerance: swap.slippageTolerance, + autoSlippage: swap.slippageTolerance ? undefined : "DEFAULT", + amount: swap.amount, + urgency: "urgent", + routingPreference: "CLASSIC", + }, + { + headers: { + "x-api-key": UNISWAP_API_KEY, + }, + } + ); + return response.data; +} + +export async function getUniswapClassicIndicativeQuoteFromApi( + swap: UniswapParamForApi, + tradeType: TradeType +) { + const response = await axios.post<{ + requestId: string; + input: { + amount: string; + chainId: number; + token: string; + }; + output: { + amount: string; + chainId: number; + token: string; + }; + }>( + `${UNISWAP_TRADING_API_BASE_URL}/indicative_quote`, + { + type: + tradeType === TradeType.EXACT_INPUT ? "EXACT_INPUT" : "EXACT_OUTPUT", + amount: swap.amount, + tokenInChainId: swap.tokenIn.chainId, + tokenOutChainId: swap.tokenOut.chainId, + tokenIn: swap.tokenIn.address, + tokenOut: swap.tokenOut.address, + }, + { + headers: { + "x-api-key": UNISWAP_API_KEY, + }, + } + ); + return response.data; +} + +export async function getUniswapClassicCalldataFromApi( + classicQuote: UniswapClassicQuoteFromApi +) { + const response = await axios.post<{ + requestId: string; + swap: { + to: string; + from: string; + data: string; + value: string; + gasLimit: string; + chainId: number; + maxFeePerGas: string; + maxPriorityFeePerGas: string; + gasPrice: string; + }; + }>( + `${UNISWAP_TRADING_API_BASE_URL}/swap`, + { + quote: classicQuote, + simulateTransaction: false, + urgency: "urgent", + }, + { + headers: { + "x-api-key": UNISWAP_API_KEY, + }, + } + ); + return response.data; +} diff --git a/api/_dexes/uniswap/universal-router.ts b/api/_dexes/uniswap/universal-router.ts new file mode 100644 index 000000000..b2722c5ed --- /dev/null +++ b/api/_dexes/uniswap/universal-router.ts @@ -0,0 +1,159 @@ +import { BigNumber } from "ethers"; +import { TradeType } from "@uniswap/sdk-core"; +import { CHAIN_IDs } from "@across-protocol/constants"; +import { SwapRouter } from "@uniswap/universal-router-sdk"; + +import { getLogger } from "../../_utils"; +import { Swap, SwapQuote } from "../types"; +import { getSpokePoolPeripheryAddress } from "../../_spoke-pool-periphery"; +import { + getUniswapClassicQuoteFromApi, + getUniswapClassicIndicativeQuoteFromApi, + UniswapClassicQuoteFromApi, +} from "./trading-api"; +import { + UniswapQuoteFetchStrategy, + addMarkupToAmount, + floatToPercent, +} from "./utils"; +import { RouterTradeAdapter } from "./adapter"; + +// https://uniswap-docs.readme.io/reference/faqs#i-need-to-whitelist-the-router-addresses-where-can-i-find-them +export const UNIVERSAL_ROUTER_ADDRESS = { + [CHAIN_IDs.ARBITRUM]: "0x5E325eDA8064b456f4781070C0738d849c824258", + [CHAIN_IDs.BASE]: "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", + [CHAIN_IDs.BLAST]: "0x643770E279d5D0733F21d6DC03A8efbABf3255B4", + [CHAIN_IDs.MAINNET]: "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", + [CHAIN_IDs.OPTIMISM]: "0xCb1355ff08Ab38bBCE60111F1bb2B784bE25D7e8", + [CHAIN_IDs.POLYGON]: "0xec7BE89e9d109e7e3Fec59c222CF297125FEFda2", + [CHAIN_IDs.WORLD_CHAIN]: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + [CHAIN_IDs.ZORA]: "0x2986d9721A49838ab4297b695858aF7F17f38014", + [CHAIN_IDs.ZK_SYNC]: "0x28731BCC616B5f51dD52CF2e4dF0E78dD1136C06", +}; + +export function getUniversalRouterStrategy(): UniswapQuoteFetchStrategy { + const getRouterAddress = (chainId: number) => + UNIVERSAL_ROUTER_ADDRESS[chainId]; + const getPeripheryAddress = (chainId: number) => + getSpokePoolPeripheryAddress("uniswap-universalRouter", chainId); + + const fetchFn = async ( + swap: Swap, + tradeType: TradeType, + opts: Partial<{ + useIndicativeQuote: boolean; + }> = { + useIndicativeQuote: false, + } + ) => { + let swapQuote: SwapQuote; + if (!opts.useIndicativeQuote) { + const { quote } = await getUniswapClassicQuoteFromApi( + { ...swap, swapper: swap.recipient }, + tradeType + ); + const swapTx = buildUniversalRouterSwapTx(swap, tradeType, quote); + + const expectedAmountIn = BigNumber.from(quote.input.amount); + const maxAmountIn = + tradeType === TradeType.EXACT_INPUT + ? expectedAmountIn + : addMarkupToAmount(expectedAmountIn, quote.slippage / 100); + const expectedAmountOut = BigNumber.from(quote.output.amount); + const minAmountOut = + tradeType === TradeType.EXACT_OUTPUT + ? expectedAmountOut + : addMarkupToAmount(expectedAmountOut, -quote.slippage / 100); + + swapQuote = { + tokenIn: swap.tokenIn, + tokenOut: swap.tokenOut, + maximumAmountIn: maxAmountIn, + minAmountOut, + expectedAmountOut, + expectedAmountIn, + slippageTolerance: quote.slippage, + swapTx, + }; + } else { + const { input, output } = await getUniswapClassicIndicativeQuoteFromApi( + { ...swap, swapper: swap.recipient }, + tradeType + ); + + const expectedAmountIn = BigNumber.from(input.amount); + const maxAmountIn = + tradeType === TradeType.EXACT_INPUT + ? expectedAmountIn + : addMarkupToAmount(expectedAmountIn, swap.slippageTolerance / 100); + const expectedAmountOut = BigNumber.from(output.amount); + const minAmountOut = + tradeType === TradeType.EXACT_OUTPUT + ? expectedAmountOut + : addMarkupToAmount(expectedAmountOut, -swap.slippageTolerance / 100); + + swapQuote = { + tokenIn: swap.tokenIn, + tokenOut: swap.tokenOut, + maximumAmountIn: maxAmountIn, + minAmountOut, + expectedAmountOut, + expectedAmountIn, + slippageTolerance: swap.slippageTolerance, + swapTx: { + to: "0x", + data: "0x", + value: "0x", + }, + }; + } + + getLogger().debug({ + at: "uniswap/universal-router/fetchFn", + message: "Swap quote", + type: + tradeType === TradeType.EXACT_INPUT ? "EXACT_INPUT" : "EXACT_OUTPUT", + tokenIn: swapQuote.tokenIn.symbol, + tokenOut: swapQuote.tokenOut.symbol, + chainId: swap.chainId, + maximumAmountIn: swapQuote.maximumAmountIn.toString(), + minAmountOut: swapQuote.minAmountOut.toString(), + expectedAmountOut: swapQuote.expectedAmountOut.toString(), + expectedAmountIn: swapQuote.expectedAmountIn.toString(), + }); + + return swapQuote; + }; + + return { + getRouterAddress, + getPeripheryAddress, + fetchFn, + }; +} + +export function buildUniversalRouterSwapTx( + swap: Swap, + tradeType: TradeType, + quote: UniswapClassicQuoteFromApi +) { + const options = { + recipient: swap.recipient, + slippageTolerance: floatToPercent(swap.slippageTolerance), + }; + const routerTrade = RouterTradeAdapter.fromClassicQuote({ + tokenIn: quote.input.token, + tokenOut: quote.output.token, + tradeType, + route: quote.route, + }); + const { calldata, value } = SwapRouter.swapCallParameters( + routerTrade, + options + ); + return { + data: calldata, + value, + to: UNIVERSAL_ROUTER_ADDRESS[swap.chainId], + }; +} diff --git a/api/_dexes/uniswap/utils.ts b/api/_dexes/uniswap/utils.ts index 66d218f4b..b85818564 100644 --- a/api/_dexes/uniswap/utils.ts +++ b/api/_dexes/uniswap/utils.ts @@ -1,5 +1,23 @@ import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants"; -import { Token } from "../types"; +import { Percent, TradeType } from "@uniswap/sdk-core"; +import axios from "axios"; +import { BigNumber, ethers } from "ethers"; +import { utils } from "@across-protocol/sdk"; + +import { Swap, SwapQuote, Token } from "../types"; + +export type UniswapQuoteFetchStrategy = { + getRouterAddress: (chainId: number) => string; + getPeripheryAddress: (chainId: number) => string; + fetchFn: UniswapQuoteFetchFn; +}; +export type UniswapQuoteFetchFn = ( + swap: Swap, + tradeType: TradeType, + opts?: Partial<{ + useIndicativeQuote: boolean; + }> +) => Promise; // Maps testnet chain IDs to their prod counterparts. Used to get the prod token // info for testnet tokens. @@ -10,6 +28,64 @@ const TESTNET_TO_PROD = { [CHAIN_IDs.ARBITRUM_SEPOLIA]: CHAIN_IDs.ARBITRUM, }; +export const UNISWAP_TRADING_API_BASE_URL = + process.env.UNISWAP_TRADING_API_BASE_URL || + "https://trading-api-labs.interface.gateway.uniswap.org/v1"; + +export const UNISWAP_API_KEY = + process.env.UNISWAP_API_KEY || "JoyCGj29tT4pymvhaGciK4r1aIPvqW6W53xT1fwo"; + +/** + * Based on https://uniswap-docs.readme.io/reference/aggregator_quote-1 + */ +export async function getUniswapClassicQuoteFromApi( + swap: Omit & { swapper: string }, + tradeType: TradeType +) { + const response = await axios.post( + `${UNISWAP_TRADING_API_BASE_URL}/quote`, + { + type: + tradeType === TradeType.EXACT_INPUT ? "EXACT_INPUT" : "EXACT_OUTPUT", + tokenInChainId: swap.tokenIn.chainId, + tokenOutChainId: swap.tokenOut.chainId, + tokenIn: swap.tokenIn.address, + tokenOut: swap.tokenOut.address, + swapper: swap.swapper, + slippageTolerance: swap.slippageTolerance, + amount: swap.amount, + routingPreference: "CLASSIC", + urgency: "urgent", + }, + { + headers: { + "x-api-key": UNISWAP_API_KEY, + }, + } + ); + return response.data as { + requestId: string; + routing: "CLASSIC"; + quote: { + chainId: number; + input: { + amount: string; + token: string; + }; + output: { + amount: string; + token: string; + recipient: string; + }; + swapper: string; + route: any[]; + slippage: number; + tradeType: "EXACT_OUTPUT" | "EXACT_INPUT"; + quoteId: string; + }; + }; +} + export function getProdToken(token: Token) { const prodChainId = TESTNET_TO_PROD[token.chainId] || token.chainId; @@ -29,3 +105,17 @@ export function getProdToken(token: Token) { address: prodTokenAddress, }; } + +export function addMarkupToAmount(amount: BigNumber, markup = 0.01) { + return amount + .mul(ethers.utils.parseEther((1 + Number(markup)).toString())) + .div(utils.fixedPointAdjustment); +} + +export function floatToPercent(value: number) { + return new Percent( + // max. slippage decimals is 2 + Number(value.toFixed(2)) * 100, + 10_000 + ); +} diff --git a/api/_errors.ts b/api/_errors.ts index 93db80b38..a9e2b978e 100644 --- a/api/_errors.ts +++ b/api/_errors.ts @@ -241,7 +241,7 @@ export function handleErrorCondition( { cause: error } ); } else { - const message = `Upstream http request to ${error.request?.url} failed with ${error.status} ${error.message}`; + const message = `Upstream http request to ${error.request?.host} failed with ${error.response?.status}`; acrossApiError = new AcrossApiError( { message, @@ -276,6 +276,7 @@ export function handleErrorCondition( at: endpoint, code: acrossApiError.code, message: `Status ${acrossApiError.status} - ${acrossApiError.message}`, + cause: acrossApiError.cause, }); return response.status(acrossApiError.status).json(acrossApiError); diff --git a/api/_spoke-pool-periphery.ts b/api/_spoke-pool-periphery.ts index 26a2e440c..6e568682b 100644 --- a/api/_spoke-pool-periphery.ts +++ b/api/_spoke-pool-periphery.ts @@ -25,9 +25,7 @@ export function getSpokePoolPeripheryAddress(dex: string, chainId: number) { return address; } -export function getSpokePoolPeriphery(dex: string, chainId: number) { - const address = getSpokePoolPeripheryAddress(dex, chainId); - +export function getSpokePoolPeriphery(address: string, chainId: number) { return SpokePoolV3Periphery__factory.connect(address, getProvider(chainId)); } diff --git a/api/_utils.ts b/api/_utils.ts index 076f39a0c..70e1e9502 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -971,7 +971,7 @@ export async function getBridgeQuoteForMinOutput(params: { { cause: err } ); } else { - const message = `Upstream http request to ${err.request?.url} failed with ${err.status} ${err.message}`; + const message = `Upstream http request to ${err.request?.host} failed with ${err.response?.status}`; throw new AcrossApiError( { message, diff --git a/api/swap/_utils.ts b/api/swap/_utils.ts index c1f57e13d..bde17a64a 100644 --- a/api/swap/_utils.ts +++ b/api/swap/_utils.ts @@ -131,12 +131,12 @@ export async function handleBaseSwapQueryParams({ }); // 3. Calculate fees based for full route - const fees = await calculateCrossSwapFees(crossSwapQuotes); + // const fees = await calculateCrossSwapFees(crossSwapQuotes); return { crossSwapQuotes: { ...crossSwapQuotes, - fees, + // fees, }, integratorId, skipOriginTxEstimation, diff --git a/api/swap/allowance.ts b/api/swap/allowance.ts index 2e593a741..31498c0a2 100644 --- a/api/swap/allowance.ts +++ b/api/swap/allowance.ts @@ -8,7 +8,10 @@ import { handleErrorCondition, latestGasPriceCache, } from "../_utils"; -import { buildCrossSwapTxForAllowanceHolder } from "../_dexes/cross-swap"; +import { + AMOUNT_TYPE, + buildCrossSwapTxForAllowanceHolder, +} from "../_dexes/cross-swap"; import { handleBaseSwapQueryParams, BaseSwapQueryParams, @@ -96,7 +99,7 @@ const handler = async ( : crossSwapQuotes.bridgeQuote.outputToken; const responseJson = { - fees: crossSwapQuotes.fees, + // fees: crossSwapQuotes.fees, checks: { allowance: { token: inputTokenAddress, diff --git a/scripts/generate-routes.ts b/scripts/generate-routes.ts index 4bc4790d2..ca07e01fb 100644 --- a/scripts/generate-routes.ts +++ b/scripts/generate-routes.ts @@ -105,7 +105,7 @@ const enabledRoutes = { }, }, spokePoolPeripheryAddresses: { - uniswap: { + "uniswap-swapRouter02": { [CHAIN_IDs.POLYGON]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", [CHAIN_IDs.OPTIMISM]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", [CHAIN_IDs.ARBITRUM]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", @@ -116,6 +116,10 @@ const enabledRoutes = { [CHAIN_IDs.ZORA]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", [CHAIN_IDs.MAINNET]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", }, + "uniswap-universalRouter": { + [CHAIN_IDs.OPTIMISM]: "0xaED9bBFdCC63219d77D54F8427aeb84C2Be46c5f", + [CHAIN_IDs.ARBITRUM]: "0xaED9bBFdCC63219d77D54F8427aeb84C2Be46c5f", + }, }, routes: transformChainConfigs(enabledMainnetChainConfigs), }, diff --git a/scripts/tests/swap-allowance.ts b/scripts/tests/swap-allowance.ts index 7f0b539bc..a32311ccd 100644 --- a/scripts/tests/swap-allowance.ts +++ b/scripts/tests/swap-allowance.ts @@ -109,7 +109,7 @@ const MIN_OUTPUT_CASES = [ { labels: ["A2B", "MIN_OUTPUT", "USDC - WETH"], params: { - amount: ethers.utils.parseUnits("0.01", 18).toString(), + amount: ethers.utils.parseUnits("0.001", 18).toString(), tradeType: "minOutput", inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[originChainId], originChainId, @@ -170,7 +170,6 @@ async function swap() { console.log("\nTest case:", testCase.labels.join(" ")); console.log("Params:", testCase.params); const response = await axios.get( - // `https://preview.across.to/api/swap/allowance`, `http://localhost:3000/api/swap/allowance`, { params: testCase.params, @@ -178,7 +177,7 @@ async function swap() { ); console.log(response.data); - if (!process.env.DEV_WALLET_PK) { + if (process.env.DEV_WALLET_PK) { const wallet = new Wallet(process.env.DEV_WALLET_PK!).connect( getProvider(testCase.params.originChainId) ); diff --git a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json index ffd9eafe3..63b16f428 100644 --- a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json +++ b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json @@ -21,7 +21,7 @@ } }, "spokePoolPeripheryAddresses": { - "uniswap": { + "uniswap-swapRouter02": { "1": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", "10": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", "137": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", @@ -31,6 +31,10 @@ "42161": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", "81457": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", "7777777": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e" + }, + "uniswap-universalRouter": { + "10": "0xaED9bBFdCC63219d77D54F8427aeb84C2Be46c5f", + "42161": "0xaED9bBFdCC63219d77D54F8427aeb84C2Be46c5f" } }, "routes": [ From bdd60f1ca9c8d90e3333cdc74b6207e759d4e660 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp <51655063+gsteenkamp89@users.noreply.github.com> Date: Wed, 4 Dec 2024 16:03:19 +0200 Subject: [PATCH 28/47] measure swap/allowance performance (#1301) * update sdk * measure by input type * measure by cross swap type * measure full run of endpoint * use console instance without global loglevel restriction * remove unnecessary awaits --- api/_dexes/cross-swap.ts | 29 +++++++++++++++++++++++++---- api/_utils.ts | 1 + api/swap/_utils.ts | 37 ++++++++++++++++++++++++------------- api/swap/allowance.ts | 16 +++++++++++----- package.json | 2 +- yarn.lock | 8 ++++---- 6 files changed, 66 insertions(+), 27 deletions(-) diff --git a/api/_dexes/cross-swap.ts b/api/_dexes/cross-swap.ts index 2a4a28b7c..bcaa5bce9 100644 --- a/api/_dexes/cross-swap.ts +++ b/api/_dexes/cross-swap.ts @@ -8,6 +8,7 @@ import { isOutputTokenBridgeable, getBridgeQuoteForMinOutput, getSpokePool, + Profiler, } from "../_utils"; import { getBestUniswapCrossSwapQuotesForOutputA2A, @@ -78,6 +79,10 @@ export async function getCrossSwapQuotes( } export async function getCrossSwapQuotesForOutput(crossSwap: CrossSwap) { + const profiler = new Profiler({ + at: "api/cross-swap#getCrossSwapQuotesForOutput", + logger: console, + }); const crossSwapType = getCrossSwapType({ inputToken: crossSwap.inputToken.address, originChainId: crossSwap.inputToken.chainId, @@ -86,19 +91,35 @@ export async function getCrossSwapQuotesForOutput(crossSwap: CrossSwap) { }); if (crossSwapType === CROSS_SWAP_TYPE.BRIDGEABLE_TO_BRIDGEABLE) { - return getCrossSwapQuotesForOutputB2B(crossSwap); + return profiler.measureAsync( + getCrossSwapQuotesForOutputB2B(crossSwap), + "getCrossSwapQuotesForOutputB2B", + crossSwap + ); } if (crossSwapType === CROSS_SWAP_TYPE.BRIDGEABLE_TO_ANY) { - return getCrossSwapQuotesForOutputB2A(crossSwap); + return profiler.measureAsync( + getCrossSwapQuotesForOutputB2A(crossSwap), + "getCrossSwapQuotesForOutputB2A", + crossSwap + ); } if (crossSwapType === CROSS_SWAP_TYPE.ANY_TO_BRIDGEABLE) { - return getCrossSwapQuotesForOutputA2B(crossSwap); + return profiler.measureAsync( + getCrossSwapQuotesForOutputA2B(crossSwap), + "getCrossSwapQuotesForOutputA2B", + crossSwap + ); } if (crossSwapType === CROSS_SWAP_TYPE.ANY_TO_ANY) { - return getCrossSwapQuotesForOutputA2A(crossSwap); + return profiler.measureAsync( + getCrossSwapQuotesForOutputA2A(crossSwap), + "getCrossSwapQuotesForOutputA2A", + crossSwap + ); } throw new Error("Invalid cross swap type"); diff --git a/api/_utils.ts b/api/_utils.ts index dc81bee5b..7af9e511e 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -86,6 +86,7 @@ import { import { Token } from "./_dexes/types"; export { InputError, handleErrorCondition } from "./_errors"; +export const { Profiler } = sdk.utils; type LoggingUtility = sdk.relayFeeCalculator.Logger; type RpcProviderName = keyof typeof rpcProvidersJson.providers.urls; diff --git a/api/swap/_utils.ts b/api/swap/_utils.ts index bde17a64a..eb92683e7 100644 --- a/api/swap/_utils.ts +++ b/api/swap/_utils.ts @@ -10,6 +10,7 @@ import { getCachedTokenInfo, getWrappedNativeTokenAddress, getCachedTokenPrice, + Profiler, } from "../_utils"; import { AMOUNT_TYPE, @@ -48,6 +49,10 @@ export type BaseSwapQueryParams = Infer; export async function handleBaseSwapQueryParams({ query, }: TypedVercelRequest) { + const profiler = new Profiler({ + at: "api/_utils#handleBaseSwapQueryParams", + logger: console, + }); assert(query, BaseSwapQueryParamsSchema); const { @@ -116,19 +121,25 @@ export async function handleBaseSwapQueryParams({ ]); // 2. Get swap quotes and calldata based on the swap type - const crossSwapQuotes = await getCrossSwapQuotes({ - amount, - inputToken, - outputToken, - depositor, - recipient: recipient || depositor, - slippageTolerance: Number(slippageTolerance), - type: amountType, - refundOnOrigin, - refundAddress, - isInputNative, - isOutputNative, - }); + const crossSwapQuotes = await profiler.measureAsync( + getCrossSwapQuotes({ + amount, + inputToken, + outputToken, + depositor, + recipient: recipient || depositor, + slippageTolerance: Number(slippageTolerance), + type: amountType, + refundOnOrigin, + refundAddress, + isInputNative, + isOutputNative, + }), + "getCrossSwapQuotes", + { + swapType: amountType, + } + ); // 3. Calculate fees based for full route // const fees = await calculateCrossSwapFees(crossSwapQuotes); diff --git a/api/swap/allowance.ts b/api/swap/allowance.ts index 31498c0a2..f5cbc9b69 100644 --- a/api/swap/allowance.ts +++ b/api/swap/allowance.ts @@ -7,11 +7,9 @@ import { getProvider, handleErrorCondition, latestGasPriceCache, + Profiler, } from "../_utils"; -import { - AMOUNT_TYPE, - buildCrossSwapTxForAllowanceHolder, -} from "../_dexes/cross-swap"; +import { buildCrossSwapTxForAllowanceHolder } from "../_dexes/cross-swap"; import { handleBaseSwapQueryParams, BaseSwapQueryParams, @@ -30,6 +28,11 @@ const handler = async ( query: request.query, }); try { + const profiler = new Profiler({ + at: "swap/allowance", + logger: console, + }); + const mark = profiler.start("e2e endpoint runtime"); const { crossSwapQuotes, integratorId, @@ -142,7 +145,10 @@ const handler = async ( expectedFillTime: crossSwapQuotes.bridgeQuote.suggestedFees.estimatedFillTimeSec, }; - + mark.stop({ + crossSwapQuotes, + request, + }); logger.debug({ at: "Swap/allowance", message: "Response data", diff --git a/package.json b/package.json index 66c5aff7d..b0afbcc24 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@across-protocol/constants": "^3.1.20", "@across-protocol/contracts": "^3.0.16", "@across-protocol/contracts-v3.0.6": "npm:@across-protocol/contracts@3.0.6", - "@across-protocol/sdk": "^3.3.22", + "@across-protocol/sdk": "^3.3.23", "@amplitude/analytics-browser": "^2.3.5", "@balancer-labs/sdk": "1.1.6-beta.16", "@emotion/react": "^11.13.0", diff --git a/yarn.lock b/yarn.lock index 7ee51ac43..2ce25fdaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -78,10 +78,10 @@ yargs "^17.7.2" zksync-web3 "^0.14.3" -"@across-protocol/sdk@^3.3.22": - version "3.3.22" - resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-3.3.22.tgz#d71a88fcf239141a170aec12e7c8dd05f9c8405a" - integrity sha512-+YIJm5SU8tjWWwD8TNsJRoHwJ2PhyhwggfAX6CP3dSHsEza8r8T9c4ijV6oTmIvZRxQo3rTMSzLkrCh/tQGoCg== +"@across-protocol/sdk@^3.3.23": + version "3.3.23" + resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-3.3.23.tgz#ecc638895f33425f5905d95f1680a76504ad66ec" + integrity sha512-1AVVE8Z3rCjPDu/HAqAe2sErQXm+vbBz7VuVZPVmzdlLAHKAaAFOqtmWUNJg3iPYAkfAAjfoykBT48APDWRkfg== dependencies: "@across-protocol/across-token" "^1.0.0" "@across-protocol/constants" "^3.1.20" From 5101979d7d15d090c4843fc92eeabbe05e9aff89 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Tue, 17 Dec 2024 00:37:49 +0100 Subject: [PATCH 29/47] feat: support `UniversalSwapAndBridge` in swap endpoint (#1311) --- api/_dexes/cross-swap.ts | 84 ++++++++++++------ api/_dexes/types.ts | 16 +++- api/_dexes/uniswap/quote-resolver.ts | 14 +-- api/_dexes/uniswap/swap-router-02.ts | 28 ++++-- api/_dexes/uniswap/universal-router.ts | 9 +- api/_dexes/uniswap/utils.ts | 11 ++- api/_dexes/utils.ts | 64 ++++++++++++-- scripts/generate-routes.ts | 85 ++++++++++++------- ...24e63716afAcE30C9a417E0542281869f7d9e.json | 5 +- ...6fA914353c44b2E33eBE05f21846F1048bEda.json | 18 ++++ 10 files changed, 251 insertions(+), 83 deletions(-) diff --git a/api/_dexes/cross-swap.ts b/api/_dexes/cross-swap.ts index bcaa5bce9..9bbad0e9d 100644 --- a/api/_dexes/cross-swap.ts +++ b/api/_dexes/cross-swap.ts @@ -21,6 +21,7 @@ import { CrossSwap, CrossSwapQuotes } from "./types"; import { buildExactOutputBridgeTokenMessage, buildMinOutputBridgeTokenMessage, + getUniversalSwapAndBridge, } from "./utils"; import { getSpokePoolPeriphery } from "../_spoke-pool-periphery"; import { tagIntegratorId } from "../_integrator-id"; @@ -32,8 +33,6 @@ export type CrossSwapType = export type AmountType = (typeof AMOUNT_TYPE)[keyof typeof AMOUNT_TYPE]; -export type LeftoverType = (typeof LEFTOVER_TYPE)[keyof typeof LEFTOVER_TYPE]; - export const AMOUNT_TYPE = { EXACT_INPUT: "exactInput", EXACT_OUTPUT: "exactOutput", @@ -47,15 +46,11 @@ export const CROSS_SWAP_TYPE = { ANY_TO_ANY: "anyToAny", } as const; -export const LEFTOVER_TYPE = { - OUTPUT_TOKEN: "outputToken", - BRIDGEABLE_TOKEN: "bridgeableToken", -} as const; - export const PREFERRED_BRIDGE_TOKENS = ["WETH"]; const defaultQuoteFetchStrategy: UniswapQuoteFetchStrategy = - getSwapRouter02Strategy(); + // This will be our default strategy until the periphery contract is audited + getSwapRouter02Strategy("UniversalSwapAndBridge"); const strategyOverrides = { [CHAIN_IDs.BLAST]: defaultQuoteFetchStrategy, }; @@ -88,6 +83,7 @@ export async function getCrossSwapQuotesForOutput(crossSwap: CrossSwap) { originChainId: crossSwap.inputToken.chainId, outputToken: crossSwap.outputToken.address, destinationChainId: crossSwap.outputToken.chainId, + isInputNative: Boolean(crossSwap.isInputNative), }); if (crossSwapType === CROSS_SWAP_TYPE.BRIDGEABLE_TO_BRIDGEABLE) { @@ -194,6 +190,7 @@ export function getCrossSwapType(params: { originChainId: number; outputToken: string; destinationChainId: number; + isInputNative: boolean; }): CrossSwapType { if ( isRouteEnabled( @@ -206,6 +203,20 @@ export function getCrossSwapType(params: { return CROSS_SWAP_TYPE.BRIDGEABLE_TO_BRIDGEABLE; } + // Prefer destination swap if input token is native because legacy + // `UniversalSwapAndBridge` does not support native tokens as input. + if (params.isInputNative) { + if (isInputTokenBridgeable(params.inputToken, params.originChainId)) { + return CROSS_SWAP_TYPE.BRIDGEABLE_TO_ANY; + } + // We can't bridge native tokens that are not ETH, e.g. MATIC or AZERO. Therefore + // throw until we have periphery contract audited so that it can accept native + // tokens and do an origin swap. + throw new Error( + "Unsupported swap: Input token is native but not bridgeable" + ); + } + if (isOutputTokenBridgeable(params.outputToken, params.destinationChainId)) { return CROSS_SWAP_TYPE.ANY_TO_BRIDGEABLE; } @@ -230,24 +241,45 @@ export async function buildCrossSwapTxForAllowanceHolder( let toAddress: string; if (crossSwapQuotes.originSwapQuote) { - const spokePoolPeriphery = getSpokePoolPeriphery( - crossSwapQuotes.originSwapQuote.peripheryAddress, - originChainId - ); - tx = await spokePoolPeriphery.populateTransaction.swapAndBridge( - crossSwapQuotes.originSwapQuote.tokenIn.address, - crossSwapQuotes.originSwapQuote.tokenOut.address, - crossSwapQuotes.originSwapQuote.swapTx.data, - crossSwapQuotes.originSwapQuote.maximumAmountIn, - crossSwapQuotes.originSwapQuote.minAmountOut, - deposit, - { - value: crossSwapQuotes.crossSwap.isInputNative - ? crossSwapQuotes.originSwapQuote.maximumAmountIn - : 0, - } - ); - toAddress = spokePoolPeriphery.address; + const { entryPointContract } = crossSwapQuotes.originSwapQuote; + if (entryPointContract.name === "SpokePoolPeriphery") { + const spokePoolPeriphery = getSpokePoolPeriphery( + entryPointContract.address, + originChainId + ); + tx = await spokePoolPeriphery.populateTransaction.swapAndBridge( + crossSwapQuotes.originSwapQuote.tokenIn.address, + crossSwapQuotes.originSwapQuote.tokenOut.address, + crossSwapQuotes.originSwapQuote.swapTx.data, + crossSwapQuotes.originSwapQuote.maximumAmountIn, + crossSwapQuotes.originSwapQuote.minAmountOut, + deposit, + { + value: crossSwapQuotes.crossSwap.isInputNative + ? crossSwapQuotes.originSwapQuote.maximumAmountIn + : 0, + } + ); + toAddress = spokePoolPeriphery.address; + } else if (entryPointContract.name === "UniversalSwapAndBridge") { + const universalSwapAndBridge = getUniversalSwapAndBridge( + entryPointContract.dex, + originChainId + ); + tx = await universalSwapAndBridge.populateTransaction.swapAndBridge( + crossSwapQuotes.originSwapQuote.tokenIn.address, + crossSwapQuotes.originSwapQuote.tokenOut.address, + crossSwapQuotes.originSwapQuote.swapTx.data, + crossSwapQuotes.originSwapQuote.maximumAmountIn, + crossSwapQuotes.originSwapQuote.minAmountOut, + deposit + ); + toAddress = universalSwapAndBridge.address; + } else { + throw new Error( + `Could not build cross swap tx for unknown entry point contract` + ); + } } else { tx = await spokePool.populateTransaction.depositV3( deposit.depositor, diff --git a/api/_dexes/types.ts b/api/_dexes/types.ts index 8eab02695..e69fc1418 100644 --- a/api/_dexes/types.ts +++ b/api/_dexes/types.ts @@ -1,6 +1,6 @@ import { BigNumber } from "ethers"; import { getSuggestedFees } from "../_utils"; -import { AmountType, CrossSwapType, LeftoverType } from "./cross-swap"; +import { AmountType, CrossSwapType } from "./cross-swap"; export type { AmountType, CrossSwapType }; @@ -31,7 +31,6 @@ export type CrossSwap = { recipient: string; slippageTolerance: number; type: AmountType; - leftoverType?: LeftoverType; refundOnOrigin: boolean; refundAddress?: string; isInputNative?: boolean; @@ -77,10 +76,21 @@ export type CrossSwapQuotes = { }; destinationSwapQuote?: SwapQuote; originSwapQuote?: SwapQuote & { - peripheryAddress: string; + entryPointContract: OriginSwapEntryPointContract; }; }; +export type OriginSwapEntryPointContract = + | { + name: "SpokePoolPeriphery"; + address: string; + } + | { + name: "UniversalSwapAndBridge"; + address: string; + dex: SupportedDex; + }; + export type CrossSwapQuotesWithFees = CrossSwapQuotes & { fees: CrossSwapFees; }; diff --git a/api/_dexes/uniswap/quote-resolver.ts b/api/_dexes/uniswap/quote-resolver.ts index b4506573e..b4f3733b6 100644 --- a/api/_dexes/uniswap/quote-resolver.ts +++ b/api/_dexes/uniswap/quote-resolver.ts @@ -171,13 +171,13 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( ); } - const spokePoolPeripheryAddress = - strategy.getPeripheryAddress(originSwapChainId); + const originSwapEntryPoint = + strategy.getOriginSwapEntryPoint(originSwapChainId); const originSwap = { chainId: originSwapChainId, tokenIn: crossSwap.inputToken, tokenOut: bridgeableInputToken, - recipient: spokePoolPeripheryAddress, + recipient: originSwapEntryPoint.address, slippageTolerance: crossSwap.slippageTolerance, type: crossSwap.type, }; @@ -215,7 +215,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( destinationSwapQuote: undefined, originSwapQuote: { ...adjOriginSwapQuote, - peripheryAddress: spokePoolPeripheryAddress, + entryPointContract: originSwapEntryPoint, }, }; } @@ -341,11 +341,13 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( const multiCallHandlerAddress = getMultiCallHandlerAddress( destinationSwapChainId ); + const originSwapEntryPoint = + originStrategy.getOriginSwapEntryPoint(originSwapChainId); const originSwap = { chainId: originSwapChainId, tokenIn: crossSwap.inputToken, tokenOut: bridgeableInputToken, - recipient: originStrategy.getPeripheryAddress(originSwapChainId), + recipient: originSwapEntryPoint.address, slippageTolerance: crossSwap.slippageTolerance, type: crossSwap.type, }; @@ -418,7 +420,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( bridgeQuote, originSwapQuote: { ...adjOriginSwapQuote, - peripheryAddress: originStrategy.getPeripheryAddress(originSwapChainId), + entryPointContract: originSwapEntryPoint, }, }; } diff --git a/api/_dexes/uniswap/swap-router-02.ts b/api/_dexes/uniswap/swap-router-02.ts index 91482ac1a..094f5428b 100644 --- a/api/_dexes/uniswap/swap-router-02.ts +++ b/api/_dexes/uniswap/swap-router-02.ts @@ -5,7 +5,7 @@ import { SwapRouter } from "@uniswap/router-sdk"; import { CHAIN_IDs } from "@across-protocol/constants"; import { getLogger } from "../../_utils"; -import { Swap, SwapQuote } from "../types"; +import { OriginSwapEntryPointContract, Swap, SwapQuote } from "../types"; import { getSpokePoolPeripheryAddress } from "../../_spoke-pool-periphery"; import { addMarkupToAmount, @@ -18,6 +18,7 @@ import { UniswapClassicQuoteFromApi, } from "./trading-api"; import { RouterTradeAdapter } from "./adapter"; +import { getUniversalSwapAndBridgeAddress } from "../utils"; // Taken from here: https://docs.uniswap.org/contracts/v3/reference/deployments/ export const SWAP_ROUTER_02_ADDRESS = { @@ -32,10 +33,27 @@ export const SWAP_ROUTER_02_ADDRESS = { [CHAIN_IDs.ZORA]: "0x7De04c96BE5159c3b5CeffC82aa176dc81281557", }; -export function getSwapRouter02Strategy(): UniswapQuoteFetchStrategy { +export function getSwapRouter02Strategy( + originSwapEntryPointContractName: OriginSwapEntryPointContract["name"] +): UniswapQuoteFetchStrategy { const getRouterAddress = (chainId: number) => SWAP_ROUTER_02_ADDRESS[chainId]; - const getPeripheryAddress = (chainId: number) => - getSpokePoolPeripheryAddress("uniswap-swapRouter02", chainId); + const getOriginSwapEntryPoint = (chainId: number) => { + if (originSwapEntryPointContractName === "SpokePoolPeriphery") { + return { + name: "SpokePoolPeriphery", + address: getSpokePoolPeripheryAddress("uniswap-swapRouter02", chainId), + } as const; + } else if (originSwapEntryPointContractName === "UniversalSwapAndBridge") { + return { + name: "UniversalSwapAndBridge", + address: getUniversalSwapAndBridgeAddress("uniswap", chainId), + dex: "uniswap", + } as const; + } + throw new Error( + `Unknown origin swap entry point contract '${originSwapEntryPointContractName}'` + ); + }; const fetchFn = async ( swap: Swap, @@ -127,7 +145,7 @@ export function getSwapRouter02Strategy(): UniswapQuoteFetchStrategy { return { getRouterAddress, - getPeripheryAddress, + getOriginSwapEntryPoint, fetchFn, }; } diff --git a/api/_dexes/uniswap/universal-router.ts b/api/_dexes/uniswap/universal-router.ts index b2722c5ed..bbaad27df 100644 --- a/api/_dexes/uniswap/universal-router.ts +++ b/api/_dexes/uniswap/universal-router.ts @@ -34,8 +34,11 @@ export const UNIVERSAL_ROUTER_ADDRESS = { export function getUniversalRouterStrategy(): UniswapQuoteFetchStrategy { const getRouterAddress = (chainId: number) => UNIVERSAL_ROUTER_ADDRESS[chainId]; - const getPeripheryAddress = (chainId: number) => - getSpokePoolPeripheryAddress("uniswap-universalRouter", chainId); + const getOriginSwapEntryPoint = (chainId: number) => + ({ + name: "SpokePoolPeriphery", + address: getSpokePoolPeripheryAddress("uniswap-universalRouter", chainId), + }) as const; const fetchFn = async ( swap: Swap, @@ -127,7 +130,7 @@ export function getUniversalRouterStrategy(): UniswapQuoteFetchStrategy { return { getRouterAddress, - getPeripheryAddress, + getOriginSwapEntryPoint, fetchFn, }; } diff --git a/api/_dexes/uniswap/utils.ts b/api/_dexes/uniswap/utils.ts index b85818564..2840ff4cb 100644 --- a/api/_dexes/uniswap/utils.ts +++ b/api/_dexes/uniswap/utils.ts @@ -8,7 +8,16 @@ import { Swap, SwapQuote, Token } from "../types"; export type UniswapQuoteFetchStrategy = { getRouterAddress: (chainId: number) => string; - getPeripheryAddress: (chainId: number) => string; + getOriginSwapEntryPoint: (chainId: number) => + | { + name: "SpokePoolPeriphery"; + address: string; + } + | { + name: "UniversalSwapAndBridge"; + address: string; + dex: "uniswap" | "1inch"; + }; fetchFn: UniswapQuoteFetchFn; }; export type UniswapQuoteFetchFn = ( diff --git a/api/_dexes/utils.ts b/api/_dexes/utils.ts index 6c1dfccbc..d90f95664 100644 --- a/api/_dexes/utils.ts +++ b/api/_dexes/utils.ts @@ -1,4 +1,7 @@ -import { SwapAndBridge__factory } from "@across-protocol/contracts"; +import { + SwapAndBridge__factory, + UniversalSwapAndBridge__factory, +} from "@across-protocol/contracts"; import { BigNumber, constants } from "ethers"; import { ENABLED_ROUTES, getProvider } from "../_utils"; @@ -11,15 +14,19 @@ import { } from "../_multicall-handler"; import { CrossSwap } from "./types"; +type SwapAndBridgeType = "SwapAndBridge" | "UniversalSwapAndBridge"; + export class UnsupportedDex extends Error { - constructor(dex: string) { - super(`DEX/Aggregator ${dex} not supported`); + constructor(dex: string, type: SwapAndBridgeType) { + super(`DEX/Aggregator '${dex}' not supported for '${type}'`); } } export class UnsupportedDexOnChain extends Error { - constructor(chainId: number, dex: string) { - super(`DEX/Aggregator ${dex} not supported on chain ${chainId}`); + constructor(chainId: number, dex: string, type: SwapAndBridgeType) { + super( + `DEX/Aggregator '${dex}' not supported on chain ${chainId} for '${type}'` + ); } } @@ -41,16 +48,37 @@ export const swapAndBridgeDexes = Object.keys( ENABLED_ROUTES.swapAndBridgeAddresses ); +export const universalSwapAndBridgeDexes = Object.keys( + ENABLED_ROUTES.universalSwapAndBridgeAddresses +); + export function getSwapAndBridgeAddress(dex: string, chainId: number) { - if (!_isDexSupported(dex)) { - throw new UnsupportedDex(dex); + if (!_isDexSupportedForSwapAndBridge(dex)) { + throw new UnsupportedDex(dex, "SwapAndBridge"); } const address = ( ENABLED_ROUTES.swapAndBridgeAddresses[dex] as Record )?.[chainId]; if (!address) { - throw new UnsupportedDexOnChain(chainId, dex); + throw new UnsupportedDexOnChain(chainId, dex, "SwapAndBridge"); + } + return address; +} + +export function getUniversalSwapAndBridgeAddress(dex: string, chainId: number) { + if (!_isDexSupportedForUniversalSwapAndBridge(dex)) { + throw new UnsupportedDex(dex, "UniversalSwapAndBridge"); + } + + const address = ( + ENABLED_ROUTES.universalSwapAndBridgeAddresses[dex] as Record< + string, + string + > + )?.[chainId]; + if (!address) { + throw new UnsupportedDexOnChain(chainId, dex, "UniversalSwapAndBridge"); } return address; } @@ -64,12 +92,30 @@ export function getSwapAndBridge(dex: string, chainId: number) { ); } -function _isDexSupported( +export function getUniversalSwapAndBridge(dex: string, chainId: number) { + const universalSwapAndBridgeAddress = getUniversalSwapAndBridgeAddress( + dex, + chainId + ); + + return UniversalSwapAndBridge__factory.connect( + universalSwapAndBridgeAddress, + getProvider(chainId) + ); +} + +function _isDexSupportedForSwapAndBridge( dex: string ): dex is keyof typeof ENABLED_ROUTES.swapAndBridgeAddresses { return swapAndBridgeDexes.includes(dex); } +function _isDexSupportedForUniversalSwapAndBridge( + dex: string +): dex is keyof typeof ENABLED_ROUTES.universalSwapAndBridgeAddresses { + return universalSwapAndBridgeDexes.includes(dex); +} + /** * This builds a cross-chain message for a (any/bridgeable)-to-bridgeable cross swap * with a specific amount of output tokens that the recipient will receive. Excess diff --git a/scripts/generate-routes.ts b/scripts/generate-routes.ts index ca07e01fb..125f48956 100644 --- a/scripts/generate-routes.ts +++ b/scripts/generate-routes.ts @@ -91,6 +91,7 @@ const enabledRoutes = { CHAIN_IDs.WORLD_CHAIN, ], }, + // Addresses of token-scoped `SwapAndBridge` contracts, i.e. USDC.e -> USDC swaps swapAndBridgeAddresses: { "1inch": { [CHAIN_IDs.POLYGON]: "0xaBa0F11D55C5dDC52cD0Cb2cd052B621d45159d5", @@ -104,6 +105,26 @@ const enabledRoutes = { [CHAIN_IDs.ARBITRUM]: "0xF633b72A4C2Fb73b77A379bf72864A825aD35b6D", }, }, + // Addresses of `UniversalSwapAndBridge` contracts from deployment: + // https://github.com/across-protocol/contracts/pull/731/commits/6bdbfd38f50b616ac25e49687cbac6fb6bcb543b + universalSwapAndBridgeAddresses: { + "1inch": { + [CHAIN_IDs.ARBITRUM]: "0x81C7601ac0c5825e89F967f9222B977CCD78aD77", + [CHAIN_IDs.BASE]: "0x98285D11B9F7aFec2d475805E5255f26B4490167", + [CHAIN_IDs.OPTIMISM]: "0x7631eA29479Ee265241F13FB48555A2C886d3Bf8", + [CHAIN_IDs.POLYGON]: "0xc2dcb88873e00c9d401de2cbba4c6a28f8a6e2c2", + }, + uniswap: { + [CHAIN_IDs.ARBITRUM]: "0x2414A759d4EFF700Ad81e257Ab5187d07eCeEbAb", + [CHAIN_IDs.BASE]: "0xed8b9c9aE7aCEf12eb4650d26Eb876005a4752d2", + [CHAIN_IDs.BLAST]: "0x57EE47829369e2EF62fBb423648bec70d0366204", + [CHAIN_IDs.MAINNET]: "0x0e84f089B0923EfeA51C6dF91581BFBa66A3484A", + [CHAIN_IDs.OPTIMISM]: "0x04989eaF03547E6583f9d9e42aeD11D2b78A808b", + [CHAIN_IDs.POLYGON]: "0xa55490E20057BD4775618D0FC8D51F59f602FED0", + [CHAIN_IDs.WORLD_CHAIN]: "0x56e2d1b8C7dE8D11B282E1b4C924C32D91f9102B", + [CHAIN_IDs.ZORA]: "0x75b84707e6Bf5bc48DbC3AD883c23192C869AAE4", + }, + }, spokePoolPeripheryAddresses: { "uniswap-swapRouter02": { [CHAIN_IDs.POLYGON]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", @@ -148,9 +169,12 @@ const enabledRoutes = { "0x17496824Ba574A4e9De80110A91207c4c63e552a", // Mocked }, }, - spokePoolPeripheryAddresses: { + universalSwapAndBridgeAddresses: { uniswap: {}, }, + spokePoolPeripheryAddresses: { + "uniswap-universalRouter": {}, + }, routes: transformChainConfigs(enabledSepoliaChainConfigs), }, } as const; @@ -373,35 +397,20 @@ async function generateRoutes(hubPoolChainId = 1) { ), merkleDistributorAddress: utils.getAddress(config.merkleDistributorAddress), claimAndStakeAddress: utils.getAddress(config.claimAndStakeAddress), - swapAndBridgeAddresses: Object.entries( - config.swapAndBridgeAddresses - ).reduce( - (acc, [key, value]) => ({ - ...acc, - [key]: Object.entries(value).reduce( - (acc, [chainId, address]) => ({ - ...acc, - [chainId]: utils.getAddress(address as string), - }), - {} - ), - }), - {} + swapAndBridgeAddresses: checksumAddressesOfNestedMap( + config.swapAndBridgeAddresses as Record> ), - spokePoolPeripheryAddresses: Object.entries( - config.spokePoolPeripheryAddresses - ).reduce( - (acc, [key, value]) => ({ - ...acc, - [key]: Object.entries(value).reduce( - (acc, [chainId, address]) => ({ - ...acc, - [chainId]: utils.getAddress(address as string), - }), - {} - ), - }), - {} + universalSwapAndBridgeAddresses: checksumAddressesOfNestedMap( + config.universalSwapAndBridgeAddresses as Record< + string, + Record + > + ), + spokePoolPeripheryAddresses: checksumAddressesOfNestedMap( + config.spokePoolPeripheryAddresses as Record< + string, + Record + > ), routes: config.routes.flatMap((route) => transformBridgeRoute(route, config.hubPoolChain) @@ -656,4 +665,22 @@ function getBridgedUsdcSymbol(chainId: number) { } } +function checksumAddressesOfNestedMap( + nestedMap: Record> +) { + return Object.entries(nestedMap).reduce( + (acc, [key, value]) => ({ + ...acc, + [key]: Object.entries(value).reduce( + (acc, [chainId, address]) => ({ + ...acc, + [chainId]: utils.getAddress(address as string), + }), + {} + ), + }), + {} + ); +} + generateRoutes(Number(process.argv[2])); diff --git a/src/data/routes_11155111_0x14224e63716afAcE30C9a417E0542281869f7d9e.json b/src/data/routes_11155111_0x14224e63716afAcE30C9a417E0542281869f7d9e.json index 5d7264fa4..c70c802f3 100644 --- a/src/data/routes_11155111_0x14224e63716afAcE30C9a417E0542281869f7d9e.json +++ b/src/data/routes_11155111_0x14224e63716afAcE30C9a417E0542281869f7d9e.json @@ -13,9 +13,12 @@ "11155420": "0x17496824Ba574A4e9De80110A91207c4c63e552a" } }, - "spokePoolPeripheryAddresses": { + "universalSwapAndBridgeAddresses": { "uniswap": {} }, + "spokePoolPeripheryAddresses": { + "uniswap-universalRouter": {} + }, "routes": [ { "fromChain": 11155111, diff --git a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json index 63b16f428..ed6a01693 100644 --- a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json +++ b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json @@ -20,6 +20,24 @@ "42161": "0xF633b72A4C2Fb73b77A379bf72864A825aD35b6D" } }, + "universalSwapAndBridgeAddresses": { + "1inch": { + "10": "0x7631eA29479Ee265241F13FB48555A2C886d3Bf8", + "137": "0xC2dCB88873E00c9d401De2CBBa4C6A28f8A6e2c2", + "8453": "0x98285D11B9F7aFec2d475805E5255f26B4490167", + "42161": "0x81C7601ac0c5825e89F967f9222B977CCD78aD77" + }, + "uniswap": { + "1": "0x0e84f089B0923EfeA51C6dF91581BFBa66A3484A", + "10": "0x04989eaF03547E6583f9d9e42aeD11D2b78A808b", + "137": "0xa55490E20057BD4775618D0FC8D51F59f602FED0", + "480": "0x56e2d1b8C7dE8D11B282E1b4C924C32D91f9102B", + "8453": "0xed8b9c9aE7aCEf12eb4650d26Eb876005a4752d2", + "42161": "0x2414A759d4EFF700Ad81e257Ab5187d07eCeEbAb", + "81457": "0x57EE47829369e2EF62fBb423648bec70d0366204", + "7777777": "0x75b84707e6Bf5bc48DbC3AD883c23192C869AAE4" + } + }, "spokePoolPeripheryAddresses": { "uniswap-swapRouter02": { "1": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", From 5eebd02810b5e377ba0c080c9676ed0e0db3100e Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Mon, 16 Dec 2024 19:21:34 -0600 Subject: [PATCH 30/47] chore: add swap quotes to response --- api/swap/allowance.ts | 70 +++++++++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/api/swap/allowance.ts b/api/swap/allowance.ts index f5cbc9b69..43c0013cd 100644 --- a/api/swap/allowance.ts +++ b/api/swap/allowance.ts @@ -45,18 +45,20 @@ const handler = async ( integratorId ); - const originChainId = crossSwapQuotes.crossSwap.inputToken.chainId; + const { originSwapQuote, bridgeQuote, destinationSwapQuote, crossSwap } = + crossSwapQuotes; + + const originChainId = crossSwap.inputToken.chainId; const inputTokenAddress = isInputNative ? constants.AddressZero - : crossSwapQuotes.crossSwap.inputToken.address; + : crossSwap.inputToken.address; const inputAmount = - crossSwapQuotes.originSwapQuote?.maximumAmountIn || - crossSwapQuotes.bridgeQuote.inputAmount; + originSwapQuote?.maximumAmountIn || bridgeQuote.inputAmount; const { allowance, balance } = await getBalanceAndAllowance({ chainId: originChainId, tokenAddress: inputTokenAddress, - owner: crossSwapQuotes.crossSwap.depositor, + owner: crossSwap.depositor, spender: crossSwapTx.to, }); @@ -72,7 +74,7 @@ const handler = async ( [originTxGas, originTxGasPrice] = await Promise.all([ provider.estimateGas({ ...crossSwapTx, - from: crossSwapQuotes.crossSwap.depositor, + from: crossSwap.depositor, }), latestGasPriceCache(originChainId).get(), ]); @@ -91,15 +93,15 @@ const handler = async ( const approvalAmount = constants.MaxUint256; if (allowance.lt(inputAmount)) { approvalTxns = getApprovalTxns({ - token: crossSwapQuotes.crossSwap.inputToken, + token: crossSwap.inputToken, spender: crossSwapTx.to, amount: approvalAmount, }); } - const refundToken = crossSwapQuotes.crossSwap.refundOnOrigin - ? crossSwapQuotes.bridgeQuote.inputToken - : crossSwapQuotes.bridgeQuote.outputToken; + const refundToken = crossSwap.refundOnOrigin + ? bridgeQuote.inputToken + : bridgeQuote.outputToken; const responseJson = { // fees: crossSwapQuotes.fees, @@ -117,6 +119,34 @@ const handler = async ( }, }, approvalTxns, + quotes: { + 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, @@ -134,21 +164,17 @@ const handler = async ( } : refundToken, inputAmount: - crossSwapQuotes.originSwapQuote?.expectedAmountIn.toString() ?? - crossSwapQuotes.bridgeQuote.inputAmount.toString(), + originSwapQuote?.expectedAmountIn.toString() ?? + bridgeQuote.inputAmount.toString(), expectedOutputAmount: - crossSwapQuotes.destinationSwapQuote?.expectedAmountOut.toString() ?? - crossSwapQuotes.bridgeQuote.outputAmount.toString(), + destinationSwapQuote?.expectedAmountOut.toString() ?? + bridgeQuote.outputAmount.toString(), minOutputAmount: - crossSwapQuotes.destinationSwapQuote?.minAmountOut.toString() ?? - crossSwapQuotes.bridgeQuote.outputAmount.toString(), - expectedFillTime: - crossSwapQuotes.bridgeQuote.suggestedFees.estimatedFillTimeSec, + destinationSwapQuote?.minAmountOut.toString() ?? + bridgeQuote.outputAmount.toString(), + expectedFillTime: bridgeQuote.suggestedFees.estimatedFillTimeSec, }; - mark.stop({ - crossSwapQuotes, - request, - }); + mark.stop(); logger.debug({ at: "Swap/allowance", message: "Response data", From 0a148539db639a09d5a6b4433185b0790cf934e0 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Mon, 16 Dec 2024 20:04:28 -0600 Subject: [PATCH 31/47] perf: bridge quote for min output --- api/_utils.ts | 54 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/api/_utils.ts b/api/_utils.ts index 7af9e511e..ec3b9b269 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -84,6 +84,7 @@ import { TokenNotFoundError, } from "./_errors"; import { Token } from "./_dexes/types"; +import { addMarkupToAmount } from "./_dexes/uniswap/utils"; export { InputError, handleErrorCondition } from "./_errors"; export const { Profiler } = sdk.utils; @@ -897,6 +898,8 @@ export async function getBridgeQuoteForMinOutput(params: { recipient?: string; message?: string; }) { + const maxTries = 3; + const tryChunkSize = 3; const baseParams = { inputToken: params.inputToken.address, outputToken: params.outputToken.address, @@ -911,7 +914,7 @@ export async function getBridgeQuoteForMinOutput(params: { // 1. Use the suggested fees to get an indicative quote with // input amount equal to minOutputAmount let tries = 0; - let adjustedInputAmount = params.minOutputAmount; + let adjustedInputAmount = addMarkupToAmount(params.minOutputAmount, 0.005); let indicativeQuote = await getSuggestedFees({ ...baseParams, amount: adjustedInputAmount.toString(), @@ -921,27 +924,44 @@ export async function getBridgeQuoteForMinOutput(params: { undefined; // 2. Adjust input amount to meet minOutputAmount - while (tries < 3) { - adjustedInputAmount = adjustedInputAmount - .mul(utils.parseEther("1").add(adjustmentPct)) - .div(sdk.utils.fixedPointAdjustment); - const adjustedQuote = await getSuggestedFees({ - ...baseParams, - amount: adjustedInputAmount.toString(), + while (tries < maxTries) { + const inputAmounts = Array.from({ length: tryChunkSize }).map((_, i) => { + const buffer = 0.001 * i; + return addMarkupToAmount( + adjustedInputAmount + .mul(utils.parseEther("1").add(adjustmentPct)) + .div(sdk.utils.fixedPointAdjustment), + buffer + ); }); - const outputAmount = adjustedInputAmount.sub( - adjustedInputAmount - .mul(adjustedQuote.totalRelayFee.pct) - .div(sdk.utils.fixedPointAdjustment) + const quotes = await Promise.all( + inputAmounts.map((inputAmount) => { + return getSuggestedFees({ + ...baseParams, + amount: inputAmount.toString(), + }); + }) ); - if (outputAmount.gte(params.minOutputAmount)) { - finalQuote = adjustedQuote; + for (const [i, quote] of Object.entries(quotes)) { + const inputAmount = inputAmounts[Number(i)]; + const outputAmount = inputAmount.sub( + inputAmount + .mul(quote.totalRelayFee.pct) + .div(sdk.utils.fixedPointAdjustment) + ); + if (outputAmount.gte(params.minOutputAmount)) { + finalQuote = quote; + break; + } + } + + if (finalQuote) { break; - } else { - adjustmentPct = adjustedQuote.totalRelayFee.pct; - tries++; } + + adjustedInputAmount = inputAmounts[inputAmounts.length - 1]; + tries++; } if (!finalQuote) { From 83dc8c95a1f12bc85e5b4fba6d759e27d65ba0e4 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Mon, 16 Dec 2024 20:14:12 -0600 Subject: [PATCH 32/47] chore: improve profiling --- api/_dexes/cross-swap.ts | 29 +--- api/_dexes/uniswap/quote-resolver.ts | 198 ++++++++++++++++----------- api/swap/_utils.ts | 37 ++--- 3 files changed, 136 insertions(+), 128 deletions(-) diff --git a/api/_dexes/cross-swap.ts b/api/_dexes/cross-swap.ts index 9bbad0e9d..4d6d0e655 100644 --- a/api/_dexes/cross-swap.ts +++ b/api/_dexes/cross-swap.ts @@ -8,7 +8,6 @@ import { isOutputTokenBridgeable, getBridgeQuoteForMinOutput, getSpokePool, - Profiler, } from "../_utils"; import { getBestUniswapCrossSwapQuotesForOutputA2A, @@ -74,10 +73,6 @@ export async function getCrossSwapQuotes( } export async function getCrossSwapQuotesForOutput(crossSwap: CrossSwap) { - const profiler = new Profiler({ - at: "api/cross-swap#getCrossSwapQuotesForOutput", - logger: console, - }); const crossSwapType = getCrossSwapType({ inputToken: crossSwap.inputToken.address, originChainId: crossSwap.inputToken.chainId, @@ -87,35 +82,19 @@ export async function getCrossSwapQuotesForOutput(crossSwap: CrossSwap) { }); if (crossSwapType === CROSS_SWAP_TYPE.BRIDGEABLE_TO_BRIDGEABLE) { - return profiler.measureAsync( - getCrossSwapQuotesForOutputB2B(crossSwap), - "getCrossSwapQuotesForOutputB2B", - crossSwap - ); + return getCrossSwapQuotesForOutputB2B(crossSwap); } if (crossSwapType === CROSS_SWAP_TYPE.BRIDGEABLE_TO_ANY) { - return profiler.measureAsync( - getCrossSwapQuotesForOutputB2A(crossSwap), - "getCrossSwapQuotesForOutputB2A", - crossSwap - ); + return getCrossSwapQuotesForOutputB2A(crossSwap); } if (crossSwapType === CROSS_SWAP_TYPE.ANY_TO_BRIDGEABLE) { - return profiler.measureAsync( - getCrossSwapQuotesForOutputA2B(crossSwap), - "getCrossSwapQuotesForOutputA2B", - crossSwap - ); + return getCrossSwapQuotesForOutputA2B(crossSwap); } if (crossSwapType === CROSS_SWAP_TYPE.ANY_TO_ANY) { - return profiler.measureAsync( - getCrossSwapQuotesForOutputA2A(crossSwap), - "getCrossSwapQuotesForOutputA2A", - crossSwap - ); + return getCrossSwapQuotesForOutputA2A(crossSwap); } throw new Error("Invalid cross swap type"); diff --git a/api/_dexes/uniswap/quote-resolver.ts b/api/_dexes/uniswap/quote-resolver.ts index b4f3733b6..0ce219063 100644 --- a/api/_dexes/uniswap/quote-resolver.ts +++ b/api/_dexes/uniswap/quote-resolver.ts @@ -7,6 +7,7 @@ import { getBridgeQuoteForMinOutput, getRoutesByChainIds, getRouteByOutputTokenAndOriginChain, + Profiler, } from "../../_utils"; import { buildMulticallHandlerMessage, @@ -42,6 +43,10 @@ export async function getUniswapCrossSwapQuotesForOutputB2A( crossSwap: CrossSwap, strategy: UniswapQuoteFetchStrategy ): Promise { + const profiler = new Profiler({ + at: "api/_dexes/uniswap/quote-resolver#getUniswapCrossSwapQuotesForOutputB2A", + logger: console, + }); const destinationSwapChainId = crossSwap.outputToken.chainId; const bridgeRoute = getRouteByInputTokenAndDestinationChain( crossSwap.inputToken.address, @@ -83,27 +88,33 @@ export async function getUniswapCrossSwapQuotesForOutputB2A( }; // 1. Get destination swap quote for bridgeable output token -> any token // with exact output amount. - let destinationSwapQuote = await strategy.fetchFn( - { - ...destinationSwap, - amount: crossSwap.amount.toString(), - }, - TradeType.EXACT_OUTPUT + let destinationSwapQuote = await profiler.measureAsync( + strategy.fetchFn( + { + ...destinationSwap, + amount: crossSwap.amount.toString(), + }, + TradeType.EXACT_OUTPUT + ), + "getDestinationSwapQuote" ); // 2. Get bridge quote for bridgeable input token -> bridgeable output token - const bridgeQuote = await getBridgeQuoteForMinOutput({ - inputToken: crossSwap.inputToken, - outputToken: bridgeableOutputToken, - minOutputAmount: destinationSwapQuote.maximumAmountIn, - recipient: getMultiCallHandlerAddress(destinationSwapChainId), - message: buildDestinationSwapCrossChainMessage({ - crossSwap, - destinationSwapQuote, - bridgeableOutputToken, - routerAddress: strategy.getRouterAddress(destinationSwapChainId), + const bridgeQuote = await profiler.measureAsync( + getBridgeQuoteForMinOutput({ + inputToken: crossSwap.inputToken, + outputToken: bridgeableOutputToken, + minOutputAmount: destinationSwapQuote.maximumAmountIn, + recipient: getMultiCallHandlerAddress(destinationSwapChainId), + message: buildDestinationSwapCrossChainMessage({ + crossSwap, + destinationSwapQuote, + bridgeableOutputToken, + routerAddress: strategy.getRouterAddress(destinationSwapChainId), + }), }), - }); + "getBridgeQuote" + ); return { crossSwap, @@ -123,6 +134,10 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( crossSwap: CrossSwap, strategy: UniswapQuoteFetchStrategy ): Promise { + const profiler = new Profiler({ + at: "api/_dexes/uniswap/quote-resolver#getUniswapCrossSwapQuotesForOutputA2B", + logger: console, + }); const originSwapChainId = crossSwap.inputToken.chainId; const destinationChainId = crossSwap.outputToken.chainId; const bridgeRoute = getRouteByOutputTokenAndOriginChain( @@ -156,13 +171,16 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( }; // 1. Get bridge quote for bridgeable input token -> bridgeable output token - const bridgeQuote = await getBridgeQuoteForMinOutput({ - inputToken: bridgeableInputToken, - outputToken: crossSwap.outputToken, - minOutputAmount: crossSwap.amount, - recipient: getMultiCallHandlerAddress(destinationChainId), - message: buildExactOutputBridgeTokenMessage(crossSwap), - }); + const bridgeQuote = await profiler.measureAsync( + getBridgeQuoteForMinOutput({ + inputToken: bridgeableInputToken, + outputToken: crossSwap.outputToken, + minOutputAmount: crossSwap.amount, + recipient: getMultiCallHandlerAddress(destinationChainId), + message: buildExactOutputBridgeTokenMessage(crossSwap), + }), + "getBridgeQuote" + ); // 1.1. Update bridge quote message for min. output amount if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT && crossSwap.isOutputNative) { bridgeQuote.message = buildMinOutputBridgeTokenMessage( @@ -182,27 +200,33 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( type: crossSwap.type, }; // 2.1. Get origin swap quote for any input token -> bridgeable input token - const originSwapQuote = await strategy.fetchFn( - { - ...originSwap, - amount: bridgeQuote.inputAmount.toString(), - }, - TradeType.EXACT_OUTPUT, - { - useIndicativeQuote: true, - } + const originSwapQuote = await profiler.measureAsync( + strategy.fetchFn( + { + ...originSwap, + amount: bridgeQuote.inputAmount.toString(), + }, + TradeType.EXACT_OUTPUT, + { + useIndicativeQuote: true, + } + ), + "INDICATIVE_getOriginSwapQuote" ); // 2.2. Re-fetch origin swap quote with updated input amount and EXACT_INPUT type. // This prevents leftover tokens in the SwapAndBridge contract. - let adjOriginSwapQuote = await strategy.fetchFn( - { - ...originSwap, - amount: addMarkupToAmount( - originSwapQuote.maximumAmountIn, - indicativeQuoteBuffer - ).toString(), - }, - TradeType.EXACT_INPUT + let adjOriginSwapQuote = await profiler.measureAsync( + strategy.fetchFn( + { + ...originSwap, + amount: addMarkupToAmount( + originSwapQuote.maximumAmountIn, + indicativeQuoteBuffer + ).toString(), + }, + TradeType.EXACT_INPUT + ), + "getOriginSwapQuote" ); assertMinOutputAmount( adjOriginSwapQuote.minAmountOut, @@ -302,6 +326,10 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( originStrategy: UniswapQuoteFetchStrategy, destinationStrategy: UniswapQuoteFetchStrategy ): Promise { + const profiler = new Profiler({ + at: "api/_dexes/uniswap/quote-resolver#getUniswapCrossSwapQuotesForOutputA2A", + logger: console, + }); const originSwapChainId = crossSwap.inputToken.chainId; const destinationSwapChainId = crossSwap.outputToken.chainId; @@ -362,52 +390,64 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( // 1. Get destination swap quote for bridgeable output token -> any token // with exact output amount - let destinationSwapQuote = await destinationStrategy.fetchFn( - { - ...destinationSwap, - amount: crossSwap.amount.toString(), - }, - TradeType.EXACT_OUTPUT + let destinationSwapQuote = await profiler.measureAsync( + destinationStrategy.fetchFn( + { + ...destinationSwap, + amount: crossSwap.amount.toString(), + }, + TradeType.EXACT_OUTPUT + ), + "getDestinationSwapQuote" ); // 2. Get bridge quote for bridgeable input token -> bridgeable output token - const bridgeQuote = await getBridgeQuoteForMinOutput({ - inputToken: bridgeableInputToken, - outputToken: bridgeableOutputToken, - minOutputAmount: destinationSwapQuote.maximumAmountIn, - recipient: getMultiCallHandlerAddress(destinationSwapChainId), - message: buildDestinationSwapCrossChainMessage({ - crossSwap, - destinationSwapQuote, - bridgeableOutputToken, - routerAddress: destinationStrategy.getRouterAddress( - destinationSwapChainId - ), + const bridgeQuote = await profiler.measureAsync( + getBridgeQuoteForMinOutput({ + inputToken: bridgeableInputToken, + outputToken: bridgeableOutputToken, + minOutputAmount: destinationSwapQuote.maximumAmountIn, + recipient: getMultiCallHandlerAddress(destinationSwapChainId), + message: buildDestinationSwapCrossChainMessage({ + crossSwap, + destinationSwapQuote, + bridgeableOutputToken, + routerAddress: destinationStrategy.getRouterAddress( + destinationSwapChainId + ), + }), }), - }); + "getBridgeQuote" + ); // 3.1. Get origin swap quote for any input token -> bridgeable input token - const originSwapQuote = await originStrategy.fetchFn( - { - ...originSwap, - amount: bridgeQuote.inputAmount.toString(), - }, - TradeType.EXACT_OUTPUT, - { - useIndicativeQuote: true, - } + const originSwapQuote = await profiler.measureAsync( + originStrategy.fetchFn( + { + ...originSwap, + amount: bridgeQuote.inputAmount.toString(), + }, + TradeType.EXACT_OUTPUT, + { + useIndicativeQuote: true, + } + ), + "INDICATIVE_getOriginSwapQuote" ); // 3.2. Re-fetch origin swap quote with updated input amount and EXACT_INPUT type. // This prevents leftover tokens in the SwapAndBridge contract. - let adjOriginSwapQuote = await originStrategy.fetchFn( - { - ...originSwap, - amount: addMarkupToAmount( - originSwapQuote.maximumAmountIn, - indicativeQuoteBuffer - ).toString(), - }, - TradeType.EXACT_INPUT + let adjOriginSwapQuote = await profiler.measureAsync( + originStrategy.fetchFn( + { + ...originSwap, + amount: addMarkupToAmount( + originSwapQuote.maximumAmountIn, + indicativeQuoteBuffer + ).toString(), + }, + TradeType.EXACT_INPUT + ), + "getOriginSwapQuote" ); assertMinOutputAmount( adjOriginSwapQuote.minAmountOut, diff --git a/api/swap/_utils.ts b/api/swap/_utils.ts index eb92683e7..bde17a64a 100644 --- a/api/swap/_utils.ts +++ b/api/swap/_utils.ts @@ -10,7 +10,6 @@ import { getCachedTokenInfo, getWrappedNativeTokenAddress, getCachedTokenPrice, - Profiler, } from "../_utils"; import { AMOUNT_TYPE, @@ -49,10 +48,6 @@ export type BaseSwapQueryParams = Infer; export async function handleBaseSwapQueryParams({ query, }: TypedVercelRequest) { - const profiler = new Profiler({ - at: "api/_utils#handleBaseSwapQueryParams", - logger: console, - }); assert(query, BaseSwapQueryParamsSchema); const { @@ -121,25 +116,19 @@ export async function handleBaseSwapQueryParams({ ]); // 2. Get swap quotes and calldata based on the swap type - const crossSwapQuotes = await profiler.measureAsync( - getCrossSwapQuotes({ - amount, - inputToken, - outputToken, - depositor, - recipient: recipient || depositor, - slippageTolerance: Number(slippageTolerance), - type: amountType, - refundOnOrigin, - refundAddress, - isInputNative, - isOutputNative, - }), - "getCrossSwapQuotes", - { - swapType: amountType, - } - ); + const crossSwapQuotes = await getCrossSwapQuotes({ + amount, + inputToken, + outputToken, + depositor, + recipient: recipient || depositor, + slippageTolerance: Number(slippageTolerance), + type: amountType, + refundOnOrigin, + refundAddress, + isInputNative, + isOutputNative, + }); // 3. Calculate fees based for full route // const fees = await calculateCrossSwapFees(crossSwapQuotes); From a0afc7e38ccc3e5df72f08443ef2319f0eb462fe Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Tue, 17 Dec 2024 19:42:54 +0100 Subject: [PATCH 33/47] fix: bridge quote min output --- api/_utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/api/_utils.ts b/api/_utils.ts index ec3b9b269..299264c97 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -952,6 +952,7 @@ export async function getBridgeQuoteForMinOutput(params: { ); if (outputAmount.gte(params.minOutputAmount)) { finalQuote = quote; + adjustedInputAmount = inputAmount; break; } } From 28b66489aa3433063242e0daab143745aa234020 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Tue, 17 Dec 2024 19:43:41 +0100 Subject: [PATCH 34/47] chore: allow vercel endpoint override --- api/_utils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/_utils.ts b/api/_utils.ts index 299264c97..ded372730 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -203,6 +203,9 @@ export const getLogger = (): LoggingUtility => { * @returns A valid URL of the current endpoint in vercel */ export const resolveVercelEndpoint = () => { + if (process.env.REACT_APP_VERCEL_API_BASE_URL_OVERRIDE) { + return process.env.REACT_APP_VERCEL_API_BASE_URL_OVERRIDE; + } const url = process.env.VERCEL_URL ?? "across.to"; const env = process.env.VERCEL_ENV ?? "development"; switch (env) { From 13839807c72a68f36dcf0a35dbcf133f59c99306 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Fri, 20 Dec 2024 12:52:07 +0100 Subject: [PATCH 35/47] perf: use indicative quotes as guesstimates (#1337) * perf: only use indicative quotes * add profiling * add caching of indicative quotes * fixup * fixup * fixup * fixup * add more profiling --- api/_dexes/uniswap/quote-resolver.ts | 199 +++++++++++++++++++-------- api/_dexes/uniswap/swap-router-02.ts | 68 +++++++-- api/_dexes/uniswap/trading-api.ts | 4 + 3 files changed, 203 insertions(+), 68 deletions(-) diff --git a/api/_dexes/uniswap/quote-resolver.ts b/api/_dexes/uniswap/quote-resolver.ts index 0ce219063..e1bfae60a 100644 --- a/api/_dexes/uniswap/quote-resolver.ts +++ b/api/_dexes/uniswap/quote-resolver.ts @@ -34,10 +34,12 @@ import { UniswapQuoteFetchStrategy, addMarkupToAmount } from "./utils"; const indicativeQuoteBuffer = 0.005; // 0.5% buffer for indicative quotes /** - * Returns Uniswap v3 quote for a swap with min. output amount for route - * BRIDGEABLE input token -> ANY output token, e.g. USDC -> ARB. Required steps: + * Returns Uniswap v3 quote for a swap based on the output amount (exact or minimal) for + * route BRIDGEABLE input token -> ANY output token, e.g. USDC -> ARB. Required steps: * 1. Get destination swap quote for bridgeable output token -> any token * 2. Get bridge quote for bridgeable input token -> bridgeable output token + * @param crossSwap - Cross swap params + * @param strategy - Uniswap quote fetch strategy */ export async function getUniswapCrossSwapQuotesForOutputB2A( crossSwap: CrossSwap, @@ -86,36 +88,64 @@ export async function getUniswapCrossSwapQuotesForOutputB2A( slippageTolerance: crossSwap.slippageTolerance, type: crossSwap.type, }; - // 1. Get destination swap quote for bridgeable output token -> any token - // with exact output amount. - let destinationSwapQuote = await profiler.measureAsync( + // 1. Get INDICATIVE destination swap quote for bridgeable output token -> any token + // with exact output amount. This request is faster but does not contain calldata. + const indicativeDestinationSwapQuote = await profiler.measureAsync( strategy.fetchFn( { ...destinationSwap, amount: crossSwap.amount.toString(), }, - TradeType.EXACT_OUTPUT + TradeType.EXACT_OUTPUT, + { + useIndicativeQuote: true, + } ), - "getDestinationSwapQuote" + "INDICATIVE_getDestinationSwapQuote" ); - // 2. Get bridge quote for bridgeable input token -> bridgeable output token - const bridgeQuote = await profiler.measureAsync( - getBridgeQuoteForMinOutput({ - inputToken: crossSwap.inputToken, - outputToken: bridgeableOutputToken, - minOutputAmount: destinationSwapQuote.maximumAmountIn, - recipient: getMultiCallHandlerAddress(destinationSwapChainId), - message: buildDestinationSwapCrossChainMessage({ - crossSwap, - destinationSwapQuote, - bridgeableOutputToken, - routerAddress: strategy.getRouterAddress(destinationSwapChainId), + // 2. Fetch REAL destination swap quote and bridge quote in parallel to improve performance. + const [destinationSwapQuote, bridgeQuote] = await profiler.measureAsync( + Promise.all([ + // 2.1. REAL destination swap quote for bridgeable output token -> any token. + // Quote contains calldata. + strategy.fetchFn( + { + ...destinationSwap, + amount: crossSwap.amount.toString(), + }, + TradeType.EXACT_OUTPUT + ), + // 2.2. Bridge quote for bridgeable input token -> bridgeable output token based on + // indicative destination swap quote. + getBridgeQuoteForMinOutput({ + inputToken: crossSwap.inputToken, + outputToken: bridgeableOutputToken, + minOutputAmount: indicativeDestinationSwapQuote.maximumAmountIn, + recipient: getMultiCallHandlerAddress(destinationSwapChainId), + message: buildDestinationSwapCrossChainMessage({ + crossSwap, + destinationSwapQuote: indicativeDestinationSwapQuote, + bridgeableOutputToken, + routerAddress: strategy.getRouterAddress(destinationSwapChainId), + }), }), - }), - "getBridgeQuote" + ]), + "getAllQuotes" + ); + assertMinOutputAmount(destinationSwapQuote.minAmountOut, crossSwap.amount); + assertMinOutputAmount( + bridgeQuote.outputAmount, + destinationSwapQuote.maximumAmountIn ); + bridgeQuote.message = buildDestinationSwapCrossChainMessage({ + crossSwap, + destinationSwapQuote, + bridgeableOutputToken, + routerAddress: strategy.getRouterAddress(destinationSwapChainId), + }); + return { crossSwap, bridgeQuote, @@ -388,44 +418,52 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( type: crossSwap.type, }; - // 1. Get destination swap quote for bridgeable output token -> any token - // with exact output amount - let destinationSwapQuote = await profiler.measureAsync( + // Fetch INDICATIVE quotes sequentially: + // 1. Destination swap quote for bridgeable output token -> any token + // 2. Bridge quote for bridgeable input token -> bridgeable output token + // 3. Origin swap quote for any input token -> bridgeable input token + // These requests are faster but do not contain calldata. + const indicativeDestinationSwapQuote = await profiler.measureAsync( destinationStrategy.fetchFn( { ...destinationSwap, amount: crossSwap.amount.toString(), }, - TradeType.EXACT_OUTPUT + TradeType.EXACT_OUTPUT, + { + useIndicativeQuote: true, + } ), - "getDestinationSwapQuote" + "INDICATIVE_getDestinationSwapQuote" ); - - // 2. Get bridge quote for bridgeable input token -> bridgeable output token - const bridgeQuote = await profiler.measureAsync( + const indicativeBridgeQuote = await profiler.measureAsync( getBridgeQuoteForMinOutput({ inputToken: bridgeableInputToken, outputToken: bridgeableOutputToken, - minOutputAmount: destinationSwapQuote.maximumAmountIn, + minOutputAmount: addMarkupToAmount( + indicativeDestinationSwapQuote.maximumAmountIn, + indicativeQuoteBuffer + ), recipient: getMultiCallHandlerAddress(destinationSwapChainId), message: buildDestinationSwapCrossChainMessage({ crossSwap, - destinationSwapQuote, + destinationSwapQuote: indicativeDestinationSwapQuote, bridgeableOutputToken, routerAddress: destinationStrategy.getRouterAddress( destinationSwapChainId ), }), }), - "getBridgeQuote" + "INDICATIVE_getBridgeQuote" ); - - // 3.1. Get origin swap quote for any input token -> bridgeable input token - const originSwapQuote = await profiler.measureAsync( + const indicativeOriginSwapQuote = await profiler.measureAsync( originStrategy.fetchFn( { ...originSwap, - amount: bridgeQuote.inputAmount.toString(), + amount: addMarkupToAmount( + indicativeBridgeQuote.inputAmount, + indicativeQuoteBuffer + ).toString(), }, TradeType.EXACT_OUTPUT, { @@ -434,32 +472,65 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( ), "INDICATIVE_getOriginSwapQuote" ); - // 3.2. Re-fetch origin swap quote with updated input amount and EXACT_INPUT type. - // This prevents leftover tokens in the SwapAndBridge contract. - let adjOriginSwapQuote = await profiler.measureAsync( - originStrategy.fetchFn( - { - ...originSwap, - amount: addMarkupToAmount( - originSwapQuote.maximumAmountIn, - indicativeQuoteBuffer - ).toString(), - }, - TradeType.EXACT_INPUT - ), - "getOriginSwapQuote" - ); + + // Fetch REAL quotes in parallel. These requests are slower but contain calldata. + const [destinationSwapQuote, bridgeQuote, originSwapQuote] = + await profiler.measureAsync( + Promise.all([ + destinationStrategy.fetchFn( + { + ...destinationSwap, + amount: crossSwap.amount.toString(), + }, + TradeType.EXACT_OUTPUT + ), + getBridgeQuoteForMinOutput({ + inputToken: bridgeableInputToken, + outputToken: bridgeableOutputToken, + minOutputAmount: indicativeDestinationSwapQuote.maximumAmountIn, + recipient: getMultiCallHandlerAddress(destinationSwapChainId), + message: buildDestinationSwapCrossChainMessage({ + crossSwap, + destinationSwapQuote: indicativeDestinationSwapQuote, + bridgeableOutputToken, + routerAddress: destinationStrategy.getRouterAddress( + destinationSwapChainId + ), + }), + }), + originStrategy.fetchFn( + { + ...originSwap, + amount: addMarkupToAmount( + indicativeOriginSwapQuote.maximumAmountIn, + indicativeQuoteBuffer + ).toString(), + }, + TradeType.EXACT_INPUT + ), + ]), + "getAllQuotes" + ); + assertMinOutputAmount(destinationSwapQuote.minAmountOut, crossSwap.amount); assertMinOutputAmount( - adjOriginSwapQuote.minAmountOut, - bridgeQuote.inputAmount + bridgeQuote.outputAmount, + destinationSwapQuote.maximumAmountIn ); + assertMinOutputAmount(originSwapQuote.minAmountOut, bridgeQuote.inputAmount); + + bridgeQuote.message = buildDestinationSwapCrossChainMessage({ + crossSwap, + destinationSwapQuote, + bridgeableOutputToken, + routerAddress: destinationStrategy.getRouterAddress(destinationSwapChainId), + }); return { crossSwap, destinationSwapQuote, bridgeQuote, originSwapQuote: { - ...adjOriginSwapQuote, + ...originSwapQuote, entryPointContract: originSwapEntryPoint, }, }; @@ -477,6 +548,10 @@ function buildDestinationSwapCrossChainMessage({ routerAddress: string; }) { const destinationSwapChainId = destinationSwapQuote.tokenOut.chainId; + const isIndicativeQuote = + destinationSwapQuote.swapTx.to === "0x0" && + destinationSwapQuote.swapTx.data === "0x0" && + destinationSwapQuote.swapTx.value === "0x0"; let transferActions: { target: string; @@ -535,6 +610,16 @@ function buildDestinationSwapCrossChainMessage({ ]; } + const swapActions = isIndicativeQuote + ? [] + : [ + { + target: destinationSwapQuote.swapTx.to, + callData: destinationSwapQuote.swapTx.data, + value: destinationSwapQuote.swapTx.value, + }, + ]; + return buildMulticallHandlerMessage({ fallbackRecipient: getFallbackRecipient(crossSwap), actions: [ @@ -548,11 +633,7 @@ function buildDestinationSwapCrossChainMessage({ value: "0", }, // swap bridgeable output token -> cross swap output token - { - target: destinationSwapQuote.swapTx.to, - callData: destinationSwapQuote.swapTx.data, - value: destinationSwapQuote.swapTx.value, - }, + ...swapActions, // transfer output tokens to recipient ...transferActions, // drain remaining bridgeable output tokens from MultiCallHandler contract diff --git a/api/_dexes/uniswap/swap-router-02.ts b/api/_dexes/uniswap/swap-router-02.ts index 094f5428b..841617628 100644 --- a/api/_dexes/uniswap/swap-router-02.ts +++ b/api/_dexes/uniswap/swap-router-02.ts @@ -1,4 +1,4 @@ -import { BigNumber } from "ethers"; +import { BigNumber, ethers } from "ethers"; import { TradeType } from "@uniswap/sdk-core"; import { SwapRouter } from "@uniswap/router-sdk"; @@ -19,6 +19,7 @@ import { } from "./trading-api"; import { RouterTradeAdapter } from "./adapter"; import { getUniversalSwapAndBridgeAddress } from "../utils"; +import { buildCacheKey, makeCacheGetterAndSetter } from "../../_cache"; // Taken from here: https://docs.uniswap.org/contracts/v3/reference/deployments/ export const SWAP_ROUTER_02_ADDRESS = { @@ -94,17 +95,39 @@ export function getSwapRouter02Strategy( swapTx, }; } else { - const { input, output } = await getUniswapClassicIndicativeQuoteFromApi( - { ...swap, swapper: swap.recipient }, + const indicativeQuotePricePerTokenOut = await indicativeQuotePriceCache( + swap, tradeType - ); + ).get(); + const inputAmount = + tradeType === TradeType.EXACT_INPUT + ? swap.amount + : ethers.utils.parseUnits( + ( + Number( + ethers.utils.formatUnits(swap.amount, swap.tokenOut.decimals) + ) * indicativeQuotePricePerTokenOut + ).toFixed(swap.tokenIn.decimals), + swap.tokenIn.decimals + ); + const outputAmount = + tradeType === TradeType.EXACT_INPUT + ? ethers.utils.parseUnits( + ( + Number( + ethers.utils.formatUnits(swap.amount, swap.tokenIn.decimals) + ) / indicativeQuotePricePerTokenOut + ).toFixed(swap.tokenOut.decimals), + swap.tokenOut.decimals + ) + : swap.amount; - const expectedAmountIn = BigNumber.from(input.amount); + const expectedAmountIn = BigNumber.from(inputAmount); const maxAmountIn = tradeType === TradeType.EXACT_INPUT ? expectedAmountIn : addMarkupToAmount(expectedAmountIn, swap.slippageTolerance / 100); - const expectedAmountOut = BigNumber.from(output.amount); + const expectedAmountOut = BigNumber.from(outputAmount); const minAmountOut = tradeType === TradeType.EXACT_OUTPUT ? expectedAmountOut @@ -119,9 +142,9 @@ export function getSwapRouter02Strategy( expectedAmountIn, slippageTolerance: swap.slippageTolerance, swapTx: { - to: "0x", - data: "0x", - value: "0x", + to: "0x0", + data: "0x0", + value: "0x0", }, }; } @@ -176,3 +199,30 @@ export function buildSwapRouterSwapTx( to: SWAP_ROUTER_02_ADDRESS[swap.chainId], }; } + +export function indicativeQuotePriceCache(swap: Swap, tradeType: TradeType) { + // TODO: Add price buckets based on USD value, e.g. 100, 1000, 10000 + const cacheKey = buildCacheKey( + "uniswap-indicative-quote", + tradeType === TradeType.EXACT_INPUT ? "EXACT_INPUT" : "EXACT_OUTPUT", + swap.chainId, + swap.tokenIn.symbol, + swap.tokenOut.symbol + ); + const ttl = 60; + const fetchFn = async () => { + const quote = await getUniswapClassicIndicativeQuoteFromApi( + { ...swap, swapper: swap.recipient }, + tradeType + ); + const pricePerTokenOut = + Number( + ethers.utils.formatUnits(quote.input.amount, swap.tokenIn.decimals) + ) / + Number( + ethers.utils.formatUnits(quote.output.amount, swap.tokenOut.decimals) + ); + return pricePerTokenOut; + }; + return makeCacheGetterAndSetter(cacheKey, ttl, fetchFn); +} diff --git a/api/_dexes/uniswap/trading-api.ts b/api/_dexes/uniswap/trading-api.ts index 53a640580..8f556ab07 100644 --- a/api/_dexes/uniswap/trading-api.ts +++ b/api/_dexes/uniswap/trading-api.ts @@ -22,6 +22,10 @@ export type UniswapClassicQuoteFromApi = { quoteId: string; }; +export type UniswapIndicativeQuoteFromApi = Awaited< + ReturnType +>; + export type UniswapParamForApi = Omit & { swapper: string; slippageTolerance?: number; From bf1f33e46b22d3f641a65075d005758292548868 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Fri, 20 Dec 2024 15:53:21 +0100 Subject: [PATCH 36/47] feat: support permit swap (#1288) * feat: support permit swap * fixup * refactor: swap test scripts * wip * feat: refactor + support permit * fixup --- api/_abis.ts | 49 + api/_dexes/1inch.ts | 2 +- ...uote-resolver.ts => cross-swap-service.ts} | 407 +++-- api/_dexes/cross-swap.ts | 335 ---- api/_dexes/types.ts | 55 +- api/_dexes/uniswap/swap-quoter.ts | 2 +- api/_dexes/uniswap/swap-router-02.ts | 70 +- api/_dexes/uniswap/universal-router.ts | 38 +- api/_dexes/uniswap/utils.ts | 86 +- api/_dexes/utils.ts | 343 ++-- api/_permit.ts | 147 ++ api/_spoke-pool-periphery.ts | 262 ++- api/_swap-and-bridge.ts | 108 ++ api/_typechain/SpokePoolPeripheryProxy.ts | 268 +++ api/_typechain/SpokePoolV3Periphery.ts | 749 +++++---- .../SpokePoolPeripheryProxy__factory.ts | 251 +++ .../factories/SpokePoolV3Periphery.ts | 838 ---------- .../SpokePoolV3Periphery__factory.ts | 1433 +++++++++++++++++ api/_utils.ts | 9 +- api/swap-quote.ts | 2 +- api/swap/_utils.ts | 49 +- api/swap/allowance.ts | 192 +-- api/swap/approval/_utils.ts | 150 ++ api/swap/approval/index.ts | 224 +++ api/swap/permit/_utils.ts | 127 ++ api/swap/permit/index.ts | 106 ++ scripts/generate-routes.ts | 65 +- scripts/tests/_swap-utils.ts | 222 +++ scripts/tests/swap-allowance.ts | 221 +-- scripts/tests/swap-permit.ts | 15 + ...24e63716afAcE30C9a417E0542281869f7d9e.json | 5 +- ...6fA914353c44b2E33eBE05f21846F1048bEda.json | 35 +- 32 files changed, 4482 insertions(+), 2383 deletions(-) rename api/_dexes/{uniswap/quote-resolver.ts => cross-swap-service.ts} (64%) delete mode 100644 api/_dexes/cross-swap.ts create mode 100644 api/_permit.ts create mode 100644 api/_swap-and-bridge.ts create mode 100644 api/_typechain/SpokePoolPeripheryProxy.ts create mode 100644 api/_typechain/factories/SpokePoolPeripheryProxy__factory.ts delete mode 100644 api/_typechain/factories/SpokePoolV3Periphery.ts create mode 100644 api/_typechain/factories/SpokePoolV3Periphery__factory.ts create mode 100644 api/swap/approval/_utils.ts create mode 100644 api/swap/approval/index.ts create mode 100644 api/swap/permit/_utils.ts create mode 100644 api/swap/permit/index.ts create mode 100644 scripts/tests/_swap-utils.ts create mode 100644 scripts/tests/swap-permit.ts diff --git a/api/_abis.ts b/api/_abis.ts index da0bf6884..3dd4fc41f 100644 --- a/api/_abis.ts +++ b/api/_abis.ts @@ -77,3 +77,52 @@ export const MINIMAL_MULTICALL3_ABI = [ type: "function", }, ]; + +export const ERC20_PERMIT_ABI = [ + { + inputs: [], + stateMutability: "view", + type: "function", + name: "name", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + }, + { + inputs: [ + { + internalType: "address", + name: "owner", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + name: "nonces", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + }, + { + inputs: [], + name: "version", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "DOMAIN_SEPARATOR", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, +]; diff --git a/api/_dexes/1inch.ts b/api/_dexes/1inch.ts index b3febda27..f87c2e79c 100644 --- a/api/_dexes/1inch.ts +++ b/api/_dexes/1inch.ts @@ -1,7 +1,7 @@ import axios from "axios"; import { Swap, OriginSwapQuoteAndCalldata } from "./types"; -import { getSwapAndBridgeAddress } from "./utils"; +import { getSwapAndBridgeAddress } from "../_swap-and-bridge"; export async function get1inchQuoteForOriginSwapExactInput( swap: Omit diff --git a/api/_dexes/uniswap/quote-resolver.ts b/api/_dexes/cross-swap-service.ts similarity index 64% rename from api/_dexes/uniswap/quote-resolver.ts rename to api/_dexes/cross-swap-service.ts index e1bfae60a..d1f3dbfd9 100644 --- a/api/_dexes/uniswap/quote-resolver.ts +++ b/api/_dexes/cross-swap-service.ts @@ -1,54 +1,144 @@ -import { BigNumber } from "ethers"; import { TradeType } from "@uniswap/sdk-core"; import { - getRouteByInputTokenAndDestinationChain, - getTokenByAddress, getBridgeQuoteForMinOutput, - getRoutesByChainIds, + getRouteByInputTokenAndDestinationChain, getRouteByOutputTokenAndOriginChain, + getRoutesByChainIds, + getTokenByAddress, Profiler, -} from "../../_utils"; -import { - buildMulticallHandlerMessage, - encodeApproveCalldata, - encodeDrainCalldata, - encodeTransferCalldata, - encodeWethWithdrawCalldata, - getMultiCallHandlerAddress, -} from "../../_multicall-handler"; -import { - Token as AcrossToken, - CrossSwap, - CrossSwapQuotes, - SwapQuote, -} from "../types"; + addMarkupToAmount, +} from "../_utils"; +import { CrossSwap, CrossSwapQuotes, QuoteFetchStrategy } from "./types"; import { buildExactOutputBridgeTokenMessage, buildMinOutputBridgeTokenMessage, - getFallbackRecipient, -} from "../utils"; -import { AMOUNT_TYPE } from "../cross-swap"; -import { UniswapQuoteFetchStrategy, addMarkupToAmount } from "./utils"; + getCrossSwapType, + getQuoteFetchStrategy, + PREFERRED_BRIDGE_TOKENS, + QuoteFetchStrategies, +} from "./utils"; +import { getMultiCallHandlerAddress } from "../_multicall-handler"; +import { + defaultQuoteFetchStrategy, + AMOUNT_TYPE, + CROSS_SWAP_TYPE, + buildDestinationSwapCrossChainMessage, + assertMinOutputAmount, +} from "./utils"; const indicativeQuoteBuffer = 0.005; // 0.5% buffer for indicative quotes -/** - * Returns Uniswap v3 quote for a swap based on the output amount (exact or minimal) for - * route BRIDGEABLE input token -> ANY output token, e.g. USDC -> ARB. Required steps: - * 1. Get destination swap quote for bridgeable output token -> any token - * 2. Get bridge quote for bridgeable input token -> bridgeable output token - * @param crossSwap - Cross swap params - * @param strategy - Uniswap quote fetch strategy - */ -export async function getUniswapCrossSwapQuotesForOutputB2A( +export async function getCrossSwapQuotes( crossSwap: CrossSwap, - strategy: UniswapQuoteFetchStrategy + strategies: QuoteFetchStrategies = { + default: defaultQuoteFetchStrategy, + } +): Promise { + if (crossSwap.type === AMOUNT_TYPE.EXACT_INPUT) { + // @TODO: Add support for exact input amount + throw new Error("Not implemented yet"); + } + + if ( + crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT || + crossSwap.type === AMOUNT_TYPE.EXACT_OUTPUT + ) { + return getCrossSwapQuotesForOutput(crossSwap, strategies); + } + + throw new Error("Invalid amount type"); +} + +async function getCrossSwapQuotesForOutput( + crossSwap: CrossSwap, + strategies: QuoteFetchStrategies +) { + const crossSwapType = getCrossSwapType({ + inputToken: crossSwap.inputToken.address, + originChainId: crossSwap.inputToken.chainId, + outputToken: crossSwap.outputToken.address, + destinationChainId: crossSwap.outputToken.chainId, + isInputNative: Boolean(crossSwap.isInputNative), + }); + + const typeToHandler = { + [CROSS_SWAP_TYPE.BRIDGEABLE_TO_BRIDGEABLE]: getCrossSwapQuotesForOutputB2B, + [CROSS_SWAP_TYPE.BRIDGEABLE_TO_ANY]: getCrossSwapQuotesForOutputB2A, + [CROSS_SWAP_TYPE.ANY_TO_BRIDGEABLE]: getCrossSwapQuotesForOutputA2B, + [CROSS_SWAP_TYPE.ANY_TO_ANY]: getCrossSwapQuotesForOutputA2A, + }; + + const handler = typeToHandler[crossSwapType]; + + if (!handler) { + throw new Error("Invalid cross swap type"); + } + + return handler(crossSwap, strategies); +} + +// @TODO: Implement the following function +export async function getCrossSwapQuotesForExactInput(crossSwap: CrossSwap) { + throw new Error("Not implemented yet"); +} + +export async function getCrossSwapQuotesForOutputB2B( + crossSwap: CrossSwap, + strategies: QuoteFetchStrategies +) { + const originStrategy = getQuoteFetchStrategy( + crossSwap.inputToken.chainId, + strategies + ); + const bridgeQuote = await getBridgeQuoteForMinOutput({ + inputToken: crossSwap.inputToken, + outputToken: crossSwap.outputToken, + minOutputAmount: crossSwap.amount, + recipient: getMultiCallHandlerAddress(crossSwap.outputToken.chainId), + message: + crossSwap.type === AMOUNT_TYPE.EXACT_OUTPUT + ? buildExactOutputBridgeTokenMessage(crossSwap) + : buildMinOutputBridgeTokenMessage(crossSwap), + }); + + if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT) { + bridgeQuote.message = buildMinOutputBridgeTokenMessage( + crossSwap, + bridgeQuote.outputAmount + ); + } + + return { + crossSwap, + destinationSwapQuote: undefined, + bridgeQuote, + originSwapQuote: undefined, + contracts: { + depositEntryPoint: originStrategy.getOriginEntryPoints( + crossSwap.inputToken.chainId + ).deposit, + }, + }; +} + +export async function getCrossSwapQuotesForOutputB2A( + crossSwap: CrossSwap, + strategies: QuoteFetchStrategies ): Promise { const profiler = new Profiler({ - at: "api/_dexes/uniswap/quote-resolver#getUniswapCrossSwapQuotesForOutputB2A", + at: "api/_dexes/cross-swap-service#getCrossSwapQuotesForOutputB2A", logger: console, }); + const originStrategy = getQuoteFetchStrategy( + crossSwap.inputToken.chainId, + strategies + ); + const destinationStrategy = getQuoteFetchStrategy( + crossSwap.outputToken.chainId, + strategies + ); + const originSwapChainId = crossSwap.inputToken.chainId; const destinationSwapChainId = crossSwap.outputToken.chainId; const bridgeRoute = getRouteByInputTokenAndDestinationChain( crossSwap.inputToken.address, @@ -88,10 +178,17 @@ export async function getUniswapCrossSwapQuotesForOutputB2A( slippageTolerance: crossSwap.slippageTolerance, type: crossSwap.type, }; + const originRouter = originStrategy.getRouter(originSwapChainId); + const destinationRouter = destinationStrategy.getRouter( + destinationSwapChainId + ); + const depositEntryPoint = + originStrategy.getOriginEntryPoints(originSwapChainId).deposit; + // 1. Get INDICATIVE destination swap quote for bridgeable output token -> any token // with exact output amount. This request is faster but does not contain calldata. const indicativeDestinationSwapQuote = await profiler.measureAsync( - strategy.fetchFn( + destinationStrategy.fetchFn( { ...destinationSwap, amount: crossSwap.amount.toString(), @@ -109,7 +206,7 @@ export async function getUniswapCrossSwapQuotesForOutputB2A( Promise.all([ // 2.1. REAL destination swap quote for bridgeable output token -> any token. // Quote contains calldata. - strategy.fetchFn( + destinationStrategy.fetchFn( { ...destinationSwap, amount: crossSwap.amount.toString(), @@ -127,7 +224,7 @@ export async function getUniswapCrossSwapQuotesForOutputB2A( crossSwap, destinationSwapQuote: indicativeDestinationSwapQuote, bridgeableOutputToken, - routerAddress: strategy.getRouterAddress(destinationSwapChainId), + routerAddress: destinationRouter.address, }), }), ]), @@ -143,7 +240,7 @@ export async function getUniswapCrossSwapQuotesForOutputB2A( crossSwap, destinationSwapQuote, bridgeableOutputToken, - routerAddress: strategy.getRouterAddress(destinationSwapChainId), + routerAddress: destinationRouter.address, }); return { @@ -151,24 +248,24 @@ export async function getUniswapCrossSwapQuotesForOutputB2A( bridgeQuote, destinationSwapQuote, originSwapQuote: undefined, + contracts: { + originRouter, + destinationRouter, + depositEntryPoint, + }, }; } -/** - * Returns Uniswap v3 quote for a swap with min. output amount for route - * ANY input token -> BRIDGEABLE output token, e.g. ARB -> USDC. Required steps: - * 1. Get bridge quote for bridgeable input token -> bridgeable output token - * 2. Get origin swap quote for any input token -> bridgeable input token - */ -export async function getUniswapCrossSwapQuotesForOutputA2B( +export async function getCrossSwapQuotesForOutputA2B( crossSwap: CrossSwap, - strategy: UniswapQuoteFetchStrategy -): Promise { + strategies: QuoteFetchStrategies +) { const profiler = new Profiler({ - at: "api/_dexes/uniswap/quote-resolver#getUniswapCrossSwapQuotesForOutputA2B", + at: "api/_dexes/cross-swap-service#getCrossSwapQuotesForOutputA2B", logger: console, }); const originSwapChainId = crossSwap.inputToken.chainId; + const originStrategy = getQuoteFetchStrategy(originSwapChainId, strategies); const destinationChainId = crossSwap.outputToken.chainId; const bridgeRoute = getRouteByOutputTokenAndOriginChain( crossSwap.outputToken.address, @@ -220,7 +317,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( } const originSwapEntryPoint = - strategy.getOriginSwapEntryPoint(originSwapChainId); + originStrategy.getOriginEntryPoints(originSwapChainId).swapAndBridge; const originSwap = { chainId: originSwapChainId, tokenIn: crossSwap.inputToken, @@ -231,7 +328,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( }; // 2.1. Get origin swap quote for any input token -> bridgeable input token const originSwapQuote = await profiler.measureAsync( - strategy.fetchFn( + originStrategy.fetchFn( { ...originSwap, amount: bridgeQuote.inputAmount.toString(), @@ -246,7 +343,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( // 2.2. Re-fetch origin swap quote with updated input amount and EXACT_INPUT type. // This prevents leftover tokens in the SwapAndBridge contract. let adjOriginSwapQuote = await profiler.measureAsync( - strategy.fetchFn( + originStrategy.fetchFn( { ...originSwap, amount: addMarkupToAmount( @@ -267,32 +364,30 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( crossSwap, bridgeQuote, destinationSwapQuote: undefined, - originSwapQuote: { - ...adjOriginSwapQuote, - entryPointContract: originSwapEntryPoint, + originSwapQuote, + contracts: { + originSwapEntryPoint, + depositEntryPoint: + originStrategy.getOriginEntryPoints(originSwapChainId).deposit, + originRouter: originStrategy.getRouter(originSwapChainId), }, }; } -/** - * Returns Uniswap v3 quote for a swap with min. output amount for route - * ANY input token -> ANY output token, e.g. ARB -> OP. We compare quotes from - * different bridge routes and return the best one. In this iteration, we only - * consider a hardcoded list of high-liquid bridge routes. - * @param crossSwap - * @param opts - */ -export async function getBestUniswapCrossSwapQuotesForOutputA2A( +export async function getCrossSwapQuotesForOutputA2A( crossSwap: CrossSwap, - originStrategy: UniswapQuoteFetchStrategy, - destinationStrategy: UniswapQuoteFetchStrategy, - opts: { - preferredBridgeTokens: string[]; - bridgeRoutesLimit: number; - } -): Promise { + strategies: QuoteFetchStrategies +) { + const preferredBridgeTokens = PREFERRED_BRIDGE_TOKENS; + const bridgeRoutesLimit = 1; + const originSwapChainId = crossSwap.inputToken.chainId; const destinationSwapChainId = crossSwap.outputToken.chainId; + const originStrategy = getQuoteFetchStrategy(originSwapChainId, strategies); + const destinationStrategy = getQuoteFetchStrategy( + destinationSwapChainId, + strategies + ); const allBridgeRoutes = getRoutesByChainIds( originSwapChainId, destinationSwapChainId @@ -305,11 +400,11 @@ export async function getBestUniswapCrossSwapQuotesForOutputA2A( } const preferredBridgeRoutes = allBridgeRoutes.filter(({ toTokenSymbol }) => - opts.preferredBridgeTokens.includes(toTokenSymbol) + preferredBridgeTokens.includes(toTokenSymbol) ); const bridgeRoutesToCompare = ( preferredBridgeRoutes.length > 0 ? preferredBridgeRoutes : allBridgeRoutes - ).slice(0, opts.bridgeRoutesLimit); + ).slice(0, bridgeRoutesLimit); if (bridgeRoutesToCompare.length === 0) { throw new Error( @@ -319,7 +414,7 @@ export async function getBestUniswapCrossSwapQuotesForOutputA2A( const crossSwapQuotes = await Promise.all( bridgeRoutesToCompare.map((bridgeRoute) => - getUniswapCrossSwapQuotesForOutputA2A( + getCrossSwapQuotesForOutputByRouteA2A( crossSwap, bridgeRoute, originStrategy, @@ -339,13 +434,7 @@ export async function getBestUniswapCrossSwapQuotesForOutputA2A( return bestCrossSwapQuote; } -/** - * Returns Uniswap v3 quote for a swap with min. output amount for route - * ANY input token -> ANY output token, e.g. ARB -> OP, using a specific bridge route. - * @param crossSwap - * @param bridgeRoute - */ -export async function getUniswapCrossSwapQuotesForOutputA2A( +export async function getCrossSwapQuotesForOutputByRouteA2A( crossSwap: CrossSwap, bridgeRoute: { fromTokenAddress: string; @@ -353,11 +442,11 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( toTokenAddress: string; toChain: number; }, - originStrategy: UniswapQuoteFetchStrategy, - destinationStrategy: UniswapQuoteFetchStrategy + originStrategy: QuoteFetchStrategy, + destinationStrategy: QuoteFetchStrategy ): Promise { const profiler = new Profiler({ - at: "api/_dexes/uniswap/quote-resolver#getUniswapCrossSwapQuotesForOutputA2A", + at: "api/_dexes/cross-swap-service#getCrossSwapQuotesForOutputByRouteA2A", logger: console, }); const originSwapChainId = crossSwap.inputToken.chainId; @@ -400,7 +489,13 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( destinationSwapChainId ); const originSwapEntryPoint = - originStrategy.getOriginSwapEntryPoint(originSwapChainId); + originStrategy.getOriginEntryPoints(originSwapChainId).swapAndBridge; + const depositEntryPoint = + originStrategy.getOriginEntryPoints(originSwapChainId).deposit; + const originRouter = originStrategy.getRouter(originSwapChainId); + const destinationRouter = destinationStrategy.getRouter( + destinationSwapChainId + ); const originSwap = { chainId: originSwapChainId, tokenIn: crossSwap.inputToken, @@ -449,9 +544,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( crossSwap, destinationSwapQuote: indicativeDestinationSwapQuote, bridgeableOutputToken, - routerAddress: destinationStrategy.getRouterAddress( - destinationSwapChainId - ), + routerAddress: destinationRouter.address, }), }), "INDICATIVE_getBridgeQuote" @@ -493,9 +586,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( crossSwap, destinationSwapQuote: indicativeDestinationSwapQuote, bridgeableOutputToken, - routerAddress: destinationStrategy.getRouterAddress( - destinationSwapChainId - ), + routerAddress: destinationRouter.address, }), }), originStrategy.fetchFn( @@ -522,141 +613,19 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( crossSwap, destinationSwapQuote, bridgeableOutputToken, - routerAddress: destinationStrategy.getRouterAddress(destinationSwapChainId), + routerAddress: destinationRouter.address, }); return { crossSwap, destinationSwapQuote, bridgeQuote, - originSwapQuote: { - ...originSwapQuote, - entryPointContract: originSwapEntryPoint, + originSwapQuote, + contracts: { + originSwapEntryPoint, + depositEntryPoint, + originRouter: originRouter, + destinationRouter: destinationRouter, }, }; } - -function buildDestinationSwapCrossChainMessage({ - crossSwap, - destinationSwapQuote, - bridgeableOutputToken, - routerAddress, -}: { - crossSwap: CrossSwap; - bridgeableOutputToken: AcrossToken; - destinationSwapQuote: SwapQuote; - routerAddress: string; -}) { - const destinationSwapChainId = destinationSwapQuote.tokenOut.chainId; - const isIndicativeQuote = - destinationSwapQuote.swapTx.to === "0x0" && - destinationSwapQuote.swapTx.data === "0x0" && - destinationSwapQuote.swapTx.value === "0x0"; - - let transferActions: { - target: string; - callData: string; - value: string; - }[] = []; - - // If output token is native, we need to unwrap WETH before sending it to the - // recipient. This is because we only handle WETH in the destination swap. - if (crossSwap.isOutputNative) { - transferActions = [ - { - target: crossSwap.outputToken.address, - callData: encodeWethWithdrawCalldata(crossSwap.amount), - value: "0", - }, - { - target: crossSwap.recipient, - callData: "0x", - value: crossSwap.amount.toString(), - }, - ]; - } - // If output token is an ERC-20 token and amount type is EXACT_OUTPUT, we need - // to transfer the EXACT output amount to the recipient. The refundAddress / depositor - // will receive any leftovers. - else if (crossSwap.type === AMOUNT_TYPE.EXACT_OUTPUT) { - transferActions = [ - { - target: crossSwap.outputToken.address, - callData: encodeTransferCalldata(crossSwap.recipient, crossSwap.amount), - value: "0", - }, - { - target: getMultiCallHandlerAddress(destinationSwapChainId), - callData: encodeDrainCalldata( - crossSwap.outputToken.address, - crossSwap.refundAddress ?? crossSwap.depositor - ), - value: "0", - }, - ]; - } - // If output token is an ERC-20 token and amount type is MIN_OUTPUT, we need - // to transfer all realized output tokens to the recipient. - else if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT) { - transferActions = [ - { - target: getMultiCallHandlerAddress(destinationSwapChainId), - callData: encodeDrainCalldata( - crossSwap.outputToken.address, - crossSwap.recipient - ), - value: "0", - }, - ]; - } - - const swapActions = isIndicativeQuote - ? [] - : [ - { - target: destinationSwapQuote.swapTx.to, - callData: destinationSwapQuote.swapTx.data, - value: destinationSwapQuote.swapTx.value, - }, - ]; - - return buildMulticallHandlerMessage({ - fallbackRecipient: getFallbackRecipient(crossSwap), - actions: [ - // approve bridgeable output token - { - target: bridgeableOutputToken.address, - callData: encodeApproveCalldata( - routerAddress, - destinationSwapQuote.maximumAmountIn - ), - value: "0", - }, - // swap bridgeable output token -> cross swap output token - ...swapActions, - // transfer output tokens to recipient - ...transferActions, - // drain remaining bridgeable output tokens from MultiCallHandler contract - { - target: getMultiCallHandlerAddress(destinationSwapChainId), - callData: encodeDrainCalldata( - bridgeableOutputToken.address, - crossSwap.refundAddress ?? crossSwap.depositor - ), - value: "0", - }, - ], - }); -} - -function assertMinOutputAmount( - amountOut: BigNumber, - expectedMinAmountOut: BigNumber -) { - if (amountOut.lt(expectedMinAmountOut)) { - throw new Error( - `Swap quote output amount ${amountOut.toString()} ` + - `is less than required min. output amount ${expectedMinAmountOut.toString()}` - ); - } -} diff --git a/api/_dexes/cross-swap.ts b/api/_dexes/cross-swap.ts deleted file mode 100644 index 4d6d0e655..000000000 --- a/api/_dexes/cross-swap.ts +++ /dev/null @@ -1,335 +0,0 @@ -import { SpokePool } from "@across-protocol/contracts/dist/typechain"; -import { PopulatedTransaction } from "ethers"; -import { utils } from "@across-protocol/sdk"; - -import { - isRouteEnabled, - isInputTokenBridgeable, - isOutputTokenBridgeable, - getBridgeQuoteForMinOutput, - getSpokePool, -} from "../_utils"; -import { - getBestUniswapCrossSwapQuotesForOutputA2A, - getUniswapCrossSwapQuotesForOutputA2B, - getUniswapCrossSwapQuotesForOutputB2A, -} from "./uniswap/quote-resolver"; -import { getSwapRouter02Strategy } from "./uniswap/swap-router-02"; -import { UniswapQuoteFetchStrategy } from "./uniswap/utils"; -import { CrossSwap, CrossSwapQuotes } from "./types"; -import { - buildExactOutputBridgeTokenMessage, - buildMinOutputBridgeTokenMessage, - getUniversalSwapAndBridge, -} from "./utils"; -import { getSpokePoolPeriphery } from "../_spoke-pool-periphery"; -import { tagIntegratorId } from "../_integrator-id"; -import { getMultiCallHandlerAddress } from "../_multicall-handler"; -import { CHAIN_IDs } from "../_constants"; - -export type CrossSwapType = - (typeof CROSS_SWAP_TYPE)[keyof typeof CROSS_SWAP_TYPE]; - -export type AmountType = (typeof AMOUNT_TYPE)[keyof typeof AMOUNT_TYPE]; - -export const AMOUNT_TYPE = { - EXACT_INPUT: "exactInput", - EXACT_OUTPUT: "exactOutput", - MIN_OUTPUT: "minOutput", -} as const; - -export const CROSS_SWAP_TYPE = { - BRIDGEABLE_TO_BRIDGEABLE: "bridgeableToBridgeable", - BRIDGEABLE_TO_ANY: "bridgeableToAny", - ANY_TO_BRIDGEABLE: "anyToBridgeable", - ANY_TO_ANY: "anyToAny", -} as const; - -export const PREFERRED_BRIDGE_TOKENS = ["WETH"]; - -const defaultQuoteFetchStrategy: UniswapQuoteFetchStrategy = - // This will be our default strategy until the periphery contract is audited - getSwapRouter02Strategy("UniversalSwapAndBridge"); -const strategyOverrides = { - [CHAIN_IDs.BLAST]: defaultQuoteFetchStrategy, -}; - -export async function getCrossSwapQuotes( - crossSwap: CrossSwap -): Promise { - if (crossSwap.type === AMOUNT_TYPE.EXACT_INPUT) { - // @TODO: Add support for exact input amount - throw new Error("Not implemented yet"); - } - - if ( - crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT || - crossSwap.type === AMOUNT_TYPE.EXACT_OUTPUT - ) { - return getCrossSwapQuotesForOutput(crossSwap); - } - - throw new Error("Invalid amount type"); -} - -export async function getCrossSwapQuotesForOutput(crossSwap: CrossSwap) { - const crossSwapType = getCrossSwapType({ - inputToken: crossSwap.inputToken.address, - originChainId: crossSwap.inputToken.chainId, - outputToken: crossSwap.outputToken.address, - destinationChainId: crossSwap.outputToken.chainId, - isInputNative: Boolean(crossSwap.isInputNative), - }); - - if (crossSwapType === CROSS_SWAP_TYPE.BRIDGEABLE_TO_BRIDGEABLE) { - return getCrossSwapQuotesForOutputB2B(crossSwap); - } - - if (crossSwapType === CROSS_SWAP_TYPE.BRIDGEABLE_TO_ANY) { - return getCrossSwapQuotesForOutputB2A(crossSwap); - } - - if (crossSwapType === CROSS_SWAP_TYPE.ANY_TO_BRIDGEABLE) { - return getCrossSwapQuotesForOutputA2B(crossSwap); - } - - if (crossSwapType === CROSS_SWAP_TYPE.ANY_TO_ANY) { - return getCrossSwapQuotesForOutputA2A(crossSwap); - } - - throw new Error("Invalid cross swap type"); -} - -// @TODO: Implement the following function -export async function getCrossSwapQuotesForExactInput(crossSwap: CrossSwap) { - throw new Error("Not implemented yet"); -} - -export async function getCrossSwapQuotesForOutputB2B(crossSwap: CrossSwap) { - const bridgeQuote = await getBridgeQuoteForMinOutput({ - inputToken: crossSwap.inputToken, - outputToken: crossSwap.outputToken, - minOutputAmount: crossSwap.amount, - recipient: getMultiCallHandlerAddress(crossSwap.outputToken.chainId), - message: - crossSwap.type === AMOUNT_TYPE.EXACT_OUTPUT - ? buildExactOutputBridgeTokenMessage(crossSwap) - : buildMinOutputBridgeTokenMessage(crossSwap), - }); - - if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT) { - bridgeQuote.message = buildMinOutputBridgeTokenMessage( - crossSwap, - bridgeQuote.outputAmount - ); - } - - return { - crossSwap, - destinationSwapQuote: undefined, - bridgeQuote, - originSwapQuote: undefined, - }; -} - -export async function getCrossSwapQuotesForOutputB2A(crossSwap: CrossSwap) { - return getUniswapCrossSwapQuotesForOutputB2A( - crossSwap, - // Destination swap requires destination chain's quote fetch strategy - getQuoteFetchStrategy(crossSwap.outputToken.chainId) - ); -} - -export async function getCrossSwapQuotesForOutputA2B(crossSwap: CrossSwap) { - return getUniswapCrossSwapQuotesForOutputA2B( - crossSwap, - // Origin swap requires origin chain's quote fetch strategy - getQuoteFetchStrategy(crossSwap.inputToken.chainId) - ); -} - -export async function getCrossSwapQuotesForOutputA2A(crossSwap: CrossSwap) { - return getBestUniswapCrossSwapQuotesForOutputA2A( - crossSwap, - getQuoteFetchStrategy(crossSwap.inputToken.chainId), - getQuoteFetchStrategy(crossSwap.outputToken.chainId), - { - preferredBridgeTokens: PREFERRED_BRIDGE_TOKENS, - bridgeRoutesLimit: 1, - } - ); -} - -function getQuoteFetchStrategy(chainId: number) { - return strategyOverrides[chainId] ?? defaultQuoteFetchStrategy; -} - -export function getCrossSwapType(params: { - inputToken: string; - originChainId: number; - outputToken: string; - destinationChainId: number; - isInputNative: boolean; -}): CrossSwapType { - if ( - isRouteEnabled( - params.originChainId, - params.destinationChainId, - params.inputToken, - params.outputToken - ) - ) { - return CROSS_SWAP_TYPE.BRIDGEABLE_TO_BRIDGEABLE; - } - - // Prefer destination swap if input token is native because legacy - // `UniversalSwapAndBridge` does not support native tokens as input. - if (params.isInputNative) { - if (isInputTokenBridgeable(params.inputToken, params.originChainId)) { - return CROSS_SWAP_TYPE.BRIDGEABLE_TO_ANY; - } - // We can't bridge native tokens that are not ETH, e.g. MATIC or AZERO. Therefore - // throw until we have periphery contract audited so that it can accept native - // tokens and do an origin swap. - throw new Error( - "Unsupported swap: Input token is native but not bridgeable" - ); - } - - if (isOutputTokenBridgeable(params.outputToken, params.destinationChainId)) { - return CROSS_SWAP_TYPE.ANY_TO_BRIDGEABLE; - } - - if (isInputTokenBridgeable(params.inputToken, params.originChainId)) { - return CROSS_SWAP_TYPE.BRIDGEABLE_TO_ANY; - } - - return CROSS_SWAP_TYPE.ANY_TO_ANY; -} - -export async function buildCrossSwapTxForAllowanceHolder( - crossSwapQuotes: CrossSwapQuotes, - integratorId?: string -) { - const originChainId = crossSwapQuotes.crossSwap.inputToken.chainId; - const spokePool = getSpokePool(originChainId); - - const deposit = await extractDepositDataStruct(crossSwapQuotes); - - let tx: PopulatedTransaction; - let toAddress: string; - - if (crossSwapQuotes.originSwapQuote) { - const { entryPointContract } = crossSwapQuotes.originSwapQuote; - if (entryPointContract.name === "SpokePoolPeriphery") { - const spokePoolPeriphery = getSpokePoolPeriphery( - entryPointContract.address, - originChainId - ); - tx = await spokePoolPeriphery.populateTransaction.swapAndBridge( - crossSwapQuotes.originSwapQuote.tokenIn.address, - crossSwapQuotes.originSwapQuote.tokenOut.address, - crossSwapQuotes.originSwapQuote.swapTx.data, - crossSwapQuotes.originSwapQuote.maximumAmountIn, - crossSwapQuotes.originSwapQuote.minAmountOut, - deposit, - { - value: crossSwapQuotes.crossSwap.isInputNative - ? crossSwapQuotes.originSwapQuote.maximumAmountIn - : 0, - } - ); - toAddress = spokePoolPeriphery.address; - } else if (entryPointContract.name === "UniversalSwapAndBridge") { - const universalSwapAndBridge = getUniversalSwapAndBridge( - entryPointContract.dex, - originChainId - ); - tx = await universalSwapAndBridge.populateTransaction.swapAndBridge( - crossSwapQuotes.originSwapQuote.tokenIn.address, - crossSwapQuotes.originSwapQuote.tokenOut.address, - crossSwapQuotes.originSwapQuote.swapTx.data, - crossSwapQuotes.originSwapQuote.maximumAmountIn, - crossSwapQuotes.originSwapQuote.minAmountOut, - deposit - ); - toAddress = universalSwapAndBridge.address; - } else { - throw new Error( - `Could not build cross swap tx for unknown entry point contract` - ); - } - } else { - tx = await spokePool.populateTransaction.depositV3( - deposit.depositor, - deposit.recipient, - deposit.inputToken, - deposit.outputToken, - deposit.inputAmount, - deposit.outputAmount, - deposit.destinationChainid, - deposit.exclusiveRelayer, - deposit.quoteTimestamp, - deposit.fillDeadline, - deposit.exclusivityDeadline, - deposit.message, - { - value: crossSwapQuotes.crossSwap.isInputNative - ? deposit.inputAmount - : 0, - } - ); - toAddress = spokePool.address; - } - - return { - from: crossSwapQuotes.crossSwap.depositor, - to: toAddress, - data: integratorId ? tagIntegratorId(integratorId, tx.data!) : tx.data, - value: tx.value, - }; -} - -async function extractDepositDataStruct(crossSwapQuotes: CrossSwapQuotes) { - const originChainId = crossSwapQuotes.crossSwap.inputToken.chainId; - const destinationChainId = crossSwapQuotes.crossSwap.outputToken.chainId; - const spokePool = getSpokePool(originChainId); - const message = crossSwapQuotes.bridgeQuote.message || "0x"; - const refundAddress = - crossSwapQuotes.crossSwap.refundAddress ?? - crossSwapQuotes.crossSwap.depositor; - const deposit = { - depositor: crossSwapQuotes.crossSwap.refundOnOrigin - ? refundAddress - : crossSwapQuotes.crossSwap.depositor, - recipient: utils.isMessageEmpty(message) - ? crossSwapQuotes.crossSwap.recipient - : getMultiCallHandlerAddress(destinationChainId), - inputToken: crossSwapQuotes.bridgeQuote.inputToken.address, - outputToken: crossSwapQuotes.bridgeQuote.outputToken.address, - inputAmount: crossSwapQuotes.bridgeQuote.inputAmount, - outputAmount: crossSwapQuotes.bridgeQuote.outputAmount, - destinationChainid: destinationChainId, - exclusiveRelayer: - crossSwapQuotes.bridgeQuote.suggestedFees.exclusiveRelayer, - quoteTimestamp: crossSwapQuotes.bridgeQuote.suggestedFees.timestamp, - fillDeadline: await getFillDeadline(spokePool), - exclusivityDeadline: - crossSwapQuotes.bridgeQuote.suggestedFees.exclusivityDeadline, - exclusivityParameter: - crossSwapQuotes.bridgeQuote.suggestedFees.exclusivityDeadline, - message, - }; - return deposit; -} - -async function getFillDeadline(spokePool: SpokePool): Promise { - const calls = [ - spokePool.interface.encodeFunctionData("getCurrentTime"), - spokePool.interface.encodeFunctionData("fillDeadlineBuffer"), - ]; - - const [currentTime, fillDeadlineBuffer] = - await spokePool.callStatic.multicall(calls); - return Number(currentTime) + Number(fillDeadlineBuffer); -} diff --git a/api/_dexes/types.ts b/api/_dexes/types.ts index e69fc1418..890bcac60 100644 --- a/api/_dexes/types.ts +++ b/api/_dexes/types.ts @@ -1,6 +1,8 @@ import { BigNumber } from "ethers"; +import { TradeType } from "@uniswap/sdk-core"; + import { getSuggestedFees } from "../_utils"; -import { AmountType, CrossSwapType } from "./cross-swap"; +import { AmountType, CrossSwapType } from "./utils"; export type { AmountType, CrossSwapType }; @@ -75,14 +77,18 @@ export type CrossSwapQuotes = { suggestedFees: Awaited>; }; destinationSwapQuote?: SwapQuote; - originSwapQuote?: SwapQuote & { - entryPointContract: OriginSwapEntryPointContract; + originSwapQuote?: SwapQuote; + contracts: { + depositEntryPoint: DepositEntryPointContract; + originRouter?: RouterContract; + destinationRouter?: RouterContract; + originSwapEntryPoint?: OriginSwapEntryPointContract; }; }; export type OriginSwapEntryPointContract = | { - name: "SpokePoolPeriphery"; + name: "SpokePoolPeripheryProxy" | "SpokePoolPeriphery"; address: string; } | { @@ -90,14 +96,53 @@ export type OriginSwapEntryPointContract = address: string; dex: SupportedDex; }; +export type DepositEntryPointContract = { + name: "SpokePoolPeriphery" | "SpokePool"; + address: string; +}; +export type RouterContract = { + name: string; + address: string; +}; export type CrossSwapQuotesWithFees = CrossSwapQuotes & { fees: CrossSwapFees; }; -// { currency => amount } export type CrossSwapFees = { bridgeFees: Record; originSwapFees?: Record; destinationSwapFees?: Record; }; + +export type QuoteFetchStrategy = { + getRouter: (chainId: number) => { + address: string; + name: string; + }; + getOriginEntryPoints: (chainId: number) => { + swapAndBridge: + | { + name: "UniversalSwapAndBridge"; + address: string; + dex: "uniswap" | "1inch"; + } + | { + name: "SpokePoolPeripheryProxy" | "SpokePoolPeriphery"; + address: string; + }; + deposit: { + name: "SpokePoolPeriphery" | "SpokePool"; + address: string; + }; + }; + fetchFn: QuoteFetchFn; +}; + +export type QuoteFetchFn = ( + swap: Swap, + tradeType: TradeType, + opts?: Partial<{ + useIndicativeQuote: boolean; + }> +) => Promise; diff --git a/api/_dexes/uniswap/swap-quoter.ts b/api/_dexes/uniswap/swap-quoter.ts index f22768cae..7d3458a3e 100644 --- a/api/_dexes/uniswap/swap-quoter.ts +++ b/api/_dexes/uniswap/swap-quoter.ts @@ -22,7 +22,7 @@ import { CHAIN_IDs } from "@across-protocol/constants"; import { utils } from "@across-protocol/sdk"; import { Swap } from "../types"; -import { getSwapAndBridgeAddress } from "../utils"; +import { getSwapAndBridgeAddress } from "../../_swap-and-bridge"; import { getProdToken } from "./utils"; import { callViaMulticall3, getProvider } from "../../_utils"; diff --git a/api/_dexes/uniswap/swap-router-02.ts b/api/_dexes/uniswap/swap-router-02.ts index 841617628..534cd52ae 100644 --- a/api/_dexes/uniswap/swap-router-02.ts +++ b/api/_dexes/uniswap/swap-router-02.ts @@ -4,21 +4,24 @@ import { SwapRouter } from "@uniswap/router-sdk"; import { CHAIN_IDs } from "@across-protocol/constants"; -import { getLogger } from "../../_utils"; -import { OriginSwapEntryPointContract, Swap, SwapQuote } from "../types"; -import { getSpokePoolPeripheryAddress } from "../../_spoke-pool-periphery"; import { + getLogger, + getSpokePoolAddress, addMarkupToAmount, - floatToPercent, - UniswapQuoteFetchStrategy, -} from "./utils"; +} from "../../_utils"; +import { Swap, SwapQuote } from "../types"; +import { + getSpokePoolPeripheryAddress, + getSpokePoolPeripheryProxyAddress, +} from "../../_spoke-pool-periphery"; +import { getUniversalSwapAndBridgeAddress } from "../../_swap-and-bridge"; +import { floatToPercent, UniswapQuoteFetchStrategy } from "./utils"; import { getUniswapClassicQuoteFromApi, getUniswapClassicIndicativeQuoteFromApi, UniswapClassicQuoteFromApi, } from "./trading-api"; import { RouterTradeAdapter } from "./adapter"; -import { getUniversalSwapAndBridgeAddress } from "../utils"; import { buildCacheKey, makeCacheGetterAndSetter } from "../../_cache"; // Taken from here: https://docs.uniswap.org/contracts/v3/reference/deployments/ @@ -35,20 +38,51 @@ export const SWAP_ROUTER_02_ADDRESS = { }; export function getSwapRouter02Strategy( - originSwapEntryPointContractName: OriginSwapEntryPointContract["name"] + originSwapEntryPointContractName: + | "SpokePoolPeriphery" + | "SpokePoolPeripheryProxy" + | "UniversalSwapAndBridge" ): UniswapQuoteFetchStrategy { - const getRouterAddress = (chainId: number) => SWAP_ROUTER_02_ADDRESS[chainId]; - const getOriginSwapEntryPoint = (chainId: number) => { - if (originSwapEntryPointContractName === "SpokePoolPeriphery") { + const getRouter = (chainId: number) => { + return { + address: SWAP_ROUTER_02_ADDRESS[chainId], + name: "UniswapV3SwapRouter02", + }; + }; + const getOriginEntryPoints = (chainId: number) => { + if (originSwapEntryPointContractName === "SpokePoolPeripheryProxy") { + return { + swapAndBridge: { + name: "SpokePoolPeripheryProxy", + address: getSpokePoolPeripheryProxyAddress(chainId), + }, + deposit: { + name: "SpokePoolPeriphery", + address: getSpokePoolPeripheryAddress(chainId), + }, + } as const; + } else if (originSwapEntryPointContractName === "SpokePoolPeriphery") { return { - name: "SpokePoolPeriphery", - address: getSpokePoolPeripheryAddress("uniswap-swapRouter02", chainId), + swapAndBridge: { + name: "SpokePoolPeriphery", + address: getSpokePoolPeripheryAddress(chainId), + }, + deposit: { + name: "SpokePoolPeriphery", + address: getSpokePoolPeripheryAddress(chainId), + }, } as const; } else if (originSwapEntryPointContractName === "UniversalSwapAndBridge") { return { - name: "UniversalSwapAndBridge", - address: getUniversalSwapAndBridgeAddress("uniswap", chainId), - dex: "uniswap", + swapAndBridge: { + name: "UniversalSwapAndBridge", + address: getUniversalSwapAndBridgeAddress("uniswap", chainId), + dex: "uniswap", + }, + deposit: { + name: "SpokePool", + address: getSpokePoolAddress(chainId), + }, } as const; } throw new Error( @@ -167,8 +201,8 @@ export function getSwapRouter02Strategy( }; return { - getRouterAddress, - getOriginSwapEntryPoint, + getRouter, + getOriginEntryPoints, fetchFn, }; } diff --git a/api/_dexes/uniswap/universal-router.ts b/api/_dexes/uniswap/universal-router.ts index bbaad27df..5e24376d4 100644 --- a/api/_dexes/uniswap/universal-router.ts +++ b/api/_dexes/uniswap/universal-router.ts @@ -3,19 +3,18 @@ import { TradeType } from "@uniswap/sdk-core"; import { CHAIN_IDs } from "@across-protocol/constants"; import { SwapRouter } from "@uniswap/universal-router-sdk"; -import { getLogger } from "../../_utils"; +import { getLogger, addMarkupToAmount } from "../../_utils"; import { Swap, SwapQuote } from "../types"; -import { getSpokePoolPeripheryAddress } from "../../_spoke-pool-periphery"; +import { + getSpokePoolPeripheryAddress, + getSpokePoolPeripheryProxyAddress, +} from "../../_spoke-pool-periphery"; import { getUniswapClassicQuoteFromApi, getUniswapClassicIndicativeQuoteFromApi, UniswapClassicQuoteFromApi, } from "./trading-api"; -import { - UniswapQuoteFetchStrategy, - addMarkupToAmount, - floatToPercent, -} from "./utils"; +import { UniswapQuoteFetchStrategy, floatToPercent } from "./utils"; import { RouterTradeAdapter } from "./adapter"; // https://uniswap-docs.readme.io/reference/faqs#i-need-to-whitelist-the-router-addresses-where-can-i-find-them @@ -32,12 +31,23 @@ export const UNIVERSAL_ROUTER_ADDRESS = { }; export function getUniversalRouterStrategy(): UniswapQuoteFetchStrategy { - const getRouterAddress = (chainId: number) => - UNIVERSAL_ROUTER_ADDRESS[chainId]; - const getOriginSwapEntryPoint = (chainId: number) => + const getRouter = (chainId: number) => { + return { + address: UNIVERSAL_ROUTER_ADDRESS[chainId], + name: "UniswapV3UniversalRouter", + }; + }; + + const getOriginEntryPoints = (chainId: number) => ({ - name: "SpokePoolPeriphery", - address: getSpokePoolPeripheryAddress("uniswap-universalRouter", chainId), + swapAndBridge: { + name: "SpokePoolPeripheryProxy", + address: getSpokePoolPeripheryProxyAddress(chainId), + }, + deposit: { + name: "SpokePoolPeriphery", + address: getSpokePoolPeripheryAddress(chainId), + }, }) as const; const fetchFn = async ( @@ -129,8 +139,8 @@ export function getUniversalRouterStrategy(): UniswapQuoteFetchStrategy { }; return { - getRouterAddress, - getOriginSwapEntryPoint, + getRouter, + getOriginEntryPoints, fetchFn, }; } diff --git a/api/_dexes/uniswap/utils.ts b/api/_dexes/uniswap/utils.ts index 2840ff4cb..9699a7adc 100644 --- a/api/_dexes/uniswap/utils.ts +++ b/api/_dexes/uniswap/utils.ts @@ -1,32 +1,9 @@ import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants"; -import { Percent, TradeType } from "@uniswap/sdk-core"; -import axios from "axios"; -import { BigNumber, ethers } from "ethers"; -import { utils } from "@across-protocol/sdk"; +import { Percent } from "@uniswap/sdk-core"; -import { Swap, SwapQuote, Token } from "../types"; +import { Token, QuoteFetchStrategy } from "../types"; -export type UniswapQuoteFetchStrategy = { - getRouterAddress: (chainId: number) => string; - getOriginSwapEntryPoint: (chainId: number) => - | { - name: "SpokePoolPeriphery"; - address: string; - } - | { - name: "UniversalSwapAndBridge"; - address: string; - dex: "uniswap" | "1inch"; - }; - fetchFn: UniswapQuoteFetchFn; -}; -export type UniswapQuoteFetchFn = ( - swap: Swap, - tradeType: TradeType, - opts?: Partial<{ - useIndicativeQuote: boolean; - }> -) => Promise; +export type UniswapQuoteFetchStrategy = QuoteFetchStrategy; // Maps testnet chain IDs to their prod counterparts. Used to get the prod token // info for testnet tokens. @@ -44,57 +21,6 @@ export const UNISWAP_TRADING_API_BASE_URL = export const UNISWAP_API_KEY = process.env.UNISWAP_API_KEY || "JoyCGj29tT4pymvhaGciK4r1aIPvqW6W53xT1fwo"; -/** - * Based on https://uniswap-docs.readme.io/reference/aggregator_quote-1 - */ -export async function getUniswapClassicQuoteFromApi( - swap: Omit & { swapper: string }, - tradeType: TradeType -) { - const response = await axios.post( - `${UNISWAP_TRADING_API_BASE_URL}/quote`, - { - type: - tradeType === TradeType.EXACT_INPUT ? "EXACT_INPUT" : "EXACT_OUTPUT", - tokenInChainId: swap.tokenIn.chainId, - tokenOutChainId: swap.tokenOut.chainId, - tokenIn: swap.tokenIn.address, - tokenOut: swap.tokenOut.address, - swapper: swap.swapper, - slippageTolerance: swap.slippageTolerance, - amount: swap.amount, - routingPreference: "CLASSIC", - urgency: "urgent", - }, - { - headers: { - "x-api-key": UNISWAP_API_KEY, - }, - } - ); - return response.data as { - requestId: string; - routing: "CLASSIC"; - quote: { - chainId: number; - input: { - amount: string; - token: string; - }; - output: { - amount: string; - token: string; - recipient: string; - }; - swapper: string; - route: any[]; - slippage: number; - tradeType: "EXACT_OUTPUT" | "EXACT_INPUT"; - quoteId: string; - }; - }; -} - export function getProdToken(token: Token) { const prodChainId = TESTNET_TO_PROD[token.chainId] || token.chainId; @@ -115,12 +41,6 @@ export function getProdToken(token: Token) { }; } -export function addMarkupToAmount(amount: BigNumber, markup = 0.01) { - return amount - .mul(ethers.utils.parseEther((1 + Number(markup)).toString())) - .div(utils.fixedPointAdjustment); -} - export function floatToPercent(value: number) { return new Percent( // max. slippage decimals is 2 diff --git a/api/_dexes/utils.ts b/api/_dexes/utils.ts index d90f95664..f8d0675d6 100644 --- a/api/_dexes/utils.ts +++ b/api/_dexes/utils.ts @@ -1,119 +1,100 @@ -import { - SwapAndBridge__factory, - UniversalSwapAndBridge__factory, -} from "@across-protocol/contracts"; import { BigNumber, constants } from "ethers"; +import { utils } from "@across-protocol/sdk"; +import { SpokePool } from "@across-protocol/contracts/dist/typechain"; -import { ENABLED_ROUTES, getProvider } from "../_utils"; +import { getSwapRouter02Strategy } from "./uniswap/swap-router-02"; import { buildMulticallHandlerMessage, + encodeApproveCalldata, encodeDrainCalldata, encodeTransferCalldata, encodeWethWithdrawCalldata, getMultiCallHandlerAddress, } from "../_multicall-handler"; -import { CrossSwap } from "./types"; +import { + CrossSwap, + CrossSwapQuotes, + QuoteFetchStrategy, + SwapQuote, + Token, +} from "./types"; +import { + isInputTokenBridgeable, + isRouteEnabled, + isOutputTokenBridgeable, + getSpokePool, +} from "../_utils"; -type SwapAndBridgeType = "SwapAndBridge" | "UniversalSwapAndBridge"; +export type CrossSwapType = + (typeof CROSS_SWAP_TYPE)[keyof typeof CROSS_SWAP_TYPE]; -export class UnsupportedDex extends Error { - constructor(dex: string, type: SwapAndBridgeType) { - super(`DEX/Aggregator '${dex}' not supported for '${type}'`); - } -} +export type AmountType = (typeof AMOUNT_TYPE)[keyof typeof AMOUNT_TYPE]; -export class UnsupportedDexOnChain extends Error { - constructor(chainId: number, dex: string, type: SwapAndBridgeType) { - super( - `DEX/Aggregator '${dex}' not supported on chain ${chainId} for '${type}'` - ); - } -} +export type QuoteFetchStrategies = Partial<{ + default: QuoteFetchStrategy; + [chainId: number]: QuoteFetchStrategy; +}>; -export class NoSwapRouteError extends Error { - constructor(args: { - dex: string; - tokenInSymbol: string; - tokenOutSymbol: string; - chainId: number; - swapType: string; - }) { - super( - `No ${args.dex} swap route found for '${args.swapType}' ${args.tokenInSymbol} to ${args.tokenOutSymbol} on chain ${args.chainId}` - ); - } -} +export const AMOUNT_TYPE = { + EXACT_INPUT: "exactInput", + EXACT_OUTPUT: "exactOutput", + MIN_OUTPUT: "minOutput", +} as const; -export const swapAndBridgeDexes = Object.keys( - ENABLED_ROUTES.swapAndBridgeAddresses -); +export const CROSS_SWAP_TYPE = { + BRIDGEABLE_TO_BRIDGEABLE: "bridgeableToBridgeable", + BRIDGEABLE_TO_ANY: "bridgeableToAny", + ANY_TO_BRIDGEABLE: "anyToBridgeable", + ANY_TO_ANY: "anyToAny", +} as const; -export const universalSwapAndBridgeDexes = Object.keys( - ENABLED_ROUTES.universalSwapAndBridgeAddresses -); +export const PREFERRED_BRIDGE_TOKENS = ["WETH"]; -export function getSwapAndBridgeAddress(dex: string, chainId: number) { - if (!_isDexSupportedForSwapAndBridge(dex)) { - throw new UnsupportedDex(dex, "SwapAndBridge"); - } +export const defaultQuoteFetchStrategy: QuoteFetchStrategy = + // This will be our default strategy until the periphery contract is audited + getSwapRouter02Strategy("UniversalSwapAndBridge"); - const address = ( - ENABLED_ROUTES.swapAndBridgeAddresses[dex] as Record - )?.[chainId]; - if (!address) { - throw new UnsupportedDexOnChain(chainId, dex, "SwapAndBridge"); +export function getCrossSwapType(params: { + inputToken: string; + originChainId: number; + outputToken: string; + destinationChainId: number; + isInputNative: boolean; +}): CrossSwapType { + if ( + isRouteEnabled( + params.originChainId, + params.destinationChainId, + params.inputToken, + params.outputToken + ) + ) { + return CROSS_SWAP_TYPE.BRIDGEABLE_TO_BRIDGEABLE; } - return address; -} -export function getUniversalSwapAndBridgeAddress(dex: string, chainId: number) { - if (!_isDexSupportedForUniversalSwapAndBridge(dex)) { - throw new UnsupportedDex(dex, "UniversalSwapAndBridge"); + // Prefer destination swap if input token is native because legacy + // `UniversalSwapAndBridge` does not support native tokens as input. + if (params.isInputNative) { + if (isInputTokenBridgeable(params.inputToken, params.originChainId)) { + return CROSS_SWAP_TYPE.BRIDGEABLE_TO_ANY; + } + // We can't bridge native tokens that are not ETH, e.g. MATIC or AZERO. Therefore + // throw until we have periphery contract audited so that it can accept native + // tokens and do an origin swap. + throw new Error( + "Unsupported swap: Input token is native but not bridgeable" + ); } - const address = ( - ENABLED_ROUTES.universalSwapAndBridgeAddresses[dex] as Record< - string, - string - > - )?.[chainId]; - if (!address) { - throw new UnsupportedDexOnChain(chainId, dex, "UniversalSwapAndBridge"); + if (isOutputTokenBridgeable(params.outputToken, params.destinationChainId)) { + return CROSS_SWAP_TYPE.ANY_TO_BRIDGEABLE; } - return address; -} - -export function getSwapAndBridge(dex: string, chainId: number) { - const swapAndBridgeAddress = getSwapAndBridgeAddress(dex, chainId); - return SwapAndBridge__factory.connect( - swapAndBridgeAddress, - getProvider(chainId) - ); -} - -export function getUniversalSwapAndBridge(dex: string, chainId: number) { - const universalSwapAndBridgeAddress = getUniversalSwapAndBridgeAddress( - dex, - chainId - ); - - return UniversalSwapAndBridge__factory.connect( - universalSwapAndBridgeAddress, - getProvider(chainId) - ); -} - -function _isDexSupportedForSwapAndBridge( - dex: string -): dex is keyof typeof ENABLED_ROUTES.swapAndBridgeAddresses { - return swapAndBridgeDexes.includes(dex); -} + if (isInputTokenBridgeable(params.inputToken, params.originChainId)) { + return CROSS_SWAP_TYPE.BRIDGEABLE_TO_ANY; + } -function _isDexSupportedForUniversalSwapAndBridge( - dex: string -): dex is keyof typeof ENABLED_ROUTES.universalSwapAndBridgeAddresses { - return universalSwapAndBridgeDexes.includes(dex); + return CROSS_SWAP_TYPE.ANY_TO_ANY; } /** @@ -212,3 +193,181 @@ export function getFallbackRecipient(crossSwap: CrossSwap) { ? constants.AddressZero : crossSwap.refundAddress ?? crossSwap.depositor; } + +export async function extractDepositDataStruct( + crossSwapQuotes: CrossSwapQuotes +) { + const originChainId = crossSwapQuotes.crossSwap.inputToken.chainId; + const destinationChainId = crossSwapQuotes.crossSwap.outputToken.chainId; + const spokePool = getSpokePool(originChainId); + const message = crossSwapQuotes.bridgeQuote.message || "0x"; + const refundAddress = + crossSwapQuotes.crossSwap.refundAddress ?? + crossSwapQuotes.crossSwap.depositor; + const deposit = { + depositor: crossSwapQuotes.crossSwap.refundOnOrigin + ? refundAddress + : crossSwapQuotes.crossSwap.depositor, + recipient: utils.isMessageEmpty(message) + ? crossSwapQuotes.crossSwap.recipient + : getMultiCallHandlerAddress(destinationChainId), + inputToken: crossSwapQuotes.bridgeQuote.inputToken.address, + outputToken: crossSwapQuotes.bridgeQuote.outputToken.address, + inputAmount: crossSwapQuotes.bridgeQuote.inputAmount, + outputAmount: crossSwapQuotes.bridgeQuote.outputAmount, + destinationChainId, + exclusiveRelayer: + crossSwapQuotes.bridgeQuote.suggestedFees.exclusiveRelayer, + quoteTimestamp: crossSwapQuotes.bridgeQuote.suggestedFees.timestamp, + fillDeadline: await getFillDeadline(spokePool), + exclusivityDeadline: + crossSwapQuotes.bridgeQuote.suggestedFees.exclusivityDeadline, + exclusivityParameter: + crossSwapQuotes.bridgeQuote.suggestedFees.exclusivityDeadline, + message, + }; + return deposit; +} + +async function getFillDeadline(spokePool: SpokePool): Promise { + const calls = [ + spokePool.interface.encodeFunctionData("getCurrentTime"), + spokePool.interface.encodeFunctionData("fillDeadlineBuffer"), + ]; + + const [currentTime, fillDeadlineBuffer] = + await spokePool.callStatic.multicall(calls); + return Number(currentTime) + Number(fillDeadlineBuffer); +} + +export function getQuoteFetchStrategy( + chainId: number, + strategies: QuoteFetchStrategies +) { + return strategies[chainId] ?? strategies.default ?? defaultQuoteFetchStrategy; +} + +export function buildDestinationSwapCrossChainMessage({ + crossSwap, + destinationSwapQuote, + bridgeableOutputToken, + routerAddress, +}: { + crossSwap: CrossSwap; + bridgeableOutputToken: Token; + destinationSwapQuote: SwapQuote; + routerAddress: string; +}) { + const destinationSwapChainId = destinationSwapQuote.tokenOut.chainId; + const isIndicativeQuote = + destinationSwapQuote.swapTx.to === "0x0" && + destinationSwapQuote.swapTx.data === "0x0" && + destinationSwapQuote.swapTx.value === "0x0"; + + let transferActions: { + target: string; + callData: string; + value: string; + }[] = []; + + // If output token is native, we need to unwrap WETH before sending it to the + // recipient. This is because we only handle WETH in the destination swap. + if (crossSwap.isOutputNative) { + transferActions = [ + { + target: crossSwap.outputToken.address, + callData: encodeWethWithdrawCalldata(crossSwap.amount), + value: "0", + }, + { + target: crossSwap.recipient, + callData: "0x", + value: crossSwap.amount.toString(), + }, + ]; + } + // If output token is an ERC-20 token and amount type is EXACT_OUTPUT, we need + // to transfer the EXACT output amount to the recipient. The refundAddress / depositor + // will receive any leftovers. + else if (crossSwap.type === AMOUNT_TYPE.EXACT_OUTPUT) { + transferActions = [ + { + target: crossSwap.outputToken.address, + callData: encodeTransferCalldata(crossSwap.recipient, crossSwap.amount), + value: "0", + }, + { + target: getMultiCallHandlerAddress(destinationSwapChainId), + callData: encodeDrainCalldata( + crossSwap.outputToken.address, + crossSwap.refundAddress ?? crossSwap.depositor + ), + value: "0", + }, + ]; + } + // If output token is an ERC-20 token and amount type is MIN_OUTPUT, we need + // to transfer all realized output tokens to the recipient. + else if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT) { + transferActions = [ + { + target: getMultiCallHandlerAddress(destinationSwapChainId), + callData: encodeDrainCalldata( + crossSwap.outputToken.address, + crossSwap.recipient + ), + value: "0", + }, + ]; + } + + const swapActions = isIndicativeQuote + ? [] + : [ + { + target: destinationSwapQuote.swapTx.to, + callData: destinationSwapQuote.swapTx.data, + value: destinationSwapQuote.swapTx.value, + }, + ]; + + return buildMulticallHandlerMessage({ + fallbackRecipient: getFallbackRecipient(crossSwap), + actions: [ + // approve bridgeable output token + { + target: bridgeableOutputToken.address, + callData: encodeApproveCalldata( + routerAddress, + destinationSwapQuote.maximumAmountIn + ), + value: "0", + }, + // swap bridgeable output token -> cross swap output token + ...swapActions, + // transfer output tokens to recipient + ...transferActions, + // drain remaining bridgeable output tokens from MultiCallHandler contract + { + target: getMultiCallHandlerAddress(destinationSwapChainId), + callData: encodeDrainCalldata( + bridgeableOutputToken.address, + crossSwap.refundAddress ?? crossSwap.depositor + ), + value: "0", + }, + ], + }); +} + +export function assertMinOutputAmount( + amountOut: BigNumber, + expectedMinAmountOut: BigNumber +) { + if (amountOut.lt(expectedMinAmountOut)) { + throw new Error( + `Swap quote output amount ${amountOut.toString()} ` + + `is less than required min. output amount ${expectedMinAmountOut.toString()}` + ); + } +} diff --git a/api/_permit.ts b/api/_permit.ts new file mode 100644 index 000000000..f03f3f799 --- /dev/null +++ b/api/_permit.ts @@ -0,0 +1,147 @@ +import { BigNumberish, ethers } from "ethers"; + +import { getProvider } from "./_utils"; +import { ERC20_PERMIT_ABI } from "./_abis"; + +export async function getPermitTypedData(params: { + tokenAddress: string; + chainId: number; + ownerAddress: string; + spenderAddress: string; + value: BigNumberish; + deadline: number; + eip712DomainVersion?: number; +}) { + const provider = getProvider(params.chainId); + const erc20Permit = new ethers.Contract( + params.tokenAddress, + ERC20_PERMIT_ABI, + provider + ); + + const [ + nameResult, + versionFromContractResult, + nonceResult, + domainSeparatorResult, + ] = await Promise.allSettled([ + erc20Permit.name(), + erc20Permit.version(), + erc20Permit.nonces(params.ownerAddress), + erc20Permit.DOMAIN_SEPARATOR(), + ]); + + if ( + nameResult.status === "rejected" || + nonceResult.status === "rejected" || + domainSeparatorResult.status === "rejected" + ) { + const error = + nameResult.status === "rejected" + ? nameResult.reason + : nonceResult.status === "rejected" + ? nonceResult.reason + : domainSeparatorResult.status === "rejected" + ? domainSeparatorResult.reason + : new Error("Unknown error"); + throw new Error( + `ERC-20 contract ${params.tokenAddress} does not support permit`, + { + cause: error, + } + ); + } + + const name = nameResult.value; + const versionFromContract = + versionFromContractResult.status === "fulfilled" + ? versionFromContractResult.value + : undefined; + const nonce = nonceResult.value; + const domainSeparator = domainSeparatorResult.value; + + const eip712DomainVersion = [1, 2, "1", "2"].includes(versionFromContract) + ? 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, + ] + ) + ); + + if (domainSeparator !== domainSeparatorHash) { + throw new Error("EIP712 domain separator mismatch"); + } + + return { + domainSeparator, + eip712: { + types: { + EIP712Domain: [ + { + name: "name", + type: "string", + }, + { + name: "version", + type: "string", + }, + { + name: "chainId", + type: "uint256", + }, + { + name: "verifyingContract", + type: "address", + }, + ], + Permit: [ + { + name: "owner", + type: "address", + }, + { + name: "spender", + type: "address", + }, + { + name: "value", + type: "uint256", + }, + { + name: "nonce", + type: "uint256", + }, + { + name: "deadline", + type: "uint256", + }, + ], + }, + primaryType: "Permit", + domain: { + name, + version: eip712DomainVersion.toString(), + chainId: params.chainId, + verifyingContract: params.tokenAddress, + }, + message: { + owner: params.ownerAddress, + spender: params.spenderAddress, + value: String(params.value), + nonce: String(nonce), + deadline: String(params.deadline), + }, + }, + }; +} diff --git a/api/_spoke-pool-periphery.ts b/api/_spoke-pool-periphery.ts index 6e568682b..a399f75ca 100644 --- a/api/_spoke-pool-periphery.ts +++ b/api/_spoke-pool-periphery.ts @@ -1,26 +1,122 @@ -import { SpokePoolV3Periphery__factory } from "./_typechain/factories/SpokePoolV3Periphery"; +import { BigNumber } from "ethers"; +import { extractDepositDataStruct } from "./_dexes/utils"; +import { SpokePoolPeripheryProxy__factory } from "./_typechain/factories/SpokePoolPeripheryProxy__factory"; +import { SpokePoolV3Periphery__factory } from "./_typechain/factories/SpokePoolV3Periphery__factory"; import { ENABLED_ROUTES, getProvider } from "./_utils"; -export class UnknownPeripheryForDexOnChain extends Error { - constructor(chainId: number, dex: string) { - super(`Unknown SpokePoolPeriphery for DEX ${dex} on chain ${chainId}`); +const sharedEIP712Types = { + EIP712Domain: [ + { + name: "name", + type: "string", + }, + { + name: "version", + type: "string", + }, + { + name: "chainId", + type: "uint256", + }, + { + name: "verifyingContract", + type: "address", + }, + ], + Fees: [ + { + name: "amount", + type: "uint256", + }, + { + name: "recipient", + type: "address", + }, + ], + BaseDepositData: [ + { + name: "inputToken", + type: "address", + }, + { + name: "outputToken", + type: "address", + }, + { + name: "outputAmount", + type: "uint256", + }, + { + name: "depositor", + type: "address", + }, + { + name: "recipient", + type: "address", + }, + { + name: "destinationChainId", + type: "uint256", + }, + { + name: "exclusiveRelayer", + type: "address", + }, + { + name: "quoteTimestamp", + type: "uint32", + }, + { + name: "fillDeadline", + type: "uint32", + }, + { + name: "exclusivityParameter", + type: "uint32", + }, + { + name: "message", + type: "bytes", + }, + ], +}; + +export class UnknownPeripheryOnChain extends Error { + constructor(chainId: number) { + super(`Unknown 'SpokePoolPeriphery' on chain ${chainId}`); } } -export const spokePoolPeripheryDexes = Object.keys( - ENABLED_ROUTES.spokePoolPeripheryAddresses -); +export class UnknownPeripheryProxyOnChain extends Error { + constructor(chainId: number) { + super(`Unknown 'SpokePoolPeripheryProxy' on chain ${chainId}`); + } +} -export function getSpokePoolPeripheryAddress(dex: string, chainId: number) { - if (!_isDexSupported(dex)) { - throw new UnknownPeripheryForDexOnChain(chainId, dex); +export enum TransferType { + Approval = 0, + Transfer = 1, + Permit2Approval = 2, +} + +export function getSpokePoolPeripheryAddress(chainId: number) { + const address = + ENABLED_ROUTES.spokePoolPeripheryAddresses[ + chainId as keyof typeof ENABLED_ROUTES.spokePoolPeripheryAddresses + ]; + if (!address) { + throw new UnknownPeripheryOnChain(chainId); } + return address; +} - const address = ( - ENABLED_ROUTES.spokePoolPeripheryAddresses[dex] as Record - )?.[chainId]; +export function getSpokePoolPeripheryProxyAddress(chainId: number) { + const address = + ENABLED_ROUTES.spokePoolPeripheryProxyAddresses[ + chainId as keyof typeof ENABLED_ROUTES.spokePoolPeripheryProxyAddresses + ]; if (!address) { - throw new UnknownPeripheryForDexOnChain(chainId, dex); + throw new UnknownPeripheryProxyOnChain(chainId); } return address; } @@ -29,8 +125,138 @@ export function getSpokePoolPeriphery(address: string, chainId: number) { return SpokePoolV3Periphery__factory.connect(address, getProvider(chainId)); } -function _isDexSupported( - dex: string -): dex is keyof typeof ENABLED_ROUTES.spokePoolPeripheryAddresses { - return spokePoolPeripheryDexes.includes(dex); +export function getSpokePoolPeripheryProxy(address: string, chainId: number) { + return SpokePoolPeripheryProxy__factory.connect( + address, + getProvider(chainId) + ); +} + +export async function getDepositTypedData(params: { + depositData: { + submissionFees: { + amount: BigNumber; + recipient: string; + }; + baseDepositData: Awaited>; + inputAmount: BigNumber; + }; + chainId: number; +}) { + const spokePoolPeriphery = getSpokePoolPeriphery( + getSpokePoolPeripheryAddress(params.chainId), + params.chainId + ); + const [domainSeparatorHash, eip712Domain] = await Promise.all([ + spokePoolPeriphery.domainSeparator(), + spokePoolPeriphery.eip712Domain(), + ]); + + return { + domainSeparator: domainSeparatorHash, + eip712: { + types: { + ...sharedEIP712Types, + DepositData: [ + { + name: "submissionFees", + type: "Fees", + }, + { + name: "baseDepositData", + type: "BaseDepositData", + }, + { + name: "inputAmount", + type: "uint256", + }, + ], + }, + primaryType: "DepositData", + domain: { + name: eip712Domain.name, + version: eip712Domain.version, + chainId: params.chainId, + verifyingContract: eip712Domain.verifyingContract, + }, + message: params.depositData, + }, + }; +} + +export async function getSwapAndDepositTypedData(params: { + swapAndDepositData: { + submissionFees: { + amount: BigNumber; + recipient: string; + }; + depositData: Awaited>; + swapToken: string; + exchange: string; + transferType: TransferType; + swapTokenAmount: BigNumber; + minExpectedInputTokenAmount: BigNumber; + routerCalldata: string; + }; + chainId: number; +}) { + const spokePoolPeriphery = getSpokePoolPeriphery( + getSpokePoolPeripheryAddress(params.chainId), + params.chainId + ); + const [domainSeparatorHash, eip712Domain] = await Promise.all([ + spokePoolPeriphery.domainSeparator(), + spokePoolPeriphery.eip712Domain(), + ]); + + return { + domainSeparator: domainSeparatorHash, + eip712: { + types: { + ...sharedEIP712Types, + SwapAndDepositData: [ + { + name: "submissionFees", + type: "Fees", + }, + { + name: "depositData", + type: "BaseDepositData", + }, + { + name: "swapToken", + type: "address", + }, + { + name: "exchange", + type: "address", + }, + { + name: "transferType", + type: "uint8", + }, + { + name: "swapTokenAmount", + type: "uint256", + }, + { + name: "minExpectedInputTokenAmount", + type: "uint256", + }, + { + name: "routerCalldata", + type: "bytes", + }, + ], + }, + primaryType: "SwapAndDepositData", + domain: { + name: eip712Domain.name, + version: eip712Domain.version, + chainId: params.chainId, + verifyingContract: eip712Domain.verifyingContract, + }, + message: params.swapAndDepositData, + }, + }; } diff --git a/api/_swap-and-bridge.ts b/api/_swap-and-bridge.ts new file mode 100644 index 000000000..f57ee5eb5 --- /dev/null +++ b/api/_swap-and-bridge.ts @@ -0,0 +1,108 @@ +import { + SwapAndBridge__factory, + UniversalSwapAndBridge__factory, +} from "@across-protocol/contracts"; + +import { ENABLED_ROUTES, getProvider } from "./_utils"; + +type SwapAndBridgeType = "SwapAndBridge" | "UniversalSwapAndBridge"; + +export class UnsupportedDex extends Error { + constructor(dex: string, type: SwapAndBridgeType) { + super(`DEX/Aggregator '${dex}' not supported for '${type}'`); + } +} + +export class UnsupportedDexOnChain extends Error { + constructor(chainId: number, dex: string, type: SwapAndBridgeType) { + super( + `DEX/Aggregator '${dex}' not supported on chain ${chainId} for '${type}'` + ); + } +} + +export class NoSwapRouteError extends Error { + constructor(args: { + dex: string; + tokenInSymbol: string; + tokenOutSymbol: string; + chainId: number; + swapType: string; + }) { + super( + `No ${args.dex} swap route found for '${args.swapType}' ${args.tokenInSymbol} to ${args.tokenOutSymbol} on chain ${args.chainId}` + ); + } +} + +export const swapAndBridgeDexes = Object.keys( + ENABLED_ROUTES.swapAndBridgeAddresses +); + +export const universalSwapAndBridgeDexes = Object.keys( + ENABLED_ROUTES.universalSwapAndBridgeAddresses +); + +export function getSwapAndBridgeAddress(dex: string, chainId: number) { + if (!_isDexSupportedForSwapAndBridge(dex)) { + throw new UnsupportedDex(dex, "SwapAndBridge"); + } + + const address = ( + ENABLED_ROUTES.swapAndBridgeAddresses[dex] as Record + )?.[chainId]; + if (!address) { + throw new UnsupportedDexOnChain(chainId, dex, "SwapAndBridge"); + } + return address; +} + +export function getUniversalSwapAndBridgeAddress(dex: string, chainId: number) { + if (!_isDexSupportedForUniversalSwapAndBridge(dex)) { + throw new UnsupportedDex(dex, "UniversalSwapAndBridge"); + } + + const address = ( + ENABLED_ROUTES.universalSwapAndBridgeAddresses[dex] as Record< + string, + string + > + )?.[chainId]; + if (!address) { + throw new UnsupportedDexOnChain(chainId, dex, "UniversalSwapAndBridge"); + } + return address; +} + +export function getSwapAndBridge(dex: string, chainId: number) { + const swapAndBridgeAddress = getSwapAndBridgeAddress(dex, chainId); + + return SwapAndBridge__factory.connect( + swapAndBridgeAddress, + getProvider(chainId) + ); +} + +export function getUniversalSwapAndBridge(dex: string, chainId: number) { + const universalSwapAndBridgeAddress = getUniversalSwapAndBridgeAddress( + dex, + chainId + ); + + return UniversalSwapAndBridge__factory.connect( + universalSwapAndBridgeAddress, + getProvider(chainId) + ); +} + +function _isDexSupportedForSwapAndBridge( + dex: string +): dex is keyof typeof ENABLED_ROUTES.swapAndBridgeAddresses { + return swapAndBridgeDexes.includes(dex); +} + +function _isDexSupportedForUniversalSwapAndBridge( + dex: string +): dex is keyof typeof ENABLED_ROUTES.universalSwapAndBridgeAddresses { + return universalSwapAndBridgeDexes.includes(dex); +} diff --git a/api/_typechain/SpokePoolPeripheryProxy.ts b/api/_typechain/SpokePoolPeripheryProxy.ts new file mode 100644 index 000000000..cff3b940f --- /dev/null +++ b/api/_typechain/SpokePoolPeripheryProxy.ts @@ -0,0 +1,268 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +import type { + BaseContract, + BigNumber, + BigNumberish, + BytesLike, + CallOverrides, + ContractTransaction, + Overrides, + PopulatedTransaction, + Signer, + utils, +} from "ethers"; +import type { FunctionFragment, Result } from "@ethersproject/abi"; +import type { Listener, Provider } from "@ethersproject/providers"; +import type { + TypedEventFilter, + TypedEvent, + TypedListener, + OnEvent, +} from "@across-protocol/contracts/dist/typechain/common"; + +export namespace SpokePoolV3PeripheryInterface { + export type FeesStruct = { amount: BigNumberish; recipient: string }; + + export type FeesStructOutput = [BigNumber, string] & { + amount: BigNumber; + recipient: string; + }; + + export type BaseDepositDataStruct = { + inputToken: string; + outputToken: string; + outputAmount: BigNumberish; + depositor: string; + recipient: string; + destinationChainId: BigNumberish; + exclusiveRelayer: string; + quoteTimestamp: BigNumberish; + fillDeadline: BigNumberish; + exclusivityParameter: BigNumberish; + message: BytesLike; + }; + + export type BaseDepositDataStructOutput = [ + string, + string, + BigNumber, + string, + string, + BigNumber, + string, + number, + number, + number, + string, + ] & { + inputToken: string; + outputToken: string; + outputAmount: BigNumber; + depositor: string; + recipient: string; + destinationChainId: BigNumber; + exclusiveRelayer: string; + quoteTimestamp: number; + fillDeadline: number; + exclusivityParameter: number; + message: string; + }; + + export type SwapAndDepositDataStruct = { + submissionFees: SpokePoolV3PeripheryInterface.FeesStruct; + depositData: SpokePoolV3PeripheryInterface.BaseDepositDataStruct; + swapToken: string; + exchange: string; + transferType: BigNumberish; + swapTokenAmount: BigNumberish; + minExpectedInputTokenAmount: BigNumberish; + routerCalldata: BytesLike; + }; + + export type SwapAndDepositDataStructOutput = [ + SpokePoolV3PeripheryInterface.FeesStructOutput, + SpokePoolV3PeripheryInterface.BaseDepositDataStructOutput, + string, + string, + number, + BigNumber, + BigNumber, + string, + ] & { + submissionFees: SpokePoolV3PeripheryInterface.FeesStructOutput; + depositData: SpokePoolV3PeripheryInterface.BaseDepositDataStructOutput; + swapToken: string; + exchange: string; + transferType: number; + swapTokenAmount: BigNumber; + minExpectedInputTokenAmount: BigNumber; + routerCalldata: string; + }; +} + +export interface SpokePoolPeripheryProxyInterface extends utils.Interface { + functions: { + "SPOKE_POOL_PERIPHERY()": FunctionFragment; + "initialize(address)": FunctionFragment; + "multicall(bytes[])": FunctionFragment; + "swapAndBridge(((uint256,address),(address,address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes),address,address,uint8,uint256,uint256,bytes))": FunctionFragment; + }; + + getFunction( + nameOrSignatureOrTopic: + | "SPOKE_POOL_PERIPHERY" + | "initialize" + | "multicall" + | "swapAndBridge" + ): FunctionFragment; + + encodeFunctionData( + functionFragment: "SPOKE_POOL_PERIPHERY", + values?: undefined + ): string; + encodeFunctionData(functionFragment: "initialize", values: [string]): string; + encodeFunctionData( + functionFragment: "multicall", + values: [BytesLike[]] + ): string; + encodeFunctionData( + functionFragment: "swapAndBridge", + values: [SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct] + ): string; + + decodeFunctionResult( + functionFragment: "SPOKE_POOL_PERIPHERY", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "initialize", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "multicall", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "swapAndBridge", + data: BytesLike + ): Result; + + events: {}; +} + +export interface SpokePoolPeripheryProxy extends BaseContract { + connect(signerOrProvider: Signer | Provider | string): this; + attach(addressOrName: string): this; + deployed(): Promise; + + interface: SpokePoolPeripheryProxyInterface; + + queryFilter( + event: TypedEventFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise>; + + listeners( + eventFilter?: TypedEventFilter + ): Array>; + listeners(eventName?: string): Array; + removeAllListeners( + eventFilter: TypedEventFilter + ): this; + removeAllListeners(eventName?: string): this; + off: OnEvent; + on: OnEvent; + once: OnEvent; + removeListener: OnEvent; + + functions: { + SPOKE_POOL_PERIPHERY(overrides?: CallOverrides): Promise<[string]>; + + initialize( + _spokePoolPeriphery: string, + overrides?: Overrides & { from?: string } + ): Promise; + + multicall( + data: BytesLike[], + overrides?: Overrides & { from?: string } + ): Promise; + + swapAndBridge( + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, + overrides?: Overrides & { from?: string } + ): Promise; + }; + + SPOKE_POOL_PERIPHERY(overrides?: CallOverrides): Promise; + + initialize( + _spokePoolPeriphery: string, + overrides?: Overrides & { from?: string } + ): Promise; + + multicall( + data: BytesLike[], + overrides?: Overrides & { from?: string } + ): Promise; + + swapAndBridge( + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, + overrides?: Overrides & { from?: string } + ): Promise; + + callStatic: { + SPOKE_POOL_PERIPHERY(overrides?: CallOverrides): Promise; + + initialize( + _spokePoolPeriphery: string, + overrides?: CallOverrides + ): Promise; + + multicall(data: BytesLike[], overrides?: CallOverrides): Promise; + + swapAndBridge( + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, + overrides?: CallOverrides + ): Promise; + }; + + filters: {}; + + estimateGas: { + SPOKE_POOL_PERIPHERY(overrides?: CallOverrides): Promise; + + initialize( + _spokePoolPeriphery: string, + overrides?: Overrides & { from?: string } + ): Promise; + + multicall( + data: BytesLike[], + overrides?: Overrides & { from?: string } + ): Promise; + + swapAndBridge( + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, + overrides?: Overrides & { from?: string } + ): Promise; + }; + + populateTransaction: { + SPOKE_POOL_PERIPHERY( + overrides?: CallOverrides + ): Promise; + + initialize( + _spokePoolPeriphery: string, + overrides?: Overrides & { from?: string } + ): Promise; + + multicall( + data: BytesLike[], + overrides?: Overrides & { from?: string } + ): Promise; + + swapAndBridge( + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, + overrides?: Overrides & { from?: string } + ): Promise; + }; +} diff --git a/api/_typechain/SpokePoolV3Periphery.ts b/api/_typechain/SpokePoolV3Periphery.ts index ed66cf043..af3321c12 100644 --- a/api/_typechain/SpokePoolV3Periphery.ts +++ b/api/_typechain/SpokePoolV3Periphery.ts @@ -27,13 +27,21 @@ import type { OnEvent, } from "@across-protocol/contracts/dist/typechain/common"; -export declare namespace SpokePoolV3Periphery { - export type DepositDataStruct = { +export declare namespace SpokePoolV3PeripheryInterface { + export type FeesStruct = { amount: BigNumberish; recipient: string }; + + export type FeesStructOutput = [BigNumber, string] & { + amount: BigNumber; + recipient: string; + }; + + export type BaseDepositDataStruct = { + inputToken: string; outputToken: string; outputAmount: BigNumberish; depositor: string; recipient: string; - destinationChainid: BigNumberish; + destinationChainId: BigNumberish; exclusiveRelayer: string; quoteTimestamp: BigNumberish; fillDeadline: BigNumberish; @@ -41,7 +49,8 @@ export declare namespace SpokePoolV3Periphery { message: BytesLike; }; - export type DepositDataStructOutput = [ + export type BaseDepositDataStructOutput = [ + string, string, BigNumber, string, @@ -53,53 +62,134 @@ export declare namespace SpokePoolV3Periphery { number, string, ] & { + inputToken: string; outputToken: string; outputAmount: BigNumber; depositor: string; recipient: string; - destinationChainid: BigNumber; + destinationChainId: BigNumber; exclusiveRelayer: string; quoteTimestamp: number; fillDeadline: number; exclusivityParameter: number; message: string; }; + + export type DepositDataStruct = { + submissionFees: SpokePoolV3PeripheryInterface.FeesStruct; + baseDepositData: SpokePoolV3PeripheryInterface.BaseDepositDataStruct; + inputAmount: BigNumberish; + }; + + export type DepositDataStructOutput = [ + SpokePoolV3PeripheryInterface.FeesStructOutput, + SpokePoolV3PeripheryInterface.BaseDepositDataStructOutput, + BigNumber, + ] & { + submissionFees: SpokePoolV3PeripheryInterface.FeesStructOutput; + baseDepositData: SpokePoolV3PeripheryInterface.BaseDepositDataStructOutput; + inputAmount: BigNumber; + }; + + export type SwapAndDepositDataStruct = { + submissionFees: SpokePoolV3PeripheryInterface.FeesStruct; + depositData: SpokePoolV3PeripheryInterface.BaseDepositDataStruct; + swapToken: string; + exchange: string; + transferType: BigNumberish; + swapTokenAmount: BigNumberish; + minExpectedInputTokenAmount: BigNumberish; + routerCalldata: BytesLike; + }; + + export type SwapAndDepositDataStructOutput = [ + SpokePoolV3PeripheryInterface.FeesStructOutput, + SpokePoolV3PeripheryInterface.BaseDepositDataStructOutput, + string, + string, + number, + BigNumber, + BigNumber, + string, + ] & { + submissionFees: SpokePoolV3PeripheryInterface.FeesStructOutput; + depositData: SpokePoolV3PeripheryInterface.BaseDepositDataStructOutput; + swapToken: string; + exchange: string; + transferType: number; + swapTokenAmount: BigNumber; + minExpectedInputTokenAmount: BigNumber; + routerCalldata: string; + }; +} + +export declare namespace IPermit2 { + export type TokenPermissionsStruct = { token: string; amount: BigNumberish }; + + export type TokenPermissionsStructOutput = [string, BigNumber] & { + token: string; + amount: BigNumber; + }; + + export type PermitTransferFromStruct = { + permitted: IPermit2.TokenPermissionsStruct; + nonce: BigNumberish; + deadline: BigNumberish; + }; + + export type PermitTransferFromStructOutput = [ + IPermit2.TokenPermissionsStructOutput, + BigNumber, + BigNumber, + ] & { + permitted: IPermit2.TokenPermissionsStructOutput; + nonce: BigNumber; + deadline: BigNumber; + }; } export interface SpokePoolV3PeripheryInterface extends utils.Interface { functions: { - "allowedSelectors(bytes4)": FunctionFragment; "deposit(address,address,uint256,uint256,uint256,address,uint32,uint32,uint32,bytes)": FunctionFragment; - "depositWithAuthorization(address,uint256,(address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes),uint256,uint256,bytes32,uint8,bytes32,bytes32)": FunctionFragment; - "depositWithPermit(address,uint256,(address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes),uint256,uint8,bytes32,bytes32)": FunctionFragment; - "exchange()": FunctionFragment; - "initialize(address,address,address)": FunctionFragment; + "depositWithAuthorization(address,((uint256,address),(address,address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes),uint256),uint256,uint256,bytes32,bytes,bytes)": FunctionFragment; + "depositWithPermit(address,((uint256,address),(address,address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes),uint256),uint256,bytes,bytes)": FunctionFragment; + "depositWithPermit2(address,((uint256,address),(address,address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes),uint256),((address,uint256),uint256,uint256),bytes)": FunctionFragment; + "domainSeparator()": FunctionFragment; + "eip712Domain()": FunctionFragment; + "initialize(address,address,address,address)": FunctionFragment; + "isValidSignature(bytes32,bytes)": FunctionFragment; "multicall(bytes[])": FunctionFragment; + "permit2()": FunctionFragment; + "proxy()": FunctionFragment; "spokePool()": FunctionFragment; - "swapAndBridge(address,address,bytes,uint256,uint256,(address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes))": FunctionFragment; - "swapAndBridgeWithAuthorization(address,address,bytes,uint256,uint256,(address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes),uint256,uint256,bytes32,uint8,bytes32,bytes32)": FunctionFragment; - "swapAndBridgeWithPermit(address,address,bytes,uint256,uint256,(address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes),uint256,uint8,bytes32,bytes32)": FunctionFragment; + "swapAndBridge(((uint256,address),(address,address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes),address,address,uint8,uint256,uint256,bytes))": FunctionFragment; + "swapAndBridgeWithAuthorization(address,((uint256,address),(address,address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes),address,address,uint8,uint256,uint256,bytes),uint256,uint256,bytes32,bytes,bytes)": FunctionFragment; + "swapAndBridgeWithPermit(address,((uint256,address),(address,address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes),address,address,uint8,uint256,uint256,bytes),uint256,bytes,bytes)": FunctionFragment; + "swapAndBridgeWithPermit2(address,((uint256,address),(address,address,uint256,address,address,uint256,address,uint32,uint32,uint32,bytes),address,address,uint8,uint256,uint256,bytes),((address,uint256),uint256,uint256),bytes)": FunctionFragment; + "wrappedNativeToken()": FunctionFragment; }; getFunction( nameOrSignatureOrTopic: - | "allowedSelectors" | "deposit" | "depositWithAuthorization" | "depositWithPermit" - | "exchange" + | "depositWithPermit2" + | "domainSeparator" + | "eip712Domain" | "initialize" + | "isValidSignature" | "multicall" + | "permit2" + | "proxy" | "spokePool" | "swapAndBridge" | "swapAndBridgeWithAuthorization" | "swapAndBridgeWithPermit" + | "swapAndBridgeWithPermit2" + | "wrappedNativeToken" ): FunctionFragment; - encodeFunctionData( - functionFragment: "allowedSelectors", - values: [BytesLike] - ): string; encodeFunctionData( functionFragment: "deposit", values: [ @@ -119,12 +209,10 @@ export interface SpokePoolV3PeripheryInterface extends utils.Interface { functionFragment: "depositWithAuthorization", values: [ string, - BigNumberish, - SpokePoolV3Periphery.DepositDataStruct, + SpokePoolV3PeripheryInterface.DepositDataStruct, BigNumberish, BigNumberish, BytesLike, - BigNumberish, BytesLike, BytesLike, ] @@ -133,48 +221,56 @@ export interface SpokePoolV3PeripheryInterface extends utils.Interface { functionFragment: "depositWithPermit", values: [ string, + SpokePoolV3PeripheryInterface.DepositDataStruct, BigNumberish, - SpokePoolV3Periphery.DepositDataStruct, - BigNumberish, - BigNumberish, BytesLike, BytesLike, ] ): string; - encodeFunctionData(functionFragment: "exchange", values?: undefined): string; + encodeFunctionData( + functionFragment: "depositWithPermit2", + values: [ + string, + SpokePoolV3PeripheryInterface.DepositDataStruct, + IPermit2.PermitTransferFromStruct, + BytesLike, + ] + ): string; + encodeFunctionData( + functionFragment: "domainSeparator", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "eip712Domain", + values?: undefined + ): string; encodeFunctionData( functionFragment: "initialize", - values: [string, string, string] + values: [string, string, string, string] + ): string; + encodeFunctionData( + functionFragment: "isValidSignature", + values: [BytesLike, BytesLike] ): string; encodeFunctionData( functionFragment: "multicall", values: [BytesLike[]] ): string; + encodeFunctionData(functionFragment: "permit2", values?: undefined): string; + encodeFunctionData(functionFragment: "proxy", values?: undefined): string; encodeFunctionData(functionFragment: "spokePool", values?: undefined): string; encodeFunctionData( functionFragment: "swapAndBridge", - values: [ - string, - string, - BytesLike, - BigNumberish, - BigNumberish, - SpokePoolV3Periphery.DepositDataStruct, - ] + values: [SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct] ): string; encodeFunctionData( functionFragment: "swapAndBridgeWithAuthorization", values: [ string, - string, - BytesLike, - BigNumberish, - BigNumberish, - SpokePoolV3Periphery.DepositDataStruct, + SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, BigNumberish, BigNumberish, BytesLike, - BigNumberish, BytesLike, BytesLike, ] @@ -183,22 +279,26 @@ export interface SpokePoolV3PeripheryInterface extends utils.Interface { functionFragment: "swapAndBridgeWithPermit", values: [ string, - string, - BytesLike, - BigNumberish, - BigNumberish, - SpokePoolV3Periphery.DepositDataStruct, - BigNumberish, + SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, BigNumberish, BytesLike, BytesLike, ] ): string; + encodeFunctionData( + functionFragment: "swapAndBridgeWithPermit2", + values: [ + string, + SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, + IPermit2.PermitTransferFromStruct, + BytesLike, + ] + ): string; + encodeFunctionData( + functionFragment: "wrappedNativeToken", + values?: undefined + ): string; - decodeFunctionResult( - functionFragment: "allowedSelectors", - data: BytesLike - ): Result; decodeFunctionResult(functionFragment: "deposit", data: BytesLike): Result; decodeFunctionResult( functionFragment: "depositWithAuthorization", @@ -208,9 +308,26 @@ export interface SpokePoolV3PeripheryInterface extends utils.Interface { functionFragment: "depositWithPermit", data: BytesLike ): Result; - decodeFunctionResult(functionFragment: "exchange", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "depositWithPermit2", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "domainSeparator", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "eip712Domain", + data: BytesLike + ): Result; decodeFunctionResult(functionFragment: "initialize", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "isValidSignature", + data: BytesLike + ): Result; decodeFunctionResult(functionFragment: "multicall", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "permit2", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "proxy", data: BytesLike): Result; decodeFunctionResult(functionFragment: "spokePool", data: BytesLike): Result; decodeFunctionResult( functionFragment: "swapAndBridge", @@ -224,16 +341,36 @@ export interface SpokePoolV3PeripheryInterface extends utils.Interface { functionFragment: "swapAndBridgeWithPermit", data: BytesLike ): Result; + decodeFunctionResult( + functionFragment: "swapAndBridgeWithPermit2", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "wrappedNativeToken", + data: BytesLike + ): Result; events: { - "SwapBeforeBridge(address,address,address,uint256,uint256,address,uint256)": EventFragment; + "EIP712DomainChanged()": EventFragment; + "SwapBeforeBridge(address,bytes,address,address,uint256,uint256,address,uint256)": EventFragment; }; + getEvent(nameOrSignatureOrTopic: "EIP712DomainChanged"): EventFragment; getEvent(nameOrSignatureOrTopic: "SwapBeforeBridge"): EventFragment; } +export interface EIP712DomainChangedEventObject {} +export type EIP712DomainChangedEvent = TypedEvent< + [], + EIP712DomainChangedEventObject +>; + +export type EIP712DomainChangedEventFilter = + TypedEventFilter; + export interface SwapBeforeBridgeEventObject { exchange: string; + exchangeCalldata: string; swapToken: string; acrossInputToken: string; swapTokenAmount: BigNumber; @@ -242,7 +379,7 @@ export interface SwapBeforeBridgeEventObject { acrossOutputAmount: BigNumber; } export type SwapBeforeBridgeEvent = TypedEvent< - [string, string, string, BigNumber, BigNumber, string, BigNumber], + [string, string, string, string, BigNumber, BigNumber, string, BigNumber], SwapBeforeBridgeEventObject >; @@ -276,11 +413,6 @@ export interface SpokePoolV3Periphery extends BaseContract { removeListener: OnEvent; functions: { - allowedSelectors( - arg0: BytesLike, - overrides?: CallOverrides - ): Promise<[boolean]>; - deposit( recipient: string, inputToken: string, @@ -296,90 +428,107 @@ export interface SpokePoolV3Periphery extends BaseContract { ): Promise; depositWithAuthorization( - acrossInputToken: string, - acrossInputAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + signatureOwner: string, + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct, validAfter: BigNumberish, validBefore: BigNumberish, nonce: BytesLike, - v: BigNumberish, - r: BytesLike, - s: BytesLike, + receiveWithAuthSignature: BytesLike, + depositDataSignature: BytesLike, overrides?: Overrides & { from?: string } ): Promise; depositWithPermit( - acrossInputToken: string, - acrossInputAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + signatureOwner: string, + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct, deadline: BigNumberish, - v: BigNumberish, - r: BytesLike, - s: BytesLike, + permitSignature: BytesLike, + depositDataSignature: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + depositWithPermit2( + signatureOwner: string, + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct, + permit: IPermit2.PermitTransferFromStruct, + signature: BytesLike, overrides?: Overrides & { from?: string } ): Promise; - exchange(overrides?: CallOverrides): Promise<[string]>; + domainSeparator(overrides?: CallOverrides): Promise<[string]>; + + eip712Domain(overrides?: CallOverrides): Promise< + [string, string, string, BigNumber, string, string, BigNumber[]] & { + fields: string; + name: string; + version: string; + chainId: BigNumber; + verifyingContract: string; + salt: string; + extensions: BigNumber[]; + } + >; initialize( _spokePool: string, _wrappedNativeToken: string, - _exchange: string, + _proxy: string, + _permit2: string, overrides?: Overrides & { from?: string } ): Promise; + isValidSignature( + arg0: BytesLike, + arg1: BytesLike, + overrides?: CallOverrides + ): Promise<[string] & { magicBytes: string }>; + multicall( data: BytesLike[], overrides?: Overrides & { from?: string } ): Promise; + permit2(overrides?: CallOverrides): Promise<[string]>; + + proxy(overrides?: CallOverrides): Promise<[string]>; + spokePool(overrides?: CallOverrides): Promise<[string]>; swapAndBridge( - swapToken: string, - acrossInputToken: string, - routerCalldata: BytesLike, - swapTokenAmount: BigNumberish, - minExpectedInputTokenAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, overrides?: PayableOverrides & { from?: string } ): Promise; swapAndBridgeWithAuthorization( - swapToken: string, - acrossInputToken: string, - routerCalldata: BytesLike, - swapTokenAmount: BigNumberish, - minExpectedInputTokenAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + signatureOwner: string, + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, validAfter: BigNumberish, validBefore: BigNumberish, nonce: BytesLike, - v: BigNumberish, - r: BytesLike, - s: BytesLike, + receiveWithAuthSignature: BytesLike, + swapAndDepositDataSignature: BytesLike, overrides?: Overrides & { from?: string } ): Promise; swapAndBridgeWithPermit( - swapToken: string, - acrossInputToken: string, - routerCalldata: BytesLike, - swapTokenAmount: BigNumberish, - minExpectedInputTokenAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + signatureOwner: string, + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, deadline: BigNumberish, - v: BigNumberish, - r: BytesLike, - s: BytesLike, + permitSignature: BytesLike, + swapAndDepositDataSignature: BytesLike, overrides?: Overrides & { from?: string } ): Promise; - }; - allowedSelectors( - arg0: BytesLike, - overrides?: CallOverrides - ): Promise; + swapAndBridgeWithPermit2( + signatureOwner: string, + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, + permit: IPermit2.PermitTransferFromStruct, + signature: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + wrappedNativeToken(overrides?: CallOverrides): Promise<[string]>; + }; deposit( recipient: string, @@ -396,91 +545,108 @@ export interface SpokePoolV3Periphery extends BaseContract { ): Promise; depositWithAuthorization( - acrossInputToken: string, - acrossInputAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + signatureOwner: string, + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct, validAfter: BigNumberish, validBefore: BigNumberish, nonce: BytesLike, - v: BigNumberish, - r: BytesLike, - s: BytesLike, + receiveWithAuthSignature: BytesLike, + depositDataSignature: BytesLike, overrides?: Overrides & { from?: string } ): Promise; depositWithPermit( - acrossInputToken: string, - acrossInputAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + signatureOwner: string, + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct, deadline: BigNumberish, - v: BigNumberish, - r: BytesLike, - s: BytesLike, + permitSignature: BytesLike, + depositDataSignature: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + depositWithPermit2( + signatureOwner: string, + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct, + permit: IPermit2.PermitTransferFromStruct, + signature: BytesLike, overrides?: Overrides & { from?: string } ): Promise; - exchange(overrides?: CallOverrides): Promise; + domainSeparator(overrides?: CallOverrides): Promise; + + eip712Domain(overrides?: CallOverrides): Promise< + [string, string, string, BigNumber, string, string, BigNumber[]] & { + fields: string; + name: string; + version: string; + chainId: BigNumber; + verifyingContract: string; + salt: string; + extensions: BigNumber[]; + } + >; initialize( _spokePool: string, _wrappedNativeToken: string, - _exchange: string, + _proxy: string, + _permit2: string, overrides?: Overrides & { from?: string } ): Promise; + isValidSignature( + arg0: BytesLike, + arg1: BytesLike, + overrides?: CallOverrides + ): Promise; + multicall( data: BytesLike[], overrides?: Overrides & { from?: string } ): Promise; + permit2(overrides?: CallOverrides): Promise; + + proxy(overrides?: CallOverrides): Promise; + spokePool(overrides?: CallOverrides): Promise; swapAndBridge( - swapToken: string, - acrossInputToken: string, - routerCalldata: BytesLike, - swapTokenAmount: BigNumberish, - minExpectedInputTokenAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, overrides?: PayableOverrides & { from?: string } ): Promise; swapAndBridgeWithAuthorization( - swapToken: string, - acrossInputToken: string, - routerCalldata: BytesLike, - swapTokenAmount: BigNumberish, - minExpectedInputTokenAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + signatureOwner: string, + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, validAfter: BigNumberish, validBefore: BigNumberish, nonce: BytesLike, - v: BigNumberish, - r: BytesLike, - s: BytesLike, + receiveWithAuthSignature: BytesLike, + swapAndDepositDataSignature: BytesLike, overrides?: Overrides & { from?: string } ): Promise; swapAndBridgeWithPermit( - swapToken: string, - acrossInputToken: string, - routerCalldata: BytesLike, - swapTokenAmount: BigNumberish, - minExpectedInputTokenAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + signatureOwner: string, + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, deadline: BigNumberish, - v: BigNumberish, - r: BytesLike, - s: BytesLike, + permitSignature: BytesLike, + swapAndDepositDataSignature: BytesLike, overrides?: Overrides & { from?: string } ): Promise; - callStatic: { - allowedSelectors( - arg0: BytesLike, - overrides?: CallOverrides - ): Promise; + swapAndBridgeWithPermit2( + signatureOwner: string, + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, + permit: IPermit2.PermitTransferFromStruct, + signature: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + wrappedNativeToken(overrides?: CallOverrides): Promise; + callStatic: { deposit( recipient: string, inputToken: string, @@ -496,86 +662,112 @@ export interface SpokePoolV3Periphery extends BaseContract { ): Promise; depositWithAuthorization( - acrossInputToken: string, - acrossInputAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + signatureOwner: string, + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct, validAfter: BigNumberish, validBefore: BigNumberish, nonce: BytesLike, - v: BigNumberish, - r: BytesLike, - s: BytesLike, + receiveWithAuthSignature: BytesLike, + depositDataSignature: BytesLike, overrides?: CallOverrides ): Promise; depositWithPermit( - acrossInputToken: string, - acrossInputAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + signatureOwner: string, + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct, deadline: BigNumberish, - v: BigNumberish, - r: BytesLike, - s: BytesLike, + permitSignature: BytesLike, + depositDataSignature: BytesLike, + overrides?: CallOverrides + ): Promise; + + depositWithPermit2( + signatureOwner: string, + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct, + permit: IPermit2.PermitTransferFromStruct, + signature: BytesLike, overrides?: CallOverrides ): Promise; - exchange(overrides?: CallOverrides): Promise; + domainSeparator(overrides?: CallOverrides): Promise; + + eip712Domain(overrides?: CallOverrides): Promise< + [string, string, string, BigNumber, string, string, BigNumber[]] & { + fields: string; + name: string; + version: string; + chainId: BigNumber; + verifyingContract: string; + salt: string; + extensions: BigNumber[]; + } + >; initialize( _spokePool: string, _wrappedNativeToken: string, - _exchange: string, + _proxy: string, + _permit2: string, overrides?: CallOverrides ): Promise; + isValidSignature( + arg0: BytesLike, + arg1: BytesLike, + overrides?: CallOverrides + ): Promise; + multicall(data: BytesLike[], overrides?: CallOverrides): Promise; + permit2(overrides?: CallOverrides): Promise; + + proxy(overrides?: CallOverrides): Promise; + spokePool(overrides?: CallOverrides): Promise; swapAndBridge( - swapToken: string, - acrossInputToken: string, - routerCalldata: BytesLike, - swapTokenAmount: BigNumberish, - minExpectedInputTokenAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, overrides?: CallOverrides ): Promise; swapAndBridgeWithAuthorization( - swapToken: string, - acrossInputToken: string, - routerCalldata: BytesLike, - swapTokenAmount: BigNumberish, - minExpectedInputTokenAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + signatureOwner: string, + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, validAfter: BigNumberish, validBefore: BigNumberish, nonce: BytesLike, - v: BigNumberish, - r: BytesLike, - s: BytesLike, + receiveWithAuthSignature: BytesLike, + swapAndDepositDataSignature: BytesLike, overrides?: CallOverrides ): Promise; swapAndBridgeWithPermit( - swapToken: string, - acrossInputToken: string, - routerCalldata: BytesLike, - swapTokenAmount: BigNumberish, - minExpectedInputTokenAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + signatureOwner: string, + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, deadline: BigNumberish, - v: BigNumberish, - r: BytesLike, - s: BytesLike, + permitSignature: BytesLike, + swapAndDepositDataSignature: BytesLike, + overrides?: CallOverrides + ): Promise; + + swapAndBridgeWithPermit2( + signatureOwner: string, + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, + permit: IPermit2.PermitTransferFromStruct, + signature: BytesLike, overrides?: CallOverrides ): Promise; + + wrappedNativeToken(overrides?: CallOverrides): Promise; }; filters: { - "SwapBeforeBridge(address,address,address,uint256,uint256,address,uint256)"( + "EIP712DomainChanged()"(): EIP712DomainChangedEventFilter; + EIP712DomainChanged(): EIP712DomainChangedEventFilter; + + "SwapBeforeBridge(address,bytes,address,address,uint256,uint256,address,uint256)"( exchange?: null, + exchangeCalldata?: null, swapToken?: string | null, acrossInputToken?: string | null, swapTokenAmount?: null, @@ -585,6 +777,7 @@ export interface SpokePoolV3Periphery extends BaseContract { ): SwapBeforeBridgeEventFilter; SwapBeforeBridge( exchange?: null, + exchangeCalldata?: null, swapToken?: string | null, acrossInputToken?: string | null, swapTokenAmount?: null, @@ -595,11 +788,6 @@ export interface SpokePoolV3Periphery extends BaseContract { }; estimateGas: { - allowedSelectors( - arg0: BytesLike, - overrides?: CallOverrides - ): Promise; - deposit( recipient: string, inputToken: string, @@ -615,92 +803,99 @@ export interface SpokePoolV3Periphery extends BaseContract { ): Promise; depositWithAuthorization( - acrossInputToken: string, - acrossInputAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + signatureOwner: string, + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct, validAfter: BigNumberish, validBefore: BigNumberish, nonce: BytesLike, - v: BigNumberish, - r: BytesLike, - s: BytesLike, + receiveWithAuthSignature: BytesLike, + depositDataSignature: BytesLike, overrides?: Overrides & { from?: string } ): Promise; depositWithPermit( - acrossInputToken: string, - acrossInputAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + signatureOwner: string, + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct, deadline: BigNumberish, - v: BigNumberish, - r: BytesLike, - s: BytesLike, + permitSignature: BytesLike, + depositDataSignature: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + depositWithPermit2( + signatureOwner: string, + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct, + permit: IPermit2.PermitTransferFromStruct, + signature: BytesLike, overrides?: Overrides & { from?: string } ): Promise; - exchange(overrides?: CallOverrides): Promise; + domainSeparator(overrides?: CallOverrides): Promise; + + eip712Domain(overrides?: CallOverrides): Promise; initialize( _spokePool: string, _wrappedNativeToken: string, - _exchange: string, + _proxy: string, + _permit2: string, overrides?: Overrides & { from?: string } ): Promise; + isValidSignature( + arg0: BytesLike, + arg1: BytesLike, + overrides?: CallOverrides + ): Promise; + multicall( data: BytesLike[], overrides?: Overrides & { from?: string } ): Promise; + permit2(overrides?: CallOverrides): Promise; + + proxy(overrides?: CallOverrides): Promise; + spokePool(overrides?: CallOverrides): Promise; swapAndBridge( - swapToken: string, - acrossInputToken: string, - routerCalldata: BytesLike, - swapTokenAmount: BigNumberish, - minExpectedInputTokenAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, overrides?: PayableOverrides & { from?: string } ): Promise; swapAndBridgeWithAuthorization( - swapToken: string, - acrossInputToken: string, - routerCalldata: BytesLike, - swapTokenAmount: BigNumberish, - minExpectedInputTokenAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + signatureOwner: string, + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, validAfter: BigNumberish, validBefore: BigNumberish, nonce: BytesLike, - v: BigNumberish, - r: BytesLike, - s: BytesLike, + receiveWithAuthSignature: BytesLike, + swapAndDepositDataSignature: BytesLike, overrides?: Overrides & { from?: string } ): Promise; swapAndBridgeWithPermit( - swapToken: string, - acrossInputToken: string, - routerCalldata: BytesLike, - swapTokenAmount: BigNumberish, - minExpectedInputTokenAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + signatureOwner: string, + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, deadline: BigNumberish, - v: BigNumberish, - r: BytesLike, - s: BytesLike, + permitSignature: BytesLike, + swapAndDepositDataSignature: BytesLike, overrides?: Overrides & { from?: string } ): Promise; + + swapAndBridgeWithPermit2( + signatureOwner: string, + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, + permit: IPermit2.PermitTransferFromStruct, + signature: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + wrappedNativeToken(overrides?: CallOverrides): Promise; }; populateTransaction: { - allowedSelectors( - arg0: BytesLike, - overrides?: CallOverrides - ): Promise; - deposit( recipient: string, inputToken: string, @@ -716,83 +911,97 @@ export interface SpokePoolV3Periphery extends BaseContract { ): Promise; depositWithAuthorization( - acrossInputToken: string, - acrossInputAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + signatureOwner: string, + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct, validAfter: BigNumberish, validBefore: BigNumberish, nonce: BytesLike, - v: BigNumberish, - r: BytesLike, - s: BytesLike, + receiveWithAuthSignature: BytesLike, + depositDataSignature: BytesLike, overrides?: Overrides & { from?: string } ): Promise; depositWithPermit( - acrossInputToken: string, - acrossInputAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + signatureOwner: string, + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct, deadline: BigNumberish, - v: BigNumberish, - r: BytesLike, - s: BytesLike, + permitSignature: BytesLike, + depositDataSignature: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + depositWithPermit2( + signatureOwner: string, + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct, + permit: IPermit2.PermitTransferFromStruct, + signature: BytesLike, overrides?: Overrides & { from?: string } ): Promise; - exchange(overrides?: CallOverrides): Promise; + domainSeparator(overrides?: CallOverrides): Promise; + + eip712Domain(overrides?: CallOverrides): Promise; initialize( _spokePool: string, _wrappedNativeToken: string, - _exchange: string, + _proxy: string, + _permit2: string, overrides?: Overrides & { from?: string } ): Promise; + isValidSignature( + arg0: BytesLike, + arg1: BytesLike, + overrides?: CallOverrides + ): Promise; + multicall( data: BytesLike[], overrides?: Overrides & { from?: string } ): Promise; + permit2(overrides?: CallOverrides): Promise; + + proxy(overrides?: CallOverrides): Promise; + spokePool(overrides?: CallOverrides): Promise; swapAndBridge( - swapToken: string, - acrossInputToken: string, - routerCalldata: BytesLike, - swapTokenAmount: BigNumberish, - minExpectedInputTokenAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, overrides?: PayableOverrides & { from?: string } ): Promise; swapAndBridgeWithAuthorization( - swapToken: string, - acrossInputToken: string, - routerCalldata: BytesLike, - swapTokenAmount: BigNumberish, - minExpectedInputTokenAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + signatureOwner: string, + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, validAfter: BigNumberish, validBefore: BigNumberish, nonce: BytesLike, - v: BigNumberish, - r: BytesLike, - s: BytesLike, + receiveWithAuthSignature: BytesLike, + swapAndDepositDataSignature: BytesLike, overrides?: Overrides & { from?: string } ): Promise; swapAndBridgeWithPermit( - swapToken: string, - acrossInputToken: string, - routerCalldata: BytesLike, - swapTokenAmount: BigNumberish, - minExpectedInputTokenAmount: BigNumberish, - depositData: SpokePoolV3Periphery.DepositDataStruct, + signatureOwner: string, + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, deadline: BigNumberish, - v: BigNumberish, - r: BytesLike, - s: BytesLike, + permitSignature: BytesLike, + swapAndDepositDataSignature: BytesLike, overrides?: Overrides & { from?: string } ): Promise; + + swapAndBridgeWithPermit2( + signatureOwner: string, + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct, + permit: IPermit2.PermitTransferFromStruct, + signature: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + wrappedNativeToken( + overrides?: CallOverrides + ): Promise; }; } diff --git a/api/_typechain/factories/SpokePoolPeripheryProxy__factory.ts b/api/_typechain/factories/SpokePoolPeripheryProxy__factory.ts new file mode 100644 index 000000000..863a89c2e --- /dev/null +++ b/api/_typechain/factories/SpokePoolPeripheryProxy__factory.ts @@ -0,0 +1,251 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ + +import { Signer, utils, Contract, ContractFactory, Overrides } from "ethers"; +import type { Provider, TransactionRequest } from "@ethersproject/providers"; +import type { + SpokePoolPeripheryProxy, + SpokePoolPeripheryProxyInterface, +} from "../SpokePoolPeripheryProxy"; + +const _abi = [ + { + inputs: [], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "ContractInitialized", + type: "error", + }, + { + inputs: [], + name: "InvalidPeriphery", + type: "error", + }, + { + inputs: [], + name: "SPOKE_POOL_PERIPHERY", + outputs: [ + { + internalType: "contract SpokePoolV3Periphery", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "contract SpokePoolV3Periphery", + name: "_spokePoolPeriphery", + type: "address", + }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes[]", + name: "data", + type: "bytes[]", + }, + ], + name: "multicall", + outputs: [ + { + internalType: "bytes[]", + name: "results", + type: "bytes[]", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + components: [ + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + ], + internalType: "struct SpokePoolV3PeripheryInterface.Fees", + name: "submissionFees", + type: "tuple", + }, + { + components: [ + { + internalType: "address", + name: "inputToken", + type: "address", + }, + { + internalType: "address", + name: "outputToken", + type: "address", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "address", + name: "depositor", + type: "address", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "destinationChainId", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityParameter", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + internalType: + "struct SpokePoolV3PeripheryInterface.BaseDepositData", + name: "depositData", + type: "tuple", + }, + { + internalType: "address", + name: "swapToken", + type: "address", + }, + { + internalType: "address", + name: "exchange", + type: "address", + }, + { + internalType: "enum SpokePoolV3PeripheryInterface.TransferType", + name: "transferType", + type: "uint8", + }, + { + internalType: "uint256", + name: "swapTokenAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "minExpectedInputTokenAmount", + type: "uint256", + }, + { + internalType: "bytes", + name: "routerCalldata", + type: "bytes", + }, + ], + internalType: "struct SpokePoolV3PeripheryInterface.SwapAndDepositData", + name: "swapAndDepositData", + type: "tuple", + }, + ], + name: "swapAndBridge", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; + +const _bytecode = + "0x6080806040523461002157600160ff195f5416175f55610ee790816100268239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c8063894adf3014610054578063991fe5901461004f578063ac9650d81461004a5763c4d66de814610045575f80fd5b6103e1565b61033b565b610213565b34610205577ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc602081360112610205576004359067ffffffffffffffff821161020557610120908236030112610205576100ac61085f565b6100d77fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b61013e6101026100e96064840161086b565b73ffffffffffffffffffffffffffffffffffffffff1690565b60c48301359061011482303384610c21565b5f546101389060101c73ffffffffffffffffffffffffffffffffffffffff166100e9565b90610c9d565b5f546101629060101c73ffffffffffffffffffffffffffffffffffffffff166100e9565b803b15610205575f6101a88192846040519485809481937f894adf3000000000000000000000000000000000000000000000000000000000835260040160048301610af5565b03925af18015610200576101e7575b6101e560017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f5416175f55565b005b806101f46101fa9261056c565b80610209565b806101b7565b610c16565b5f80fd5b5f91031261020557565b34610205575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261020557602073ffffffffffffffffffffffffffffffffffffffff5f5460101c16604051908152f35b5f5b8381106102775750505f910152565b8181015183820152602001610268565b602080820190808352835180925260408301928160408460051b8301019501935f915b8483106102ba5750505050505090565b909192939495848080837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc086600196030187527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8c5161032781518092818752878088019101610266565b0116010198019301930191949392906102aa565b346102055760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102055767ffffffffffffffff6004358181116102055736602382011215610205578060040135918211610205573660248360051b83010111610205576103bf9160246103b39201610778565b60405191829182610287565b0390f35b73ffffffffffffffffffffffffffffffffffffffff81160361020557565b346102055760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126102055760043561041c816103c3565b61042461085f565b61044f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b5f5460ff8160081c16610515577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff16610100175f5573ffffffffffffffffffffffffffffffffffffffff81163b156104eb576101b7907fffffffffffffffffffff0000000000000000000000000000000000000000ffff75ffffffffffffffffffffffffffffffffffffffff00005f549260101b169116175f55565b60046040517fd07c0ecb000000000000000000000000000000000000000000000000000000008152fd5b60046040517f9f4eefba000000000000000000000000000000000000000000000000000000008152fd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b67ffffffffffffffff811161058057604052565b61053f565b6080810190811067ffffffffffffffff82111761058057604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761058057604052565b67ffffffffffffffff81116105805760051b60200190565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b91908110156106875760051b810135907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18136030182121561020557019081359167ffffffffffffffff8311610205576020018236038113610205579190565b6105fa565b908092918237015f815290565b67ffffffffffffffff811161058057601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b3d156106fd573d906106e482610699565b916106f260405193846105a1565b82523d5f602084013e565b606090565b6020818303126102055780519067ffffffffffffffff8211610205570181601f8201121561020557805161073581610699565b9261074360405194856105a1565b81845260208284010111610205576107619160208085019101610266565b90565b80518210156106875760209160051b010190565b919091610784836105e2565b90604061079460405193846105a1565b8483527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06107c1866105e2565b015f5b81811061084e57505082945f5b8181106107df575050505050565b5f806107ec838588610627565b906107fb87518093819361068c565b0390305af46108086106d3565b901561082e579060019161081c8288610764565b526108278187610764565b50016107d1565b604481511061020557806004610205920151602480918301019101610702565b8060606020809388010152016107c4565b60ff5f54161561020557565b35610761816103c3565b3590610880826103c3565b565b90357ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffea182360301811215610205570190565b359063ffffffff8216820361020557565b90357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe18236030181121561020557016020813591019167ffffffffffffffff821161020557813603831361020557565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe093818652868601375f8582860101520116010190565b6107619161016081610983829361096986610875565b73ffffffffffffffffffffffffffffffffffffffff169052565b6109af61099260208601610875565b73ffffffffffffffffffffffffffffffffffffffff166020830152565b604084013560408201526109e56109c860608601610875565b73ffffffffffffffffffffffffffffffffffffffff166060830152565b610a116109f460808601610875565b73ffffffffffffffffffffffffffffffffffffffff166080830152565b60a084013560a0820152610a47610a2a60c08601610875565b73ffffffffffffffffffffffffffffffffffffffff1660c0830152565b610a63610a5660e086016108b4565b63ffffffff1660e0830152565b610a80610100610a748187016108b4565b63ffffffff1690830152565b610a91610120610a748187016108b4565b610aa161014094858101906108c5565b9390948201520191610915565b3590600382101561020557565b906003821015610ac85752565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffd5b9061076191602081528135602082015273ffffffffffffffffffffffffffffffffffffffff6020830135610b28816103c3565b166040820152610b3b6040830183610882565b610be6610b5661012092836060860152610140850190610953565b93610b83610b6660608301610875565b73ffffffffffffffffffffffffffffffffffffffff166080860152565b610baf610b9260808301610875565b73ffffffffffffffffffffffffffffffffffffffff1660a0860152565b610bc8610bbe60a08301610aae565b60c0860190610abb565b60c081013560e085015261010060e0820135818601528101906108c5565b9290917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe082860301910152610915565b6040513d5f823e3d90fd5b9290604051927f23b872dd00000000000000000000000000000000000000000000000000000000602085015273ffffffffffffffffffffffffffffffffffffffff809216602485015216604483015260648201526064815260a081019181831067ffffffffffffffff8411176105805761088092604052610ddd565b91909160405191602083015f807f095ea7b3000000000000000000000000000000000000000000000000000000009384845273ffffffffffffffffffffffffffffffffffffffff908189166024890152604488015260448752610cff87610585565b85169286519082855af190610d126106d3565b82610d93575b5081610d88575b5015610d2c575b50505050565b604051602081019190915273ffffffffffffffffffffffffffffffffffffffff9390931660248401525f6044808501919091528352610d7f92610d7a90610d746064826105a1565b82610ddd565b610ddd565b5f808080610d26565b90503b15155f610d1f565b80519192508115918215610dab575b5050905f610d18565b610dbe9250602080918301019101610dc5565b5f80610da2565b90816020910312610205575180151581036102055790565b73ffffffffffffffffffffffffffffffffffffffff166040516040810181811067ffffffffffffffff82111761058057610e58937f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c656460205f948594604052818152015260208151910182855af1610e526106d3565b91610e88565b8051908115918215610e6e575b50501561020557565b610e819250602080918301019101610dc5565b5f80610e65565b9015610ea257815115610e99575090565b3b156102055790565b50805190811561020557602001fdfea26469706673582212207b2f1c8c1d5063a21ba98aa274966178205f188d68c4886cd240583330d63c3d64736f6c63430008170033"; + +type SpokePoolPeripheryProxyConstructorParams = + | [signer?: Signer] + | ConstructorParameters; + +const isSuperArgs = ( + xs: SpokePoolPeripheryProxyConstructorParams +): xs is ConstructorParameters => xs.length > 1; + +export class SpokePoolPeripheryProxy__factory extends ContractFactory { + constructor(...args: SpokePoolPeripheryProxyConstructorParams) { + if (isSuperArgs(args)) { + super(...args); + } else { + super(_abi, _bytecode, args[0]); + } + } + + override deploy( + overrides?: Overrides & { from?: string } + ): Promise { + return super.deploy(overrides || {}) as Promise; + } + override getDeployTransaction( + overrides?: Overrides & { from?: string } + ): TransactionRequest { + return super.getDeployTransaction(overrides || {}); + } + override attach(address: string): SpokePoolPeripheryProxy { + return super.attach(address) as SpokePoolPeripheryProxy; + } + override connect(signer: Signer): SpokePoolPeripheryProxy__factory { + return super.connect(signer) as SpokePoolPeripheryProxy__factory; + } + + static readonly bytecode = _bytecode; + static readonly abi = _abi; + static createInterface(): SpokePoolPeripheryProxyInterface { + return new utils.Interface(_abi) as SpokePoolPeripheryProxyInterface; + } + static connect( + address: string, + signerOrProvider: Signer | Provider + ): SpokePoolPeripheryProxy { + return new Contract( + address, + _abi, + signerOrProvider + ) as SpokePoolPeripheryProxy; + } +} diff --git a/api/_typechain/factories/SpokePoolV3Periphery.ts b/api/_typechain/factories/SpokePoolV3Periphery.ts deleted file mode 100644 index 4b85fa045..000000000 --- a/api/_typechain/factories/SpokePoolV3Periphery.ts +++ /dev/null @@ -1,838 +0,0 @@ -/* Autogenerated file. Do not edit manually. */ -/* tslint:disable */ - -import { - Signer, - utils, - Contract, - ContractFactory, - BytesLike, - Overrides, -} from "ethers"; -import type { Provider, TransactionRequest } from "@ethersproject/providers"; -import type { - SpokePoolV3Periphery, - SpokePoolV3PeripheryInterface, -} from "../SpokePoolV3Periphery"; - -const _abi = [ - { - inputs: [ - { - internalType: "bytes4[]", - name: "_allowedSelectors", - type: "bytes4[]", - }, - ], - stateMutability: "nonpayable", - type: "constructor", - }, - { - inputs: [], - name: "ContractInitialized", - type: "error", - }, - { - inputs: [], - name: "InvalidFunctionSelector", - type: "error", - }, - { - inputs: [], - name: "InvalidMsgValue", - type: "error", - }, - { - inputs: [], - name: "InvalidSpokePool", - type: "error", - }, - { - inputs: [], - name: "InvalidSwapToken", - type: "error", - }, - { - inputs: [], - name: "LeftoverSrcTokens", - type: "error", - }, - { - inputs: [], - name: "MinimumExpectedInputAmount", - type: "error", - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: "address", - name: "exchange", - type: "address", - }, - { - indexed: true, - internalType: "address", - name: "swapToken", - type: "address", - }, - { - indexed: true, - internalType: "address", - name: "acrossInputToken", - type: "address", - }, - { - indexed: false, - internalType: "uint256", - name: "swapTokenAmount", - type: "uint256", - }, - { - indexed: false, - internalType: "uint256", - name: "acrossInputAmount", - type: "uint256", - }, - { - indexed: true, - internalType: "address", - name: "acrossOutputToken", - type: "address", - }, - { - indexed: false, - internalType: "uint256", - name: "acrossOutputAmount", - type: "uint256", - }, - ], - name: "SwapBeforeBridge", - type: "event", - }, - { - inputs: [ - { - internalType: "bytes4", - name: "", - type: "bytes4", - }, - ], - name: "allowedSelectors", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "address", - name: "recipient", - type: "address", - }, - { - internalType: "address", - name: "inputToken", - type: "address", - }, - { - internalType: "uint256", - name: "inputAmount", - type: "uint256", - }, - { - internalType: "uint256", - name: "outputAmount", - type: "uint256", - }, - { - internalType: "uint256", - name: "destinationChainId", - type: "uint256", - }, - { - internalType: "address", - name: "exclusiveRelayer", - type: "address", - }, - { - internalType: "uint32", - name: "quoteTimestamp", - type: "uint32", - }, - { - internalType: "uint32", - name: "fillDeadline", - type: "uint32", - }, - { - internalType: "uint32", - name: "exclusivityParameter", - type: "uint32", - }, - { - internalType: "bytes", - name: "message", - type: "bytes", - }, - ], - name: "deposit", - outputs: [], - stateMutability: "payable", - type: "function", - }, - { - inputs: [ - { - internalType: "contract IERC20Auth", - name: "acrossInputToken", - type: "address", - }, - { - internalType: "uint256", - name: "acrossInputAmount", - type: "uint256", - }, - { - components: [ - { - internalType: "address", - name: "outputToken", - type: "address", - }, - { - internalType: "uint256", - name: "outputAmount", - type: "uint256", - }, - { - internalType: "address", - name: "depositor", - type: "address", - }, - { - internalType: "address", - name: "recipient", - type: "address", - }, - { - internalType: "uint256", - name: "destinationChainid", - type: "uint256", - }, - { - internalType: "address", - name: "exclusiveRelayer", - type: "address", - }, - { - internalType: "uint32", - name: "quoteTimestamp", - type: "uint32", - }, - { - internalType: "uint32", - name: "fillDeadline", - type: "uint32", - }, - { - internalType: "uint32", - name: "exclusivityParameter", - type: "uint32", - }, - { - internalType: "bytes", - name: "message", - type: "bytes", - }, - ], - internalType: "struct SpokePoolV3Periphery.DepositData", - name: "depositData", - type: "tuple", - }, - { - internalType: "uint256", - name: "validAfter", - type: "uint256", - }, - { - internalType: "uint256", - name: "validBefore", - type: "uint256", - }, - { - internalType: "bytes32", - name: "nonce", - type: "bytes32", - }, - { - internalType: "uint8", - name: "v", - type: "uint8", - }, - { - internalType: "bytes32", - name: "r", - type: "bytes32", - }, - { - internalType: "bytes32", - name: "s", - type: "bytes32", - }, - ], - name: "depositWithAuthorization", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "contract IERC20Permit", - name: "acrossInputToken", - type: "address", - }, - { - internalType: "uint256", - name: "acrossInputAmount", - type: "uint256", - }, - { - components: [ - { - internalType: "address", - name: "outputToken", - type: "address", - }, - { - internalType: "uint256", - name: "outputAmount", - type: "uint256", - }, - { - internalType: "address", - name: "depositor", - type: "address", - }, - { - internalType: "address", - name: "recipient", - type: "address", - }, - { - internalType: "uint256", - name: "destinationChainid", - type: "uint256", - }, - { - internalType: "address", - name: "exclusiveRelayer", - type: "address", - }, - { - internalType: "uint32", - name: "quoteTimestamp", - type: "uint32", - }, - { - internalType: "uint32", - name: "fillDeadline", - type: "uint32", - }, - { - internalType: "uint32", - name: "exclusivityParameter", - type: "uint32", - }, - { - internalType: "bytes", - name: "message", - type: "bytes", - }, - ], - internalType: "struct SpokePoolV3Periphery.DepositData", - name: "depositData", - type: "tuple", - }, - { - internalType: "uint256", - name: "deadline", - type: "uint256", - }, - { - internalType: "uint8", - name: "v", - type: "uint8", - }, - { - internalType: "bytes32", - name: "r", - type: "bytes32", - }, - { - internalType: "bytes32", - name: "s", - type: "bytes32", - }, - ], - name: "depositWithPermit", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [], - name: "exchange", - outputs: [ - { - internalType: "address", - name: "", - type: "address", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "contract V3SpokePoolInterface", - name: "_spokePool", - type: "address", - }, - { - internalType: "contract WETH9Interface", - name: "_wrappedNativeToken", - type: "address", - }, - { - internalType: "address", - name: "_exchange", - type: "address", - }, - ], - name: "initialize", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "bytes[]", - name: "data", - type: "bytes[]", - }, - ], - name: "multicall", - outputs: [ - { - internalType: "bytes[]", - name: "results", - type: "bytes[]", - }, - ], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [], - name: "spokePool", - outputs: [ - { - internalType: "contract V3SpokePoolInterface", - name: "", - type: "address", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "contract IERC20", - name: "swapToken", - type: "address", - }, - { - internalType: "contract IERC20", - name: "acrossInputToken", - type: "address", - }, - { - internalType: "bytes", - name: "routerCalldata", - type: "bytes", - }, - { - internalType: "uint256", - name: "swapTokenAmount", - type: "uint256", - }, - { - internalType: "uint256", - name: "minExpectedInputTokenAmount", - type: "uint256", - }, - { - components: [ - { - internalType: "address", - name: "outputToken", - type: "address", - }, - { - internalType: "uint256", - name: "outputAmount", - type: "uint256", - }, - { - internalType: "address", - name: "depositor", - type: "address", - }, - { - internalType: "address", - name: "recipient", - type: "address", - }, - { - internalType: "uint256", - name: "destinationChainid", - type: "uint256", - }, - { - internalType: "address", - name: "exclusiveRelayer", - type: "address", - }, - { - internalType: "uint32", - name: "quoteTimestamp", - type: "uint32", - }, - { - internalType: "uint32", - name: "fillDeadline", - type: "uint32", - }, - { - internalType: "uint32", - name: "exclusivityParameter", - type: "uint32", - }, - { - internalType: "bytes", - name: "message", - type: "bytes", - }, - ], - internalType: "struct SpokePoolV3Periphery.DepositData", - name: "depositData", - type: "tuple", - }, - ], - name: "swapAndBridge", - outputs: [], - stateMutability: "payable", - type: "function", - }, - { - inputs: [ - { - internalType: "contract IERC20Auth", - name: "swapToken", - type: "address", - }, - { - internalType: "contract IERC20", - name: "acrossInputToken", - type: "address", - }, - { - internalType: "bytes", - name: "routerCalldata", - type: "bytes", - }, - { - internalType: "uint256", - name: "swapTokenAmount", - type: "uint256", - }, - { - internalType: "uint256", - name: "minExpectedInputTokenAmount", - type: "uint256", - }, - { - components: [ - { - internalType: "address", - name: "outputToken", - type: "address", - }, - { - internalType: "uint256", - name: "outputAmount", - type: "uint256", - }, - { - internalType: "address", - name: "depositor", - type: "address", - }, - { - internalType: "address", - name: "recipient", - type: "address", - }, - { - internalType: "uint256", - name: "destinationChainid", - type: "uint256", - }, - { - internalType: "address", - name: "exclusiveRelayer", - type: "address", - }, - { - internalType: "uint32", - name: "quoteTimestamp", - type: "uint32", - }, - { - internalType: "uint32", - name: "fillDeadline", - type: "uint32", - }, - { - internalType: "uint32", - name: "exclusivityParameter", - type: "uint32", - }, - { - internalType: "bytes", - name: "message", - type: "bytes", - }, - ], - internalType: "struct SpokePoolV3Periphery.DepositData", - name: "depositData", - type: "tuple", - }, - { - internalType: "uint256", - name: "validAfter", - type: "uint256", - }, - { - internalType: "uint256", - name: "validBefore", - type: "uint256", - }, - { - internalType: "bytes32", - name: "nonce", - type: "bytes32", - }, - { - internalType: "uint8", - name: "v", - type: "uint8", - }, - { - internalType: "bytes32", - name: "r", - type: "bytes32", - }, - { - internalType: "bytes32", - name: "s", - type: "bytes32", - }, - ], - name: "swapAndBridgeWithAuthorization", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "contract IERC20Permit", - name: "swapToken", - type: "address", - }, - { - internalType: "contract IERC20", - name: "acrossInputToken", - type: "address", - }, - { - internalType: "bytes", - name: "routerCalldata", - type: "bytes", - }, - { - internalType: "uint256", - name: "swapTokenAmount", - type: "uint256", - }, - { - internalType: "uint256", - name: "minExpectedInputTokenAmount", - type: "uint256", - }, - { - components: [ - { - internalType: "address", - name: "outputToken", - type: "address", - }, - { - internalType: "uint256", - name: "outputAmount", - type: "uint256", - }, - { - internalType: "address", - name: "depositor", - type: "address", - }, - { - internalType: "address", - name: "recipient", - type: "address", - }, - { - internalType: "uint256", - name: "destinationChainid", - type: "uint256", - }, - { - internalType: "address", - name: "exclusiveRelayer", - type: "address", - }, - { - internalType: "uint32", - name: "quoteTimestamp", - type: "uint32", - }, - { - internalType: "uint32", - name: "fillDeadline", - type: "uint32", - }, - { - internalType: "uint32", - name: "exclusivityParameter", - type: "uint32", - }, - { - internalType: "bytes", - name: "message", - type: "bytes", - }, - ], - internalType: "struct SpokePoolV3Periphery.DepositData", - name: "depositData", - type: "tuple", - }, - { - internalType: "uint256", - name: "deadline", - type: "uint256", - }, - { - internalType: "uint8", - name: "v", - type: "uint8", - }, - { - internalType: "bytes32", - name: "r", - type: "bytes32", - }, - { - internalType: "bytes32", - name: "s", - type: "bytes32", - }, - ], - name: "swapAndBridgeWithPermit", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, -] as const; - -const _bytecode = - "0x60406080604052346200011c5762001e1e803803806200001f8162000134565b928339810160209081838203126200011c5782516001600160401b03938482116200011c57019080601f830112156200011c57815193841162000120576005918460051b9084806200007381850162000134565b8098815201928201019283116200011c578401905b828210620000fa57505050600193849360ff19936001855f5416175f555f955b620000be575b604051611cc390816200015b8239f35b8151861015620000f45786809663ffffffff60e01b8382881b86010151165f52818352845f2082888254161790550195620000a8565b620000ae565b81516001600160e01b0319811681036200011c57815290840190840162000088565b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b6040519190601f01601f191682016001600160401b03811183821017620001205760405256fe60806040526004361015610011575f80fd5b5f3560e01c8063038f12ea146100c4578063277deffe146100bf57806327e98fbd146100ba57806385f168eb146100b5578063ac9650d8146100b0578063afdac3d6146100ab578063bdf52ad3146100a6578063c0c53b8b146100a1578063c51e5eb91461009c578063d2f7265a146100975763fdf152d314610092575f80fd5b610b13565b610ac2565b6108a0565b61074d565b6106c0565b61066f565b6105dd565b610481565b61039b565b610224565b610175565b73ffffffffffffffffffffffffffffffffffffffff8116036100e757565b5f80fd5b600435906100f8826100c9565b565b602435906100f8826100c9565b9181601f840112156100e75782359167ffffffffffffffff83116100e757602083818601950101116100e757565b90816101409103126100e75790565b610124359060ff821682036100e757565b60e4359060ff821682036100e757565b6084359060ff821682036100e757565b346100e7576101807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e7576101ad6100eb565b6101b56100fa565b67ffffffffffffffff91906044358381116100e7576101d8903690600401610107565b9260a4359485116100e7576101f4610222953690600401610135565b936101fd610144565b9261016435956101443595610104359460e4359460c435946084359360643593610c95565b005b346100e7576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e757600435610260816100c9565b6102686100fa565b67ffffffffffffffff91906044358381116100e75761028b903690600401610107565b919060a4359485116100e7576102a8610222953690600401610135565b6102b0610155565b926101243595610104359560c435946084359360643593610dc0565b63ffffffff8116036100e757565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b67ffffffffffffffff811161031b57604052565b6102da565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761031b57604052565b67ffffffffffffffff811161031b57601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b6101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e7576004356103d2816100c9565b6024356103de816100c9565b60a4356103ea816100c9565b60c4356103f6816102cc565b60e43590610403826102cc565b6101043592610411846102cc565b610124359567ffffffffffffffff87116100e757366023880112156100e75786600401359561043f87610361565b9661044d6040519889610320565b808852366024828b0101116100e7576020815f9260246102229c01838c013789010152608435916064359160443591610ea0565b346100e75760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e7576004357fffffffff0000000000000000000000000000000000000000000000000000000081168091036100e7575f526001602052602060ff60405f2054166040519015158152f35b5f5b8381106105095750505f910152565b81810151838201526020016104fa565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f602093610555815180928187528780880191016104f8565b0116010190565b6020808201906020835283518092526040830192602060408460051b8301019501935f915b8483106105915750505050505090565b90919293949584806105cd837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc086600196030187528a51610519565b9801930193019194939290610581565b346100e75760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e75767ffffffffffffffff6004358181116100e757366023820112156100e75780600401359182116100e7573660248360051b830101116100e75761066191602461065592016111e4565b6040519182918261055c565b0390f35b5f9103126100e757565b346100e7575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e757602073ffffffffffffffffffffffffffffffffffffffff60025416604051908152f35b346100e7576101207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e7576004356106fc816100c9565b60443567ffffffffffffffff81116100e75761071c903690600401610135565b9060c4359160ff831683036100e75761022292610104359260e4359260a435916084359160643591602435906112cb565b346100e75760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e757600435610788816100c9565b60243590610795826100c9565b604435906107a2826100c9565b60045460ff8160a01c16610876577fffffffffffffffffffffff00000000000000000000000000000000000000000074010000000000000000000000000000000000000000926102229573ffffffffffffffffffffffffffffffffffffffff8092167fffffffffffffffffffffffff00000000000000000000000000000000000000006002541617600255169116171760045573ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000006003541617600355565b60046040517f9f4eefba000000000000000000000000000000000000000000000000000000008152fd5b60c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e757600480356108d7816100c9565b602435906108e4826100c9565b67ffffffffffffffff6044358181116100e7576109049036908601610107565b916064359060a4359081116100e7576109209036908801610135565b926109296113c2565b6109547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b3415610aac57813403610a835773ffffffffffffffffffffffffffffffffffffffff6109b0610997895473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff1690565b9080821690871603610a5a57803b156100e7575f90604051988980927fd0e30db000000000000000000000000000000000000000000000000000000000825234905af1968715610a5557610a0e97610a3c575b505b60843592611421565b61022260017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f5416175f55565b80610a49610a4f92610307565b80610665565b5f610a03565b610db5565b876040517f3539a701000000000000000000000000000000000000000000000000000000008152fd5b866040517f1841b4e1000000000000000000000000000000000000000000000000000000008152fd5b610a0e9650610abd82303388611606565b610a05565b346100e7575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e757602073ffffffffffffffffffffffffffffffffffffffff60035416604051908152f35b346100e7575f60e07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126100e75760043590610b50826100c9565b60243560443567ffffffffffffffff81116100e757610b73903690600401610135565b9073ffffffffffffffffffffffffffffffffffffffff610b91610165565b94610b9a6113c2565b610bc57fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b1693843b156100e7576040517fd505accf00000000000000000000000000000000000000000000000000000000815233600482015230602482015260448101839052606480359082015260ff91909116608482015260a480359082015260c48035908201525f8160e48183895af1610c80575b50610c4f9293610c4a82303384611606565b611765565b610c7d60017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f5416175f55565b80f35b610c4f9350610c8e90610307565b5f92610c38565b73ffffffffffffffffffffffffffffffffffffffff909c9a919b94979295989396999c610cc06113c2565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f5516998a3b156100e7576040517fef55bec6000000000000000000000000000000000000000000000000000000008152336004820152306024820152604481018990526064810193909352608483019390935260a482019b909b5260ff909a1660c48b015260e48a01919091526101048901525f8861012481838a5af1978815610a5557610d7898610da6575b50611421565b6100f860017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f5416175f55565b610daf90610307565b5f610d72565b6040513d5f823e3d90fd5b73ffffffffffffffffffffffffffffffffffffffff909a91999293949596979a610de86113c2565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f551697883b156100e7576040517fd505accf00000000000000000000000000000000000000000000000000000000815233600482015230602482015260448101879052606481019b909b5260ff1660848b015260a48a019190915260c4890152610d78975f8160e481838b5af1610e91575b50610e8c83303389611606565b611421565b610e9a90610307565b5f610e7f565b939298919697909497610eb16113c2565b610edc7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b873403610fce57610f0561099760025473ffffffffffffffffffffffffffffffffffffffff1690565b96873b15610fa457873b156100e7575f99610f51956040519c8d9b8c9a8b9a7f7b939232000000000000000000000000000000000000000000000000000000008c523360048d01610ff8565b039134905af18015610a5557610f91575b506100f860017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f5416175f55565b80610a49610f9e92610307565b5f610f62565b60046040517fb474246c000000000000000000000000000000000000000000000000000000008152fd5b60046040517f1841b4e1000000000000000000000000000000000000000000000000000000008152fd5b9794909361107c9b9a969294999793996101809a73ffffffffffffffffffffffffffffffffffffffff8097818094168d521660208c01521660408a01525f60608a0152608089015260a088015260c08701521660e085015263ffffffff92838092166101008601521661012084015216610140820152816101608201520190610519565b90565b67ffffffffffffffff811161031b5760051b60200190565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1813603018212156100e7570180359067ffffffffffffffff82116100e7576020019181360383136100e757565b908210156111305761112c9160051b8101906110c4565b9091565b611097565b908092918237015f815290565b3d1561116c573d9061115382610361565b916111616040519384610320565b82523d5f602084013e565b606090565b6020818303126100e75780519067ffffffffffffffff82116100e7570181601f820112156100e75780516111a481610361565b926111b26040519485610320565b818452602082840101116100e75761107c91602080850191016104f8565b80518210156111305760209160051b010190565b9190916111f08361107f565b9060406112006040519384610320565b8483527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe061122d8661107f565b015f5b8181106112ba57505082945f5b81811061124b575050505050565b5f80611258838588611115565b90611267875180938193611135565b0390305af4611274611142565b901561129a579060019161128882886111d0565b5261129381876111d0565b500161123d565b60448151106100e7578060046100e7920151602480918301019101611171565b806060602080938801015201611230565b73ffffffffffffffffffffffffffffffffffffffff9098959897949392979691966112f46113c2565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f551694853b156100e7576040517fef55bec6000000000000000000000000000000000000000000000000000000008152336004820152306024820152604481018890526064810193909352608483019390935260a482019790975260ff90961660c487015260e48601919091526101048501525f846101248183855af1938415610a5557610d78946113ac575b50611765565b6113b590610307565b5f6113a6565b156100e757565b60ff5f5416156100e757565b7fffffffff00000000000000000000000000000000000000000000000000000000903581811693926004811061140357505050565b60040360031b82901b16169150565b908160209103126100e7575190565b95949392919061147561147161146a61143a848b6113ce565b7fffffffff00000000000000000000000000000000000000000000000000000000165f52600160205260405f2090565b5460ff1690565b1590565b6115dc576040517f70a082310000000000000000000000000000000000000000000000000000000080825230600483015260209873ffffffffffffffffffffffffffffffffffffffff94919391929091908a816024818c8a165afa948515610a55578b915f966115bd575b5060405190815230600482015295869060249082908d165afa948515610a55576100f89a5f96611584575b50505f61157f92819261153d8661153760035473ffffffffffffffffffffffffffffffffffffffff1690565b8d6118cf565b8261155d60035473ffffffffffffffffffffffffffffffffffffffff1690565b9261156d60405180948193611135565b03925af1611579611142565b506113bb565b6119d9565b5f929650926115ad83928561157f96903d106115b6575b6115a58183610320565b810190611412565b9692509261150b565b503d61159b565b6115d5919650823d84116115b6576115a58183610320565b945f6114e0565b60046040517f42868c9b000000000000000000000000000000000000000000000000000000008152fd5b9290604051927f23b872dd00000000000000000000000000000000000000000000000000000000602085015273ffffffffffffffffffffffffffffffffffffffff809216602485015216604483015260648201526064815260a081019181831067ffffffffffffffff84111761031b576100f892604052611bb0565b3561107c816100c9565b3561107c816102cc565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe093818652868601375f8582860101520116010190565b9895909461107c9d9b97926020956117579a959c976101809d8d73ffffffffffffffffffffffffffffffffffffffff998a8096818096168452169101521660408d01521660608b015260808a015260a089015260c08801521660e086015263ffffffff8092166101008601521661012084015261014083019063ffffffff169052565b816101608201520191611696565b9190916117948361178e61099760025473ffffffffffffffffffffffffffffffffffffffff1690565b836118cf565b6117b661099760025473ffffffffffffffffffffffffffffffffffffffff1690565b916117c360408201611682565b6117cf60608301611682565b916117d981611682565b936117e660a08301611682565b6117f260c0840161168c565b6117fe60e0850161168c565b9061180c610100860161168c565b9261181b6101208701876110c4565b9690958b3b156100e7576040519c8d9b8c809c7f7b939232000000000000000000000000000000000000000000000000000000008252608086013595602001359473ffffffffffffffffffffffffffffffffffffffff16916004019c6118809d6116d4565b03815a5f948591f18015610a55576118955750565b80610a496100f892610307565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b60449192602073ffffffffffffffffffffffffffffffffffffffff604051948580927fdd62ed3e000000000000000000000000000000000000000000000000000000008252306004830152808916602483015286165afa928315610a55575f936119ab575b5082018092116119a6576040517f095ea7b300000000000000000000000000000000000000000000000000000000602082015273ffffffffffffffffffffffffffffffffffffffff9390931660248401526044808401929092529082526100f891906119a1606483610320565b611bb0565b6118a2565b6119c591935060203d6020116115b6576115a58183610320565b915f611934565b919082039182116119a657565b6040517f70a082310000000000000000000000000000000000000000000000000000000080825230600483015273ffffffffffffffffffffffffffffffffffffffff989697959695602095878b16959294939087826024818a5afa8015610a5557611a4b925f91611b93575b506119cc565b978810611b69576040519384523060048501528916928581602481875afa8015610a55578392611a81925f92611b4a57506119cc565b03611b20576100f8977f646284e396b68ff4b4f34e0aa97bcdb9c100f5b44a20da5c475f62703985384191611b18611ace60035473ffffffffffffffffffffffffffffffffffffffff1690565b89611ad88c611682565b9360405195869516998d013592859094939260609273ffffffffffffffffffffffffffffffffffffffff6080840197168352602083015260408201520152565b0390a4611765565b60046040517fd6cf42f0000000000000000000000000000000000000000000000000000000008152fd5b611b62919250883d8a116115b6576115a58183610320565b905f611a45565b60046040517f0492ff87000000000000000000000000000000000000000000000000000000008152fd5b611baa9150893d8b116115b6576115a58183610320565b5f611a45565b73ffffffffffffffffffffffffffffffffffffffff166040516040810181811067ffffffffffffffff82111761031b57611c2b937f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c656460205f948594604052818152015260208151910182855af1611c25611142565b91611c64565b8051908115918215611c41575b5050156100e757565b81925090602091810103126100e7576020015180151581036100e7575f80611c38565b9015611c7e57815115611c75575090565b3b156100e75790565b5080519081156100e757602001fdfea2646970667358221220273204c702747fdff0ffe95c6319ba2f475d4a21c4beb991049bd8c21271b20064736f6c63430008170033"; - -type SpokePoolV3PeripheryConstructorParams = - | [signer?: Signer] - | ConstructorParameters; - -const isSuperArgs = ( - xs: SpokePoolV3PeripheryConstructorParams -): xs is ConstructorParameters => xs.length > 1; - -export class SpokePoolV3Periphery__factory extends ContractFactory { - constructor(...args: SpokePoolV3PeripheryConstructorParams) { - if (isSuperArgs(args)) { - super(...args); - } else { - super(_abi, _bytecode, args[0]); - } - } - - override deploy( - _allowedSelectors: BytesLike[], - overrides?: Overrides & { from?: string } - ): Promise { - return super.deploy( - _allowedSelectors, - overrides || {} - ) as Promise; - } - override getDeployTransaction( - _allowedSelectors: BytesLike[], - overrides?: Overrides & { from?: string } - ): TransactionRequest { - return super.getDeployTransaction(_allowedSelectors, overrides || {}); - } - override attach(address: string): SpokePoolV3Periphery { - return super.attach(address) as SpokePoolV3Periphery; - } - override connect(signer: Signer): SpokePoolV3Periphery__factory { - return super.connect(signer) as SpokePoolV3Periphery__factory; - } - - static readonly bytecode = _bytecode; - static readonly abi = _abi; - static createInterface(): SpokePoolV3PeripheryInterface { - return new utils.Interface(_abi) as SpokePoolV3PeripheryInterface; - } - static connect( - address: string, - signerOrProvider: Signer | Provider - ): SpokePoolV3Periphery { - return new Contract( - address, - _abi, - signerOrProvider - ) as SpokePoolV3Periphery; - } -} diff --git a/api/_typechain/factories/SpokePoolV3Periphery__factory.ts b/api/_typechain/factories/SpokePoolV3Periphery__factory.ts new file mode 100644 index 000000000..7555ff9e6 --- /dev/null +++ b/api/_typechain/factories/SpokePoolV3Periphery__factory.ts @@ -0,0 +1,1433 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ + +import { Signer, utils, Contract, ContractFactory, Overrides } from "ethers"; +import type { Provider, TransactionRequest } from "@ethersproject/providers"; +import type { + SpokePoolV3Periphery, + SpokePoolV3PeripheryInterface, +} from "../SpokePoolV3Periphery"; + +const _abi = [ + { + inputs: [], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "ContractInitialized", + type: "error", + }, + { + inputs: [], + name: "InvalidMsgValue", + type: "error", + }, + { + inputs: [], + name: "InvalidPermit2", + type: "error", + }, + { + inputs: [], + name: "InvalidProxy", + type: "error", + }, + { + inputs: [], + name: "InvalidShortString", + type: "error", + }, + { + inputs: [], + name: "InvalidSignature", + type: "error", + }, + { + inputs: [], + name: "InvalidSignature", + type: "error", + }, + { + inputs: [], + name: "InvalidSignatureLength", + type: "error", + }, + { + inputs: [], + name: "InvalidSpokePool", + type: "error", + }, + { + inputs: [], + name: "InvalidSwapToken", + type: "error", + }, + { + inputs: [], + name: "LeftoverSrcTokens", + type: "error", + }, + { + inputs: [], + name: "MinimumExpectedInputAmount", + type: "error", + }, + { + inputs: [], + name: "NotProxy", + type: "error", + }, + { + inputs: [ + { + internalType: "string", + name: "str", + type: "string", + }, + ], + name: "StringTooLong", + type: "error", + }, + { + anonymous: false, + inputs: [], + name: "EIP712DomainChanged", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "exchange", + type: "address", + }, + { + indexed: false, + internalType: "bytes", + name: "exchangeCalldata", + type: "bytes", + }, + { + indexed: true, + internalType: "address", + name: "swapToken", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "acrossInputToken", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "swapTokenAmount", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "acrossInputAmount", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "acrossOutputToken", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "acrossOutputAmount", + type: "uint256", + }, + ], + name: "SwapBeforeBridge", + type: "event", + }, + { + inputs: [ + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "address", + name: "inputToken", + type: "address", + }, + { + internalType: "uint256", + name: "inputAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "destinationChainId", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityParameter", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + name: "deposit", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "signatureOwner", + type: "address", + }, + { + components: [ + { + components: [ + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + ], + internalType: "struct SpokePoolV3PeripheryInterface.Fees", + name: "submissionFees", + type: "tuple", + }, + { + components: [ + { + internalType: "address", + name: "inputToken", + type: "address", + }, + { + internalType: "address", + name: "outputToken", + type: "address", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "address", + name: "depositor", + type: "address", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "destinationChainId", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityParameter", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + internalType: + "struct SpokePoolV3PeripheryInterface.BaseDepositData", + name: "baseDepositData", + type: "tuple", + }, + { + internalType: "uint256", + name: "inputAmount", + type: "uint256", + }, + ], + internalType: "struct SpokePoolV3PeripheryInterface.DepositData", + name: "depositData", + type: "tuple", + }, + { + internalType: "uint256", + name: "validAfter", + type: "uint256", + }, + { + internalType: "uint256", + name: "validBefore", + type: "uint256", + }, + { + internalType: "bytes32", + name: "nonce", + type: "bytes32", + }, + { + internalType: "bytes", + name: "receiveWithAuthSignature", + type: "bytes", + }, + { + internalType: "bytes", + name: "depositDataSignature", + type: "bytes", + }, + ], + name: "depositWithAuthorization", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "signatureOwner", + type: "address", + }, + { + components: [ + { + components: [ + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + ], + internalType: "struct SpokePoolV3PeripheryInterface.Fees", + name: "submissionFees", + type: "tuple", + }, + { + components: [ + { + internalType: "address", + name: "inputToken", + type: "address", + }, + { + internalType: "address", + name: "outputToken", + type: "address", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "address", + name: "depositor", + type: "address", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "destinationChainId", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityParameter", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + internalType: + "struct SpokePoolV3PeripheryInterface.BaseDepositData", + name: "baseDepositData", + type: "tuple", + }, + { + internalType: "uint256", + name: "inputAmount", + type: "uint256", + }, + ], + internalType: "struct SpokePoolV3PeripheryInterface.DepositData", + name: "depositData", + type: "tuple", + }, + { + internalType: "uint256", + name: "deadline", + type: "uint256", + }, + { + internalType: "bytes", + name: "permitSignature", + type: "bytes", + }, + { + internalType: "bytes", + name: "depositDataSignature", + type: "bytes", + }, + ], + name: "depositWithPermit", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "signatureOwner", + type: "address", + }, + { + components: [ + { + components: [ + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + ], + internalType: "struct SpokePoolV3PeripheryInterface.Fees", + name: "submissionFees", + type: "tuple", + }, + { + components: [ + { + internalType: "address", + name: "inputToken", + type: "address", + }, + { + internalType: "address", + name: "outputToken", + type: "address", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "address", + name: "depositor", + type: "address", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "destinationChainId", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityParameter", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + internalType: + "struct SpokePoolV3PeripheryInterface.BaseDepositData", + name: "baseDepositData", + type: "tuple", + }, + { + internalType: "uint256", + name: "inputAmount", + type: "uint256", + }, + ], + internalType: "struct SpokePoolV3PeripheryInterface.DepositData", + name: "depositData", + type: "tuple", + }, + { + components: [ + { + components: [ + { + internalType: "address", + name: "token", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + internalType: "struct IPermit2.TokenPermissions", + name: "permitted", + type: "tuple", + }, + { + internalType: "uint256", + name: "nonce", + type: "uint256", + }, + { + internalType: "uint256", + name: "deadline", + type: "uint256", + }, + ], + internalType: "struct IPermit2.PermitTransferFrom", + name: "permit", + type: "tuple", + }, + { + internalType: "bytes", + name: "signature", + type: "bytes", + }, + ], + name: "depositWithPermit2", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "domainSeparator", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "eip712Domain", + outputs: [ + { + internalType: "bytes1", + name: "fields", + type: "bytes1", + }, + { + internalType: "string", + name: "name", + type: "string", + }, + { + internalType: "string", + name: "version", + type: "string", + }, + { + internalType: "uint256", + name: "chainId", + type: "uint256", + }, + { + internalType: "address", + name: "verifyingContract", + type: "address", + }, + { + internalType: "bytes32", + name: "salt", + type: "bytes32", + }, + { + internalType: "uint256[]", + name: "extensions", + type: "uint256[]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "contract V3SpokePoolInterface", + name: "_spokePool", + type: "address", + }, + { + internalType: "contract WETH9Interface", + name: "_wrappedNativeToken", + type: "address", + }, + { + internalType: "address", + name: "_proxy", + type: "address", + }, + { + internalType: "contract IPermit2", + name: "_permit2", + type: "address", + }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + { + internalType: "bytes", + name: "", + type: "bytes", + }, + ], + name: "isValidSignature", + outputs: [ + { + internalType: "bytes4", + name: "magicBytes", + type: "bytes4", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes[]", + name: "data", + type: "bytes[]", + }, + ], + name: "multicall", + outputs: [ + { + internalType: "bytes[]", + name: "results", + type: "bytes[]", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "permit2", + outputs: [ + { + internalType: "contract IPermit2", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "proxy", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "spokePool", + outputs: [ + { + internalType: "contract V3SpokePoolInterface", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { + components: [ + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + ], + internalType: "struct SpokePoolV3PeripheryInterface.Fees", + name: "submissionFees", + type: "tuple", + }, + { + components: [ + { + internalType: "address", + name: "inputToken", + type: "address", + }, + { + internalType: "address", + name: "outputToken", + type: "address", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "address", + name: "depositor", + type: "address", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "destinationChainId", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityParameter", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + internalType: + "struct SpokePoolV3PeripheryInterface.BaseDepositData", + name: "depositData", + type: "tuple", + }, + { + internalType: "address", + name: "swapToken", + type: "address", + }, + { + internalType: "address", + name: "exchange", + type: "address", + }, + { + internalType: "enum SpokePoolV3PeripheryInterface.TransferType", + name: "transferType", + type: "uint8", + }, + { + internalType: "uint256", + name: "swapTokenAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "minExpectedInputTokenAmount", + type: "uint256", + }, + { + internalType: "bytes", + name: "routerCalldata", + type: "bytes", + }, + ], + internalType: "struct SpokePoolV3PeripheryInterface.SwapAndDepositData", + name: "swapAndDepositData", + type: "tuple", + }, + ], + name: "swapAndBridge", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "signatureOwner", + type: "address", + }, + { + components: [ + { + components: [ + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + ], + internalType: "struct SpokePoolV3PeripheryInterface.Fees", + name: "submissionFees", + type: "tuple", + }, + { + components: [ + { + internalType: "address", + name: "inputToken", + type: "address", + }, + { + internalType: "address", + name: "outputToken", + type: "address", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "address", + name: "depositor", + type: "address", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "destinationChainId", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityParameter", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + internalType: + "struct SpokePoolV3PeripheryInterface.BaseDepositData", + name: "depositData", + type: "tuple", + }, + { + internalType: "address", + name: "swapToken", + type: "address", + }, + { + internalType: "address", + name: "exchange", + type: "address", + }, + { + internalType: "enum SpokePoolV3PeripheryInterface.TransferType", + name: "transferType", + type: "uint8", + }, + { + internalType: "uint256", + name: "swapTokenAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "minExpectedInputTokenAmount", + type: "uint256", + }, + { + internalType: "bytes", + name: "routerCalldata", + type: "bytes", + }, + ], + internalType: "struct SpokePoolV3PeripheryInterface.SwapAndDepositData", + name: "swapAndDepositData", + type: "tuple", + }, + { + internalType: "uint256", + name: "validAfter", + type: "uint256", + }, + { + internalType: "uint256", + name: "validBefore", + type: "uint256", + }, + { + internalType: "bytes32", + name: "nonce", + type: "bytes32", + }, + { + internalType: "bytes", + name: "receiveWithAuthSignature", + type: "bytes", + }, + { + internalType: "bytes", + name: "swapAndDepositDataSignature", + type: "bytes", + }, + ], + name: "swapAndBridgeWithAuthorization", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "signatureOwner", + type: "address", + }, + { + components: [ + { + components: [ + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + ], + internalType: "struct SpokePoolV3PeripheryInterface.Fees", + name: "submissionFees", + type: "tuple", + }, + { + components: [ + { + internalType: "address", + name: "inputToken", + type: "address", + }, + { + internalType: "address", + name: "outputToken", + type: "address", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "address", + name: "depositor", + type: "address", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "destinationChainId", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityParameter", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + internalType: + "struct SpokePoolV3PeripheryInterface.BaseDepositData", + name: "depositData", + type: "tuple", + }, + { + internalType: "address", + name: "swapToken", + type: "address", + }, + { + internalType: "address", + name: "exchange", + type: "address", + }, + { + internalType: "enum SpokePoolV3PeripheryInterface.TransferType", + name: "transferType", + type: "uint8", + }, + { + internalType: "uint256", + name: "swapTokenAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "minExpectedInputTokenAmount", + type: "uint256", + }, + { + internalType: "bytes", + name: "routerCalldata", + type: "bytes", + }, + ], + internalType: "struct SpokePoolV3PeripheryInterface.SwapAndDepositData", + name: "swapAndDepositData", + type: "tuple", + }, + { + internalType: "uint256", + name: "deadline", + type: "uint256", + }, + { + internalType: "bytes", + name: "permitSignature", + type: "bytes", + }, + { + internalType: "bytes", + name: "swapAndDepositDataSignature", + type: "bytes", + }, + ], + name: "swapAndBridgeWithPermit", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "signatureOwner", + type: "address", + }, + { + components: [ + { + components: [ + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + ], + internalType: "struct SpokePoolV3PeripheryInterface.Fees", + name: "submissionFees", + type: "tuple", + }, + { + components: [ + { + internalType: "address", + name: "inputToken", + type: "address", + }, + { + internalType: "address", + name: "outputToken", + type: "address", + }, + { + internalType: "uint256", + name: "outputAmount", + type: "uint256", + }, + { + internalType: "address", + name: "depositor", + type: "address", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "destinationChainId", + type: "uint256", + }, + { + internalType: "address", + name: "exclusiveRelayer", + type: "address", + }, + { + internalType: "uint32", + name: "quoteTimestamp", + type: "uint32", + }, + { + internalType: "uint32", + name: "fillDeadline", + type: "uint32", + }, + { + internalType: "uint32", + name: "exclusivityParameter", + type: "uint32", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + internalType: + "struct SpokePoolV3PeripheryInterface.BaseDepositData", + name: "depositData", + type: "tuple", + }, + { + internalType: "address", + name: "swapToken", + type: "address", + }, + { + internalType: "address", + name: "exchange", + type: "address", + }, + { + internalType: "enum SpokePoolV3PeripheryInterface.TransferType", + name: "transferType", + type: "uint8", + }, + { + internalType: "uint256", + name: "swapTokenAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "minExpectedInputTokenAmount", + type: "uint256", + }, + { + internalType: "bytes", + name: "routerCalldata", + type: "bytes", + }, + ], + internalType: "struct SpokePoolV3PeripheryInterface.SwapAndDepositData", + name: "swapAndDepositData", + type: "tuple", + }, + { + components: [ + { + components: [ + { + internalType: "address", + name: "token", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + internalType: "struct IPermit2.TokenPermissions", + name: "permitted", + type: "tuple", + }, + { + internalType: "uint256", + name: "nonce", + type: "uint256", + }, + { + internalType: "uint256", + name: "deadline", + type: "uint256", + }, + ], + internalType: "struct IPermit2.PermitTransferFrom", + name: "permit", + type: "tuple", + }, + { + internalType: "bytes", + name: "signature", + type: "bytes", + }, + ], + name: "swapAndBridgeWithPermit2", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "wrappedNativeToken", + outputs: [ + { + internalType: "contract WETH9Interface", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, +] as const; + +const _bytecode = + "0x61016080604052346200016f57620000178162000173565b60138152602081017f4143524f53532d56332d50455249504845525900000000000000000000000000815260405191620000518362000173565b6005835260208301640312e302e360dc1b8152600160ff195f5416175f556200007a826200018f565b926101209384526200008c8562000358565b92610140938452519020938460e05251902091610100938385524660a0526040519360208501917f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f8352604086015260608501524660808501523060a085015260a0845260c084019380851060018060401b038611176200015b57846040525190206080523060c052613ead9384620005068539608051846138f5015260a051846139aa015260c051846138c6015260e051846139440152518361396a01525182610aef01525181610b180152f35b634e487b7160e01b5f52604160045260245ffd5b5f80fd5b604081019081106001600160401b038211176200015b57604052565b805160209081811015620002295750601f825111620001ca5780825192015190808310620001bc57501790565b825f19910360031b1b161790565b90604051809263305a27a960e01b82528060048301528251908160248401525f935b8285106200020f575050604492505f838284010152601f80199101168101030190fd5b8481018201518686016044015293810193859350620001ec565b9192916001600160401b0381116200015b5760019182548381811c911680156200034d575b828210146200033957601f811162000303575b5080601f83116001146200029f5750819293945f9262000293575b50505f19600383901b1c191690821b17905560ff90565b015190505f806200027c565b90601f19831695845f52825f20925f905b888210620002eb5750508385969710620002d2575b505050811b01905560ff90565b01515f1960f88460031b161c191690555f8080620002c5565b808785968294968601518155019501930190620002b0565b835f5283601f835f20920160051c820191601f850160051c015b8281106200032d57505062000261565b5f81550184906200031d565b634e487b7160e01b5f52602260045260245ffd5b90607f16906200024e565b805160209081811015620003e45750601f825111620003855780825192015190808310620001bc57501790565b90604051809263305a27a960e01b82528060048301528251908160248401525f935b828510620003ca575050604492505f838284010152601f80199101168101030190fd5b8481018201518686016044015293810193859350620003a7565b906001600160401b0382116200015b57600254926001938481811c91168015620004fa575b838210146200033957601f8111620004c3575b5081601f84116001146200045b57509282939183925f946200044f575b50501b915f199060031b1c19161760025560ff90565b015192505f8062000439565b919083601f19811660025f52845f20945f905b88838310620004a857505050106200048f575b505050811b0160025560ff90565b01515f1960f88460031b161c191690555f808062000481565b8587015188559096019594850194879350908101906200046e565b60025f5284601f845f20920160051c820191601f860160051c015b828110620004ee5750506200041c565b5f8155018590620004de565b90607f16906200040956fe60806040526004361015610011575f80fd5b5f3560e01c806312261ee7146101245780631626ba7e1461011f57806317fcb39b1461011a57806327e98fbd146101155780632e9b8eff146101105780633cbdb8771461010b57806364b39f181461010657806384b0196e1461010157806386b13dae146100fc578063894adf30146100f7578063ac9650d8146100f2578063afdac3d6146100ed578063ec556889146100e8578063f698da25146100e3578063f8c8765e146100de578063f9760e41146100d95763f980d868146100d4575f80fd5b611650565b611479565b61119f565b61115f565b61110e565b6110bd565b611039565b610df8565b610be9565b610ab9565b6109b0565b6106b7565b6105d5565b610506565b6102c1565b6101b6565b610137565b5f91031261013357565b5f80fd5b34610133575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013357602073ffffffffffffffffffffffffffffffffffffffff60055416604051908152f35b9181601f840112156101335782359167ffffffffffffffff8311610133576020838186019501011161013357565b346101335760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101335760243567ffffffffffffffff811161013357610205903690600401610188565b505073ffffffffffffffffffffffffffffffffffffffff600554163314806102b2575b15610289576102857f1626ba7e000000000000000000000000000000000000000000000000000000005b6040517fffffffff0000000000000000000000000000000000000000000000000000000090911681529081906020820190565b0390f35b6102857fffffffff00000000000000000000000000000000000000000000000000000000610252565b5060ff60065460d81c16610228565b34610133575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013357602073ffffffffffffffffffffffffffffffffffffffff60045416604051908152f35b73ffffffffffffffffffffffffffffffffffffffff81160361013357565b63ffffffff81160361013357565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b67ffffffffffffffff811161037f57604052565b61033e565b6040810190811067ffffffffffffffff82111761037f57604052565b6080810190811067ffffffffffffffff82111761037f57604052565b6060810190811067ffffffffffffffff82111761037f57604052565b6020810190811067ffffffffffffffff82111761037f57604052565b60a0810190811067ffffffffffffffff82111761037f57604052565b60c0810190811067ffffffffffffffff82111761037f57604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761037f57604052565b6040519061047a82610384565b565b6040519061047a826103a0565b6040519061047a826103bc565b67ffffffffffffffff811161037f57601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200190565b9291926104dc82610496565b916104ea604051938461042c565b829481845281830111610133578281602093845f960137010152565b6101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101335760043561053d81610312565b60243561054981610312565b60a43561055581610312565b60c43561056181610330565b60e4359061056e82610330565b610104359261057c84610330565b610124359567ffffffffffffffff87116101335736602388011215610133576105b26105c49736906024816004013591016104d0565b956084359160643591604435916117ec565b005b90816101209103126101335790565b346101335760e07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101335760043561061081610312565b67ffffffffffffffff90602435828111610133576106329036906004016105c6565b60a4358381116101335761064a903690600401610188565b9060c435948511610133576106666105c4953690600401610188565b9490936084359160643591604435916119ac565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbc608091011261013357604490565b908160809103126101335790565b346101335760a07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610133576004356106f281610312565b67ffffffffffffffff90602435828111610133576107149036906004016106a9565b6064358381116101335761072c903690600401610188565b909360843590811161013357610749610784913690600401610188565b9290956107546127a6565b61077f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b6127b2565b949192909560408501936107a061079b8688611b79565b611b30565b976060870135978735916107b660208a01611b30565b936107c18b85611b67565b9173ffffffffffffffffffffffffffffffffffffffff8d1693843b15610133576040517fd505accf00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8b166004820152306024820152604480820186905235606482015260ff91909116608482015260a481019290925260c48201525f8160e48183875af1610997575b5086309061086a93613406565b6108749189612812565b61087d85613470565b61088693612a64565b6108908183611b79565b60600161089c90611b30565b916108a78282611b79565b6080016108b390611b30565b936108be8383611b79565b6020016108ca90611b30565b906108d58484611b79565b604001356108e38585611b79565b60a00135906108f28686611b79565b60c0016108fe90611b30565b926109098787611b79565b60e00161091590611bac565b946109208888611b79565b6101000161092d90611bac565b966109388982611b79565b6101200161094590611bac565b9861094f91611b79565b610140810161095d91611bb6565b9a6109699c919a6134e8565b6105c460017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f5416175f55565b806109a46109aa9261036b565b80610129565b5f61085d565b346101335760e07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610133576004356109eb81610312565b67ffffffffffffffff9060243582811161013357610a0d9036906004016106a9565b60a43583811161013357610a25903690600401610188565b9060c43594851161013357610a416105c4953690600401610188565b949093608435916064359160443591611c07565b5f5b838110610a665750505f910152565b8181015183820152602001610a57565b907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f602093610ab281518092818752878088019101610a55565b0116010190565b34610133575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013357610b8c610b137f0000000000000000000000000000000000000000000000000000000000000000613663565b610b3c7f0000000000000000000000000000000000000000000000000000000000000000613798565b60405190610b49826103d8565b5f8252610b9a6020916040519586957f0f00000000000000000000000000000000000000000000000000000000000000875260e0602088015260e0870190610a76565b908582036040870152610a76565b4660608501523060808501525f60a085015283810360c0850152602080845192838152019301915f5b828110610bd257505050500390f35b835185528695509381019392810192600101610bc3565b346101335760e07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013357600435610c2481610312565b67ffffffffffffffff60243581811161013357610c459036906004016106a9565b90610c4f3661067a565b9060c43590811161013357610c68903690600401610188565b610c739491946127a6565b610c9e7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b610ca784613470565b91843593606086013596610cbb8689611b67565b90610cc461046d565b308152916020830152610d08610cef60055473ffffffffffffffffffffffffffffffffffffffff1690565b73ffffffffffffffffffffffffffffffffffffffff1690565b92610d116121fc565b843b15610133575f968793610d56926040519a8b998a9889977f137c29fe000000000000000000000000000000000000000000000000000000008952600489016122ec565b03925af18015610df357610de0575b506040820190610d758284611b79565b610d7e90611b30565b90610d8b60208501611b30565b610d9492612812565b610d9e8183611b79565b606001610daa90611b30565b90610db58184611b79565b608001610dc190611b30565b92610dcc8282611b79565b610dd590611b30565b946108be8383611b79565b806109a4610ded9261036b565b5f610d65565b6119a1565b60207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610133576004803567ffffffffffffffff811161013357610e4290369083016105c6565b610e4a6127a6565b610e757fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b3415610f855760c08101353403610f5c57610e9260608201611b30565b610eb3610cef845473ffffffffffffffffffffffffffffffffffffffff1690565b9073ffffffffffffffffffffffffffffffffffffffff808316911603610f3357803b15610133575f90604051938480927fd0e30db000000000000000000000000000000000000000000000000000000000825234905af1918215610df35761096992610f20575b50612c31565b806109a4610f2d9261036b565b5f610f1a565b826040517f3539a701000000000000000000000000000000000000000000000000000000008152fd5b506040517f1841b4e1000000000000000000000000000000000000000000000000000000008152fd5b6109699150610f92613864565b610fb3610fa4610cef60608401611b30565b60c08301359030903390613406565b612c31565b6020808201906020835283518092526040830192602060408460051b8301019501935f915b848310610fed5750505050505090565b9091929394958480611029837fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc086600196030187528a51610a76565b9801930193019194939290610fdd565b346101335760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101335767ffffffffffffffff6004358181116101335736602382011215610133578060040135918211610133573660248360051b83010111610133576102859160246110b1920161247a565b60405191829182610fb8565b34610133575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013357602073ffffffffffffffffffffffffffffffffffffffff60035416604051908152f35b34610133575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261013357602073ffffffffffffffffffffffffffffffffffffffff60065416604051908152f35b34610133575f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101335760206111976138af565b604051908152f35b346101335760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610133576004356111da81610312565b6024356111e681610312565b604435906111f382610312565b6064359261120084610312565b6112086127a6565b6112337fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b60065460d01c60ff1661144f5761128a7a0100000000000000000000000000000000000000000000000000007fffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffff6006541617600655565b73ffffffffffffffffffffffffffffffffffffffff918183163b15611425576112f16113329273ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000006003541617600355565b73ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000006004541617600455565b813b156113fb576113816113889273ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000006006541617600655565b82163b1590565b6113d1576109699073ffffffffffffffffffffffffffffffffffffffff167fffffffffffffffffffffffff00000000000000000000000000000000000000006005541617600555565b60046040517f32d1c8da000000000000000000000000000000000000000000000000000000008152fd5b60046040517fb9e5cf7c000000000000000000000000000000000000000000000000000000008152fd5b60046040517fb474246c000000000000000000000000000000000000000000000000000000008152fd5b60046040517f9f4eefba000000000000000000000000000000000000000000000000000000008152fd5b346101335760a07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610133576004356114b481610312565b67ffffffffffffffff90602435828111610133576114d69036906004016105c6565b90606435838111610133576114ef903690600401610188565b90936084359081116101335761150c611542913690600401610188565b9290956115176127a6565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f556127b2565b9591939061155260608701611b30565b86359061156160208901611b30565b9061157060c08a013584611b67565b9973ffffffffffffffffffffffffffffffffffffffff82169a8b3b15610133576040517fd505accf00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff88166004820152306024820152604480820183905235606482015260ff92909216608483015260a482019990995260c481019490945261096999610fb39861162e9561162992905f8160e48183865af161163d575b50873091613406565b612812565b61163785612964565b90612a64565b806109a461164a9261036b565b5f611620565b346101335760e07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126101335760043561168b81610312565b67ffffffffffffffff90602435828111610133576116ad9036906004016105c6565b906116b73661067a565b9260c435908111610133576116d0903690600401610188565b90916116da6127a6565b6117057fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b61170e84612964565b948435936117208560c0880135611b67565b9061172961046d565b308152916020830152611754610cef60055473ffffffffffffffffffffffffffffffffffffffff1690565b9261175d6126ed565b843b15610133575f9687936117a2926040519c8d998a9889977f137c29fe000000000000000000000000000000000000000000000000000000008952600489016122ec565b03925af1908115610df35761096993610fb3926117d9575b506117c760608401611b30565b6117d360208501611b30565b90612812565b806109a46117e69261036b565b5f6117ba565b9392989196979094976117fd6127a6565b6118287fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b8734036118f057611851610cef60035473ffffffffffffffffffffffffffffffffffffffff1690565b96873b1561142557873b15610133575f9961189d956040519c8d9b8c9a8b9a7f7b939232000000000000000000000000000000000000000000000000000000008c523360048d0161191a565b039134905af18015610df3576118dd575b5061047a60017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f5416175f55565b806109a46118ea9261036b565b5f6118ae565b60046040517f1841b4e1000000000000000000000000000000000000000000000000000000008152fd5b9794909361199e9b9a969294999793996101809a73ffffffffffffffffffffffffffffffffffffffff8097818094168d521660208c01521660408a01525f60608a0152608089015260a088015260c08701521660e085015263ffffffff92838092166101008601521661012084015216610140820152816101608201520190610a76565b90565b6040513d5f823e3d90fd5b9291959794909693976119bd6127a6565b6119e87fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b6119f1916127b2565b88359960608a01989293611a048a611b30565b73ffffffffffffffffffffffffffffffffffffffff1692611a298d60c08e0135611b67565b92843b15610133576040517fef55bec600000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8a16600482015230602482015260448101949094526064840195909552608483019690965260a482019390935260ff90941660c485015260e48401919091526101048301919091525f90829061012490829084905af1968715610df357611ae3610fb39661162e93611aef9a611b1d575b50611b30565b6117d360208901611b30565b61047a60017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f5416175f55565b806109a4611b2a9261036b565b5f611add565b3561199e81610312565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b91908201809211611b7457565b611b3a565b9035907ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffea181360301821215610133570190565b3561199e81610330565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215610133570180359067ffffffffffffffff82116101335760200191813603831361013357565b94919381979391969894611c196127a6565b611c447fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff005f54165f55565b6060880135998835948591611c58916127b2565b8d60408d97949397019c8d611c6c91611b79565b611c7590611b30565b73ffffffffffffffffffffffffffffffffffffffff1694611c9591611b67565b92843b15610133576040517fef55bec600000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff8d16600482015230602482015260448101949094526064840195909552608483019690965260a482019390935260ff90941660c485015260e48401919091526101048301919091525f90829061012490829084905af18015610df357611e6c575b50611d448587611b79565b611d4d90611b30565b90611d5a60208801611b30565b611d6392612812565b611d6c85613470565b611d7593612a64565b611d7f8183611b79565b606001611d8b90611b30565b90611d968184611b79565b608001611da290611b30565b92611dad8282611b79565b611db690611b30565b94611dc18383611b79565b602001611dcd90611b30565b90611dd88484611b79565b60400135611de68585611b79565b60a0013590611df58686611b79565b60c001611e0190611b30565b92611e0c8787611b79565b60e001611e1890611bac565b94611e238888611b79565b61010001611e3090611bac565b96611e3b8982611b79565b61012001611e4890611bac565b98611e5291611b79565b6101408101611e6091611bb6565b9a611aef9c919a6134e8565b806109a4611e799261036b565b5f611d39565b67ffffffffffffffff811161037f5760051b60200190565b6040517f466565732800000000000000000000000000000000000000000000000000000060208201527f75696e7432353620616d6f756e7400000000000000000000000000000000000060258201527f6164647265737320726563697069656e7429000000000000000000000000000060338201526025815261199e816103bc565b6040517f426173654465706f73697444617461280000000000000000000000000000000060208201527f6164647265737320696e707574546f6b656e000000000000000000000000000060308201527f61646472657373206f7574707574546f6b656e0000000000000000000000000060428201527f75696e74323536206f7574707574416d6f756e7400000000000000000000000060558201527f61646472657373206465706f7369746f7200000000000000000000000000000060698201527f6164647265737320726563697069656e74000000000000000000000000000000607a8201527f75696e743235362064657374696e6174696f6e436861696e4964000000000000608b8201527f61646472657373206578636c757369766552656c61796572000000000000000060a58201527f75696e7433322071756f746554696d657374616d70000000000000000000000060bd8201527f75696e7433322066696c6c446561646c696e650000000000000000000000000060d28201527f75696e743332206578636c75736976697479506172616d65746572000000000060e58201527f6279746573206d6573736167652900000000000000000000000000000000000061010082015261199e8161010e81015b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0810183528261042c565b6040517f4465706f736974446174612846656573207375626d697373696f6e466565732c60208201527f426173654465706f7369744461746120626173654465706f736974446174612c60408201527f75696e7432353620696e707574416d6f756e742900000000000000000000000060608201526054815261199e816103a0565b604051906121aa826103bc565b602e82527f696e7432353620616d6f756e74290000000000000000000000000000000000006040837f546f6b656e5065726d697373696f6e73286164647265737320746f6b656e2c7560208201520152565b612204611e97565b61199e6034612211611f19565b9261221a61211b565b61222261219d565b906040519586937f4465706f73697444617461207769746e6573732900000000000000000000000060208601526122628151809260208989019101610a55565b84016122778251809360208985019101610a55565b0161228b8251809360208885019101610a55565b0161229f8251809360208785019101610a55565b0103601481018452018261042c565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe093818652868601375f8582860101520116010190565b959361235b61236f94602061199e9a989561014095606081359161230f83610312565b73ffffffffffffffffffffffffffffffffffffffff9283168e5280850135858f0152604080820135908f0152013560608d01528151811660808d015291015160a08b01521660c0890152565b60e087015280610100870152850190610a76565b926101208185039101526122ae565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b908210156123c6576123c29160051b810190611bb6565b9091565b61237e565b908092918237015f815290565b3d15612402573d906123e982610496565b916123f7604051938461042c565b82523d5f602084013e565b606090565b6020818303126101335780519067ffffffffffffffff8211610133570181601f8201121561013357805161243a81610496565b92612448604051948561042c565b818452602082840101116101335761199e9160208085019101610a55565b80518210156123c65760209160051b010190565b91909161248683611e7f565b906040612496604051938461042c565b8483527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06124c386611e7f565b015f5b81811061255057505082945f5b8181106124e1575050505050565b5f806124ee8385886123ab565b906124fd8751809381936123cb565b0390305af461250a6123d8565b9015612530579060019161251e8288612466565b526125298187612466565b50016124d3565b604481511061013357806004610133920151602480918301019101612407565b8060606020809388010152016124c6565b6040517f53776170416e644465706f73697444617461280000000000000000000000000060208201527f46656573207375626d697373696f6e466565730000000000000000000000000060338201527f426173654465706f73697444617461206465706f73697444617461000000000060468201527f616464726573732073776170546f6b656e00000000000000000000000000000060618201527f616464726573732065786368616e67650000000000000000000000000000000060728201527f5472616e7366657254797065207472616e73666572547970650000000000000060828201527f75696e743235362073776170546f6b656e416d6f756e74000000000000000000609b8201527f75696e74323536206d696e4578706563746564496e707574546f6b656e416d6f60b28201527f756e74000000000000000000000000000000000000000000000000000000000060d28201527f627974657320726f7574657243616c6c6461746129000000000000000000000060d582015261199e8160ea81016120ef565b6126f5611e97565b61199e603b612702611f19565b9261270b612561565b61271361219d565b906040519586937f53776170416e644465706f73697444617461207769746e65737329000000000060208601526127538151809260208989019101610a55565b84016127688251809360208985019101610a55565b0161277c8251809360208885019101610a55565b016127908251809360208785019101610a55565b0103601b81018452018261042c565b1561013357565b60ff5f54161561013357565b919091604183036127e85782604010156123c657604081013560f81c908360201161013357803593604011610133576020013591565b60046040517f8baa579f000000000000000000000000000000000000000000000000000000008152fd5b91908161281e57505050565b6040517fa9059cbb00000000000000000000000000000000000000000000000000000000602082015273ffffffffffffffffffffffffffffffffffffffff918216602482015260448082019390935291825261047a9261287f60648461042c565b16613cad565b916128ae906128a061199e9593606086526060860190610a76565b908482036020860152610a76565b916040818403910152610a76565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffd5b600311156128f357565b6128bc565b3560038110156101335790565b9596939290999897949161012087019a87526020870152604086015273ffffffffffffffffffffffffffffffffffffffff809216606086015216608084015260038410156128f3576101009360a084015260c083015260e08201520152565b61296c612561565b90612a5e612978611e97565b92612981611f19565b93612996604051958692602084019485612885565b03936129c87fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09586810183528261042c565b519020926129d5836139d0565b93612a526129ee6129e96040870187611b79565b613a39565b916129fb60608701611b30565b95612a0860808201611b30565b91612a1560a083016128f8565b612a2d612a26610100850185611bb6565b36916104d0565b6020815191012093604051998a97602089019c8d9560c060e089013598013596612905565b0390810183528261042c565b51902090565b92916042612aaf92612a746138af565b90604051917f1901000000000000000000000000000000000000000000000000000000000000835260028301526022820152209236916104d0565b91612aba8383613d95565b60058195929510156128f357159384612b7e575b508315612ae0575b505050156127e857565b5f929350908291604051612b31816120ef60208201947f1626ba7e00000000000000000000000000000000000000000000000000000000998a87526024840152604060448401526064830190610a76565b51915afa90612b3e6123d8565b82612b70575b82612b54575b50505f8080612ad6565b9091506020818051810103126101335760200151145f80612b4a565b915060208251101591612b44565b73ffffffffffffffffffffffffffffffffffffffff83811691161493505f612ace565b90816020910312610133575190565b65ffffffffffff809116908114611b745760010190565b90816020910312610133575180151581036101335790565b91908203918211611b7457565b92916080949796959273ffffffffffffffffffffffffffffffffffffffff612c229316855260a0602086015260a08501916122ae565b95604083015260608201520152565b612c40610cef60608301611b30565b906040808201612c56610cef61079b8386611b79565b92612c6360a082016128f8565b92612c7060808301611b30565b9060c08301359673ffffffffffffffffffffffffffffffffffffffff808216918351997f70a082310000000000000000000000000000000000000000000000000000000098898c528b60209b8c60049280612cea3086830191909173ffffffffffffffffffffffffffffffffffffffff6020820193169052565b03818a5afa9d8e15610df3575f9e6133e5575b5085169c8d928d8d8a5195869182528180612d373089830191909173ffffffffffffffffffffffffffffffffffffffff6020820193169052565b03915afa8015610df3578f918f908f90988b8f988f9b8f915f976133c6575b50612d60816128e9565b8061301257505050888a612d7392613b56565b8b6101009d8e89019889612d8691611bb6565b90925192838092612d96926123cb565b035a925f8094938194f1612da86123d8565b50612db29061279f565b8b5190815230868201908152909384918290819060200103915afa8015610df3578f92612de7935f92612ff5575b5050612bdf565b9b60e08b01358d10612fcd578851908152308382019081528e908290819060200103818b5afa8015610df35785928f92612e28935f93612f9e575050612bdf565b03612f7757508a9492888b8e969489612e428e9685611bb6565b94909a612e4f8883611b79565b01612e5990611b30565b96612e6391611b79565b0135928a519687961699612e779587612bec565b037fa914d6534898f72d14dd30011bb127105e9a394a611c88cd9f37ba7c7dff6cfa91a4612ea58484611b79565b606001612eb190611b30565b94612ebc8585611b79565b608001612ec890611b30565b96612ed38686611b79565b01612edd90611b30565b91612ee88686611b79565b0135612ef48686611b79565b60a0013591612f038787611b79565b60c001612f0f90611b30565b93612f1a8888611b79565b60e001612f2690611bac565b95612f318989611b79565b01612f3b90611bac565b96612f468982611b79565b61012001612f5390611bac565b98612f5d91611b79565b6101408101612f6b91611bb6565b9a61047a9c919a6134e8565b86517fd6cf42f0000000000000000000000000000000000000000000000000000000008152fd5b612fbe929350803d10612fc6575b612fb6818361042c565b810190612ba1565b908f80612de0565b503d612fac565b8289517f0492ff87000000000000000000000000000000000000000000000000000000008152fd5b61300b9250803d10612fc657612fb6818361042c565b8f80612de0565b9096506001919799508a935061302a819c969c6128e9565b036130dc5750505050905051917fa9059cbb0000000000000000000000000000000000000000000000000000000083528c838061308d878d8784016020909392919373ffffffffffffffffffffffffffffffffffffffff60408201951681520152565b03815f8b5af18015610df35789958f918f908f90968e976130af575b50612d73565b6130ce90833d85116130d5575b6130c6818361042c565b810190612bc7565b505f6130a9565b503d6130bc565b6132829392916131119161310b610cef60059b999b5473ffffffffffffffffffffffffffffffffffffffff1690565b90613b56565b61315c7b010000000000000000000000000000000000000000000000000000007fffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffff6006541617600655565b61325a8a61324b613185610cef60055473ffffffffffffffffffffffffffffffffffffffff1690565b9661323c61319e60065465ffffffffffff9060a01c1690565b936131f76131ab86612bb0565b7fffffffffffff000000000000ffffffffffffffffffffffffffffffffffffffff79ffffffffffff00000000000000000000000000000000000000006006549260a01b16911617600655565b61321e61320261047c565b73ffffffffffffffffffffffffffffffffffffffff909e168e52565b8d1673ffffffffffffffffffffffffffffffffffffffff16868d0152565b4265ffffffffffff16908b0152565b65ffffffffffff166060890152565b613262610489565b96875286019073ffffffffffffffffffffffffffffffffffffffff169052565b4289850152803b15610133576133665f949185928b519687809481937f2b67b570000000000000000000000000000000000000000000000000000000008352308a8401906002906040610140946101009273ffffffffffffffffffffffffffffffffffffffff8091168652815181815116602088015281602082015116848801526060848201519165ffffffffffff809316828a0152015116608087015260208201511660a0860152015160c08401528060e08401528201527f30780000000000000000000000000000000000000000000000000000000000006101208201520190565b03925af18015610df35789958f918f908f90968e976133b3575b506133ae7fffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffff60065416600655565b612d73565b806109a46133c09261036b565b5f613380565b6133de919750863d8811612fc657612fb6818361042c565b955f612d56565b86919e506133ff908e803d10612fc657612fb6818361042c565b9d90612cfd565b909261047a93604051937f23b872dd00000000000000000000000000000000000000000000000000000000602086015273ffffffffffffffffffffffffffffffffffffffff809216602486015216604484015260648301526064825261346b826103f4565b613cad565b61347861211b565b613480611e97565b6134a061348b611f19565b916120ef604051938492602084019687612885565b5190209060606134af826139d0565b916134c06129e96040830183611b79565b60405193602085019586526040850152828401520135608082015260808152612a5e816103f4565b9894909995919b96929a979361351a818e73ffffffffffffffffffffffffffffffffffffffff80600354169116613b56565b73ffffffffffffffffffffffffffffffffffffffff600354169b8c3b15610133576040519d8e9c8d809d7f7b93923200000000000000000000000000000000000000000000000000000000825273ffffffffffffffffffffffffffffffffffffffff16906004015273ffffffffffffffffffffffffffffffffffffffff1660248d015273ffffffffffffffffffffffffffffffffffffffff1660448c015273ffffffffffffffffffffffffffffffffffffffff1660648b015260848a015260a489015260c488015273ffffffffffffffffffffffffffffffffffffffff1660e487015261010486016136109163ffffffff169052565b63ffffffff1661012485015263ffffffff1661014484015261016483016101809052610184830190613641926122ae565b03815a5f948591f18015610df3576136565750565b806109a461047a9261036b565b60ff81146136745761199e90613d47565b506040516001805480821c915f9082811690811561378e575b602090602086108314613761578587528694602086019390811561372357506001146136c3575b50505061199e9250038261042c565b9250936136f160015f527fb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf690565b945f935b82851061370d5750505061199e9350015f80806136b4565b86548585015295860195879550938101936136f5565b91505061199e959293507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff009150168252151560051b015f80806136b4565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b93607f169361368d565b60ff81146137a95761199e90613d47565b506040515f6002546001918160011c926001831690811561385a575b602090602086108314613761578587528694602086019390811561372357506001146137fa5750505061199e9250038261042c565b92509361382860025f527f405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace90565b945f935b8285106138445750505061199e9350015f80806136b4565b865485850152958601958795509381019361382c565b93607f16936137c5565b73ffffffffffffffffffffffffffffffffffffffff60065416330361388557565b60046040517fbf10dd3a000000000000000000000000000000000000000000000000000000008152fd5b73ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000163014806139a7575b15613917577f000000000000000000000000000000000000000000000000000000000000000090565b60405160208101907f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f82527f000000000000000000000000000000000000000000000000000000000000000060408201527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a082015260a08152612a5e81610410565b507f000000000000000000000000000000000000000000000000000000000000000046146138ee565b6139d8611e97565b602081519101209073ffffffffffffffffffffffffffffffffffffffff6020820135613a0381610312565b604051926020840194855235604084015216606082015260608152612a5e816103a0565b613a2f611f19565b6020815191012090565b613a41613a27565b612a5e613a4d83611b30565b926120ef613a5d60208301611b30565b91613a6a60608201611b30565b90613a7760808201611b30565b90613a8460c08201611b30565b613a9060e08301611bac565b90613a9e6101008401611bac565b92613aac6101208201611bac565b94613abe612a26610140840184611bb6565b8051602091820120604080519283019d8e5273ffffffffffffffffffffffffffffffffffffffff9e8f16838201529a8e166060830152998301356080820152968c1660a080890191909152908c1660c0880152013560e086015290981661010084015263ffffffff978816610120840152871661014083015290951661016086015261018085019190915291929182906101a0820190565b6040517f095ea7b3000000000000000000000000000000000000000000000000000000006020820181815273ffffffffffffffffffffffffffffffffffffffff851660248401526044808401969096529482529390927fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe091613bd960648661042c565b5f8073ffffffffffffffffffffffffffffffffffffffff86169287519082855af190613c036123d8565b82613c7b575b5081613c70575b5015613c1e575b5050505050565b604051602081019590955273ffffffffffffffffffffffffffffffffffffffff1660248501525f6044850152613c669361346b91613c60908260648101612a52565b82613cad565b5f80808080613c17565b90503b15155f613c10565b80519192508115918215613c93575b5050905f613c09565b613ca69250602080918301019101612bc7565b5f80613c8a565b905f8073ffffffffffffffffffffffffffffffffffffffff613d179416927f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c65646020604051613cfa81610384565b818152015260208151910182855af1613d116123d8565b91613e4e565b8051908115918215613d2d575b50501561013357565b613d409250602080918301019101612bc7565b5f80613d24565b60ff811690601f8211613d6b5760405191613d6183610384565b8252602082015290565b60046040517fb3512b0c000000000000000000000000000000000000000000000000000000008152fd5b9060418151145f14613dbd576123c291602082015190606060408401519301515f1a90613dc6565b50505f90600290565b7f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08411613e43576020935f9360ff60809460405194855216868401526040830152606082015282805260015afa15610df3575f5173ffffffffffffffffffffffffffffffffffffffff811615613e3b57905f90565b505f90600190565b505050505f90600390565b9015613e6857815115613e5f575090565b3b156101335790565b50805190811561013357602001fdfea2646970667358221220095447cfdf911840a498e253cc4606b4cc497e7eaa081a2c8d529439a511e86164736f6c63430008170033"; + +type SpokePoolV3PeripheryConstructorParams = + | [signer?: Signer] + | ConstructorParameters; + +const isSuperArgs = ( + xs: SpokePoolV3PeripheryConstructorParams +): xs is ConstructorParameters => xs.length > 1; + +export class SpokePoolV3Periphery__factory extends ContractFactory { + constructor(...args: SpokePoolV3PeripheryConstructorParams) { + if (isSuperArgs(args)) { + super(...args); + } else { + super(_abi, _bytecode, args[0]); + } + } + + override deploy( + overrides?: Overrides & { from?: string } + ): Promise { + return super.deploy(overrides || {}) as Promise; + } + override getDeployTransaction( + overrides?: Overrides & { from?: string } + ): TransactionRequest { + return super.getDeployTransaction(overrides || {}); + } + override attach(address: string): SpokePoolV3Periphery { + return super.attach(address) as SpokePoolV3Periphery; + } + override connect(signer: Signer): SpokePoolV3Periphery__factory { + return super.connect(signer) as SpokePoolV3Periphery__factory; + } + + static readonly bytecode = _bytecode; + static readonly abi = _abi; + static createInterface(): SpokePoolV3PeripheryInterface { + return new utils.Interface(_abi) as SpokePoolV3PeripheryInterface; + } + static connect( + address: string, + signerOrProvider: Signer | Provider + ): SpokePoolV3Periphery { + return new Contract( + address, + _abi, + signerOrProvider + ) as SpokePoolV3Periphery; + } +} diff --git a/api/_utils.ts b/api/_utils.ts index ded372730..1fe12c9ea 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -84,7 +84,6 @@ import { TokenNotFoundError, } from "./_errors"; import { Token } from "./_dexes/types"; -import { addMarkupToAmount } from "./_dexes/uniswap/utils"; export { InputError, handleErrorCondition } from "./_errors"; export const { Profiler } = sdk.utils; @@ -908,7 +907,7 @@ export async function getBridgeQuoteForMinOutput(params: { outputToken: params.outputToken.address, originChainId: params.inputToken.chainId, destinationChainId: params.outputToken.chainId, - skipAmountLimit: false, + skipAmountLimit: true, recipient: params.recipient, message: params.message, }; @@ -2385,3 +2384,9 @@ export function getSpokePoolVerifier(chainId: number) { const address = ENABLED_ROUTES.spokePoolVerifier.address; return SpokePoolVerifier__factory.connect(address, getProvider(chainId)); } + +export function addMarkupToAmount(amount: BigNumber, markup = 0.01) { + return amount + .mul(ethers.utils.parseEther((1 + Number(markup)).toString())) + .div(sdk.utils.fixedPointAdjustment); +} diff --git a/api/swap-quote.ts b/api/swap-quote.ts index 073340abb..6bdd6386f 100644 --- a/api/swap-quote.ts +++ b/api/swap-quote.ts @@ -16,7 +16,7 @@ import { import { getUniswapQuoteWithSwapQuoter } from "./_dexes/uniswap/swap-quoter"; import { get1inchQuoteForOriginSwapExactInput } from "./_dexes/1inch"; import { InvalidParamError } from "./_errors"; -import { AMOUNT_TYPE } from "./_dexes/cross-swap"; +import { AMOUNT_TYPE } from "./_dexes/utils"; const SwapQuoteQueryParamsSchema = type({ swapToken: validAddress(), diff --git a/api/swap/_utils.ts b/api/swap/_utils.ts index bde17a64a..055b7093d 100644 --- a/api/swap/_utils.ts +++ b/api/swap/_utils.ts @@ -11,11 +11,6 @@ import { getWrappedNativeTokenAddress, getCachedTokenPrice, } from "../_utils"; -import { - AMOUNT_TYPE, - getCrossSwapQuotes, - AmountType, -} from "../_dexes/cross-swap"; import { InvalidParamError } from "../_errors"; import { isValidIntegratorId } from "../_integrator-id"; import { @@ -23,8 +18,9 @@ import { CrossSwapQuotes, SwapQuote, Token, + AmountType, } from "../_dexes/types"; -import { formatUnits } from "ethers/lib/utils"; +import { AMOUNT_TYPE } from "../_dexes/utils"; import { encodeApproveCalldata } from "../_multicall-handler"; export const BaseSwapQueryParamsSchema = type({ @@ -45,9 +41,9 @@ export const BaseSwapQueryParamsSchema = type({ export type BaseSwapQueryParams = Infer; -export async function handleBaseSwapQueryParams({ - query, -}: TypedVercelRequest) { +export async function handleBaseSwapQueryParams( + query: TypedVercelRequest["query"] +) { assert(query, BaseSwapQueryParamsSchema); const { @@ -103,7 +99,6 @@ export async function handleBaseSwapQueryParams({ const amountType = tradeType as AmountType; const amount = BigNumber.from(_amount); - // 1. Get token details const [inputToken, outputToken] = await Promise.all([ getCachedTokenInfo({ address: inputTokenAddress, @@ -115,32 +110,20 @@ export async function handleBaseSwapQueryParams({ }), ]); - // 2. Get swap quotes and calldata based on the swap type - const crossSwapQuotes = await getCrossSwapQuotes({ - amount, + return { inputToken, outputToken, - depositor, - recipient: recipient || depositor, - slippageTolerance: Number(slippageTolerance), - type: amountType, + amount, + amountType, refundOnOrigin, - refundAddress, - isInputNative, - isOutputNative, - }); - - // 3. Calculate fees based for full route - // const fees = await calculateCrossSwapFees(crossSwapQuotes); - - return { - crossSwapQuotes: { - ...crossSwapQuotes, - // fees, - }, integratorId, skipOriginTxEstimation, isInputNative, + isOutputNative, + refundAddress, + recipient, + depositor, + slippageTolerance, }; } @@ -193,10 +176,10 @@ async function calculateSwapFee( ]); const normalizedIn = - parseFloat(formatUnits(expectedAmountIn, tokenIn.decimals)) * + parseFloat(utils.formatUnits(expectedAmountIn, tokenIn.decimals)) * inputTokenPriceBase; const normalizedOut = - parseFloat(formatUnits(expectedAmountOut, tokenOut.decimals)) * + parseFloat(utils.formatUnits(expectedAmountOut, tokenOut.decimals)) * outputTokenPriceBase; return { [baseCurrency]: normalizedIn - normalizedOut, @@ -216,7 +199,7 @@ async function calculateBridgeFee( ); const normalizedFee = parseFloat( - formatUnits(suggestedFees.totalRelayFee.total, inputToken.decimals) + utils.formatUnits(suggestedFees.totalRelayFee.total, inputToken.decimals) ) * inputTokenPriceBase; return { diff --git a/api/swap/allowance.ts b/api/swap/allowance.ts index 43c0013cd..488216f8e 100644 --- a/api/swap/allowance.ts +++ b/api/swap/allowance.ts @@ -1,189 +1,3 @@ -import { VercelResponse } from "@vercel/node"; -import { BigNumber, constants } from "ethers"; - -import { TypedVercelRequest } from "../_types"; -import { - getLogger, - getProvider, - handleErrorCondition, - latestGasPriceCache, - Profiler, -} from "../_utils"; -import { buildCrossSwapTxForAllowanceHolder } from "../_dexes/cross-swap"; -import { - handleBaseSwapQueryParams, - BaseSwapQueryParams, - getApprovalTxns, -} from "./_utils"; -import { getBalanceAndAllowance } from "../_erc20"; - -const handler = async ( - request: TypedVercelRequest, - response: VercelResponse -) => { - const logger = getLogger(); - logger.debug({ - at: "Swap/allowance", - message: "Query data", - query: request.query, - }); - try { - const profiler = new Profiler({ - at: "swap/allowance", - logger: console, - }); - const mark = profiler.start("e2e endpoint runtime"); - const { - crossSwapQuotes, - integratorId, - skipOriginTxEstimation, - isInputNative, - } = await handleBaseSwapQueryParams(request); - - const crossSwapTx = await buildCrossSwapTxForAllowanceHolder( - crossSwapQuotes, - integratorId - ); - - const { originSwapQuote, bridgeQuote, destinationSwapQuote, crossSwap } = - crossSwapQuotes; - - const originChainId = crossSwap.inputToken.chainId; - const inputTokenAddress = isInputNative - ? constants.AddressZero - : crossSwap.inputToken.address; - const inputAmount = - originSwapQuote?.maximumAmountIn || bridgeQuote.inputAmount; - - const { allowance, balance } = await getBalanceAndAllowance({ - chainId: originChainId, - tokenAddress: inputTokenAddress, - owner: crossSwap.depositor, - spender: crossSwapTx.to, - }); - - const isSwapTxEstimationPossible = - !skipOriginTxEstimation && - allowance.gte(inputAmount) && - balance.gte(inputAmount); - - let originTxGas: BigNumber | undefined; - let originTxGasPrice: BigNumber | undefined; - if (isSwapTxEstimationPossible) { - const provider = getProvider(originChainId); - [originTxGas, originTxGasPrice] = await Promise.all([ - provider.estimateGas({ - ...crossSwapTx, - from: crossSwap.depositor, - }), - latestGasPriceCache(originChainId).get(), - ]); - } else { - originTxGasPrice = await latestGasPriceCache(originChainId).get(); - } - - let approvalTxns: - | { - chainId: number; - to: string; - data: string; - }[] - | undefined; - // @TODO: Allow for just enough approval amount to be set. - const approvalAmount = constants.MaxUint256; - if (allowance.lt(inputAmount)) { - approvalTxns = getApprovalTxns({ - token: crossSwap.inputToken, - spender: crossSwapTx.to, - amount: approvalAmount, - }); - } - - 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(), - }, - }, - approvalTxns, - quotes: { - 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, - }; - mark.stop(); - logger.debug({ - at: "Swap/allowance", - message: "Response data", - responseJson, - }); - response.status(200).json(responseJson); - } catch (error: unknown) { - return handleErrorCondition("swap/allowance", response, logger, error); - } -}; - -export default handler; +// Maps to /swap/approval +import { default as approvalHandler } from "./approval"; +export default approvalHandler; diff --git a/api/swap/approval/_utils.ts b/api/swap/approval/_utils.ts new file mode 100644 index 000000000..da8b19800 --- /dev/null +++ b/api/swap/approval/_utils.ts @@ -0,0 +1,150 @@ +import { PopulatedTransaction } from "ethers"; + +import { CrossSwapQuotes } from "../../_dexes/types"; +import { tagIntegratorId } from "../../_integrator-id"; +import { getSpokePool } from "../../_utils"; +import { + getSpokePoolPeriphery, + getSpokePoolPeripheryProxy, + TransferType, +} from "../../_spoke-pool-periphery"; +import { extractDepositDataStruct } from "../../_dexes/utils"; +import { getUniversalSwapAndBridge } from "../../_swap-and-bridge"; + +export async function buildCrossSwapTxForAllowanceHolder( + crossSwapQuotes: CrossSwapQuotes, + integratorId?: string +) { + const { originSwapQuote, crossSwap, contracts } = crossSwapQuotes; + const { originSwapEntryPoint, originRouter, depositEntryPoint } = contracts; + const originChainId = crossSwap.inputToken.chainId; + + const deposit = await extractDepositDataStruct(crossSwapQuotes); + + let tx: PopulatedTransaction; + let toAddress: string; + + // Build origin swap tx + if (originSwapQuote) { + if (!originSwapEntryPoint || !originRouter) { + throw new Error( + `'originSwapEntryPoint' and 'originRouter' need to be defined for origin swap quotes` + ); + } + if (originSwapEntryPoint.name === "SpokePoolPeripheryProxy") { + const spokePoolPeripheryProxy = getSpokePoolPeripheryProxy( + originSwapEntryPoint.address, + originChainId + ); + tx = await spokePoolPeripheryProxy.populateTransaction.swapAndBridge( + { + submissionFees: { + amount: 0, + recipient: crossSwap.depositor, + }, + depositData: deposit, + swapToken: originSwapQuote.tokenIn.address, + exchange: originRouter.address, + transferType: TransferType.Approval, + swapTokenAmount: originSwapQuote.maximumAmountIn, + minExpectedInputTokenAmount: originSwapQuote.minAmountOut, + routerCalldata: originSwapQuote.swapTx.data, + } + // TODO: Add payable modifier to SpokePoolPeripheryProxy swapAndBridge function + // { + // value: crossSwap.isInputNative ? originSwapQuote.maximumAmountIn : 0, + // } + ); + toAddress = spokePoolPeripheryProxy.address; + } else if (originSwapEntryPoint.name === "UniversalSwapAndBridge") { + const universalSwapAndBridge = getUniversalSwapAndBridge( + originSwapEntryPoint.dex, + originChainId + ); + tx = await universalSwapAndBridge.populateTransaction.swapAndBridge( + originSwapQuote.tokenIn.address, + originSwapQuote.tokenOut.address, + originSwapQuote.swapTx.data, + originSwapQuote.maximumAmountIn, + originSwapQuote.minAmountOut, + { + ...deposit, + // Typo in the contract + destinationChainid: deposit.destinationChainId, + } + ); + toAddress = universalSwapAndBridge.address; + } else { + throw new Error( + `Could not build 'swapAndBridge' tx for unknown entry point contract` + ); + } + } + // Build deposit tx + else { + if (!depositEntryPoint) { + throw new Error( + `'depositEntryPoint' needs to be defined for bridge quotes` + ); + } + + if (depositEntryPoint.name === "SpokePoolPeriphery") { + const spokePoolPeriphery = getSpokePoolPeriphery( + depositEntryPoint.address, + originChainId + ); + tx = await spokePoolPeriphery.populateTransaction.deposit( + deposit.recipient, + deposit.inputToken, + // deposit.outputToken, // TODO: allow for output token in periphery contract + deposit.inputAmount, + deposit.outputAmount, + deposit.destinationChainId, + deposit.exclusiveRelayer, + deposit.quoteTimestamp, + deposit.fillDeadline, + deposit.exclusivityDeadline, + deposit.message, + { + value: crossSwapQuotes.crossSwap.isInputNative + ? deposit.inputAmount + : 0, + } + ); + toAddress = spokePoolPeriphery.address; + } else if (depositEntryPoint.name === "SpokePool") { + const spokePool = getSpokePool(originChainId); + tx = await spokePool.populateTransaction.depositV3( + deposit.depositor, + deposit.recipient, + deposit.inputToken, + deposit.outputToken, + deposit.inputAmount, + deposit.outputAmount, + deposit.destinationChainId, + deposit.exclusiveRelayer, + deposit.quoteTimestamp, + deposit.fillDeadline, + deposit.exclusivityDeadline, + deposit.message, + { + value: crossSwapQuotes.crossSwap.isInputNative + ? deposit.inputAmount + : 0, + } + ); + toAddress = spokePool.address; + } else { + throw new Error( + `Could not build 'deposit' tx for unknown entry point contract` + ); + } + } + + return { + from: crossSwapQuotes.crossSwap.depositor, + to: toAddress, + 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 new file mode 100644 index 000000000..72f0232be --- /dev/null +++ b/api/swap/approval/index.ts @@ -0,0 +1,224 @@ +import { VercelResponse } from "@vercel/node"; +import { BigNumber, constants } from "ethers"; + +import { TypedVercelRequest } from "../../_types"; +import { + getLogger, + getProvider, + handleErrorCondition, + latestGasPriceCache, + Profiler, +} from "../../_utils"; +import { buildCrossSwapTxForAllowanceHolder } from "./_utils"; +import { + handleBaseSwapQueryParams, + BaseSwapQueryParams, + getApprovalTxns, +} from "../_utils"; +import { getBalanceAndAllowance } from "../../_erc20"; +import { getCrossSwapQuotes } from "../../_dexes/cross-swap-service"; +import { getSwapRouter02Strategy } from "../../_dexes/uniswap/swap-router-02"; +import { QuoteFetchStrategies } from "../../_dexes/utils"; + +// For approval-based flows, we use the `UniversalSwapAndBridge` strategy with Uniswap V3's `SwapRouter02` +const quoteFetchStrategies: QuoteFetchStrategies = { + default: getSwapRouter02Strategy("UniversalSwapAndBridge"), +}; + +const handler = async ( + request: TypedVercelRequest, + response: VercelResponse +) => { + const logger = getLogger(); + logger.debug({ + at: "Swap/approval", + message: "Query data", + query: request.query, + }); + try { + const profiler = new Profiler({ + at: "swap/approval", + logger: console, + }); + const mark = profiler.start("e2e endpoint runtime"); + + const { + integratorId, + skipOriginTxEstimation, + isInputNative, + isOutputNative, + inputToken, + outputToken, + amount, + amountType, + refundOnOrigin, + refundAddress, + recipient, + depositor, + slippageTolerance, + } = await handleBaseSwapQueryParams(request.query); + + const crossSwapQuotes = await getCrossSwapQuotes( + { + amount, + inputToken, + outputToken, + depositor, + recipient: recipient || depositor, + slippageTolerance: Number(slippageTolerance), + type: amountType, + refundOnOrigin, + refundAddress, + isInputNative, + isOutputNative, + }, + quoteFetchStrategies + ); + + const crossSwapTx = await buildCrossSwapTxForAllowanceHolder( + crossSwapQuotes, + integratorId + ); + + const { originSwapQuote, bridgeQuote, destinationSwapQuote, crossSwap } = + crossSwapQuotes; + + const originChainId = crossSwap.inputToken.chainId; + const inputTokenAddress = isInputNative + ? constants.AddressZero + : crossSwap.inputToken.address; + const inputAmount = + originSwapQuote?.maximumAmountIn || bridgeQuote.inputAmount; + + const { allowance, balance } = await getBalanceAndAllowance({ + chainId: originChainId, + tokenAddress: inputTokenAddress, + owner: crossSwap.depositor, + spender: crossSwapTx.to, + }); + + const isSwapTxEstimationPossible = + !skipOriginTxEstimation && + allowance.gte(inputAmount) && + balance.gte(inputAmount); + + let originTxGas: BigNumber | undefined; + let originTxGasPrice: BigNumber | undefined; + if (isSwapTxEstimationPossible) { + const provider = getProvider(originChainId); + [originTxGas, originTxGasPrice] = await Promise.all([ + provider.estimateGas({ + ...crossSwapTx, + from: crossSwap.depositor, + }), + latestGasPriceCache(originChainId).get(), + ]); + } else { + originTxGasPrice = await latestGasPriceCache(originChainId).get(); + } + + let approvalTxns: + | { + chainId: number; + to: string; + data: string; + }[] + | undefined; + // @TODO: Allow for just enough approval amount to be set. + const approvalAmount = constants.MaxUint256; + if (allowance.lt(inputAmount)) { + approvalTxns = getApprovalTxns({ + token: crossSwap.inputToken, + spender: crossSwapTx.to, + amount: approvalAmount, + }); + } + + 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(), + }, + }, + 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, + }; + mark.stop(); + logger.debug({ + at: "Swap/approval", + message: "Response data", + responseJson, + }); + response.status(200).json(responseJson); + } catch (error: unknown) { + return handleErrorCondition("swap/approval", response, logger, error); + } +}; + +export default handler; diff --git a/api/swap/permit/_utils.ts b/api/swap/permit/_utils.ts new file mode 100644 index 000000000..49a231945 --- /dev/null +++ b/api/swap/permit/_utils.ts @@ -0,0 +1,127 @@ +import { + CrossSwapQuotes, + DepositEntryPointContract, + OriginSwapEntryPointContract, +} from "../../_dexes/types"; +import { getPermitTypedData } from "../../_permit"; +import { + getDepositTypedData, + getSwapAndDepositTypedData, + TransferType, +} from "../../_spoke-pool-periphery"; +import { extractDepositDataStruct } from "../../_dexes/utils"; +import { BigNumber } from "ethers"; + +export async function buildPermitTxPayload( + crossSwapQuotes: CrossSwapQuotes, + permitDeadline: number +) { + const { originSwapQuote, bridgeQuote, crossSwap, contracts } = + crossSwapQuotes; + const originChainId = crossSwap.inputToken.chainId; + const { originSwapEntryPoint, depositEntryPoint, originRouter } = contracts; + + const baseDepositData = await extractDepositDataStruct(crossSwapQuotes); + + let entryPointContract: + | DepositEntryPointContract + | OriginSwapEntryPointContract; + let getDepositTypedDataPromise: + | ReturnType + | ReturnType; + let methodName: string; + + if (originSwapQuote) { + if (!originSwapEntryPoint) { + throw new Error( + `'originSwapEntryPoint' needs to be defined for origin swap quotes` + ); + } + // Only SpokePoolPeriphery supports permit + if (originSwapEntryPoint.name !== "SpokePoolPeriphery") { + throw new Error( + `Permit is not supported for origin swap entry point contract '${originSwapEntryPoint.name}'` + ); + } + + if (!originRouter) { + throw new Error( + `'originRouter' needs to be defined for origin swap quotes` + ); + } + + entryPointContract = originSwapEntryPoint; + getDepositTypedDataPromise = getSwapAndDepositTypedData({ + swapAndDepositData: { + // TODO: Make this dynamic + submissionFees: { + amount: BigNumber.from(0), + recipient: crossSwapQuotes.crossSwap.depositor, + }, + depositData: baseDepositData, + swapToken: originSwapQuote.tokenIn.address, + swapTokenAmount: originSwapQuote.maximumAmountIn, + minExpectedInputTokenAmount: originSwapQuote.minAmountOut, + routerCalldata: originSwapQuote.swapTx.data, + exchange: originRouter.address, + transferType: + originRouter.name === "UniswapV3UniversalRouter" + ? TransferType.Transfer + : TransferType.Approval, + }, + chainId: originChainId, + }); + methodName = "swapAndBridgeWithPermit"; + } else { + if (!depositEntryPoint) { + throw new Error( + `'depositEntryPoint' needs to be defined for bridge quotes` + ); + } + + if (depositEntryPoint.name !== "SpokePoolPeriphery") { + throw new Error( + `Permit is not supported for deposit entry point contract '${depositEntryPoint.name}'` + ); + } + + entryPointContract = depositEntryPoint; + getDepositTypedDataPromise = getDepositTypedData({ + depositData: { + // TODO: Make this dynamic + submissionFees: { + amount: BigNumber.from(0), + recipient: crossSwap.depositor, + }, + baseDepositData, + inputAmount: BigNumber.from(bridgeQuote.inputAmount), + }, + chainId: originChainId, + }); + methodName = "depositWithPermit"; + } + + const [permitTypedData, depositTypedData] = await Promise.all([ + getPermitTypedData({ + tokenAddress: + originSwapQuote?.tokenIn.address || bridgeQuote.inputToken.address, + chainId: originChainId, + ownerAddress: crossSwap.depositor, + spenderAddress: entryPointContract.address, + value: originSwapQuote?.maximumAmountIn || bridgeQuote.inputAmount, + deadline: permitDeadline, + }), + getDepositTypedDataPromise, + ]); + return { + eip712: { + permit: permitTypedData.eip712, + deposit: depositTypedData.eip712, + }, + swapTx: { + chainId: originChainId, + to: entryPointContract.address, + methodName, + }, + }; +} diff --git a/api/swap/permit/index.ts b/api/swap/permit/index.ts new file mode 100644 index 000000000..bb141c6d0 --- /dev/null +++ b/api/swap/permit/index.ts @@ -0,0 +1,106 @@ +import { VercelResponse } from "@vercel/node"; +import { assert, Infer, optional, type } from "superstruct"; + +import { TypedVercelRequest } from "../../_types"; +import { getLogger, handleErrorCondition, positiveIntStr } from "../../_utils"; +import { getCrossSwapQuotes } from "../../_dexes/cross-swap-service"; +import { handleBaseSwapQueryParams, BaseSwapQueryParams } from "../_utils"; +import { getSwapRouter02Strategy } from "../../_dexes/uniswap/swap-router-02"; +import { InvalidParamError } from "../../_errors"; +import { buildPermitTxPayload } from "./_utils"; +import { QuoteFetchStrategies } from "../../_dexes/utils"; + +export const PermitSwapQueryParamsSchema = type({ + permitDeadline: optional(positiveIntStr()), +}); + +export type PermitSwapQueryParams = Infer; + +const DEFAULT_PERMIT_DEADLINE = + Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365; // 1 year + +// For permit-based flows, we have to use the `SpokePoolPeriphery` as an entry point +const quoteFetchStrategies: QuoteFetchStrategies = { + default: getSwapRouter02Strategy("SpokePoolPeriphery"), +}; + +const handler = async ( + request: TypedVercelRequest, + response: VercelResponse +) => { + const logger = getLogger(); + logger.debug({ + at: "Swap/permit", + message: "Query data", + query: request.query, + }); + try { + // `/swap/permit` specific params validation + const { permitDeadline: _permitDeadline, ...restQuery } = request.query; + assert( + { + permitDeadline: _permitDeadline, + }, + PermitSwapQueryParamsSchema + ); + const permitDeadline = Number(_permitDeadline ?? DEFAULT_PERMIT_DEADLINE); + + if (permitDeadline < Math.floor(Date.now() / 1000)) { + throw new InvalidParamError({ + message: + "Permit deadline must be a UNIX timestamp (seconds) in the future", + param: "permitDeadline", + }); + } + + // `/swap` specific params validation + quote generation + const { + isInputNative, + isOutputNative, + inputToken, + outputToken, + amount, + amountType, + refundOnOrigin, + refundAddress, + recipient, + depositor, + slippageTolerance, + } = await handleBaseSwapQueryParams(restQuery); + + const crossSwapQuotes = await getCrossSwapQuotes( + { + amount, + inputToken, + outputToken, + depositor, + recipient: recipient || depositor, + slippageTolerance: Number(slippageTolerance), + type: amountType, + refundOnOrigin, + refundAddress, + isInputNative, + isOutputNative, + }, + quoteFetchStrategies + ); + // Build tx for permit + const crossSwapTxForPermit = await buildPermitTxPayload( + crossSwapQuotes, + permitDeadline + ); + + const responseJson = crossSwapTxForPermit; + + logger.debug({ + at: "Swap/permit", + message: "Response data", + responseJson, + }); + response.status(200).json(responseJson); + } catch (error: unknown) { + return handleErrorCondition("swap/permit", response, logger, error); + } +}; + +export default handler; diff --git a/scripts/generate-routes.ts b/scripts/generate-routes.ts index 125f48956..d60be8cec 100644 --- a/scripts/generate-routes.ts +++ b/scripts/generate-routes.ts @@ -126,21 +126,26 @@ const enabledRoutes = { }, }, spokePoolPeripheryAddresses: { - "uniswap-swapRouter02": { - [CHAIN_IDs.POLYGON]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", - [CHAIN_IDs.OPTIMISM]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", - [CHAIN_IDs.ARBITRUM]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", - [CHAIN_IDs.BASE]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", - [CHAIN_IDs.ZK_SYNC]: "0xB007dFe9A6b70e1AB5BD5E97C22e47C5e0c0B8D8", - [CHAIN_IDs.WORLD_CHAIN]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", - [CHAIN_IDs.BLAST]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", - [CHAIN_IDs.ZORA]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", - [CHAIN_IDs.MAINNET]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", - }, - "uniswap-universalRouter": { - [CHAIN_IDs.OPTIMISM]: "0xaED9bBFdCC63219d77D54F8427aeb84C2Be46c5f", - [CHAIN_IDs.ARBITRUM]: "0xaED9bBFdCC63219d77D54F8427aeb84C2Be46c5f", - }, + [CHAIN_IDs.ARBITRUM]: "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", + [CHAIN_IDs.BASE]: "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", + [CHAIN_IDs.BLAST]: "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", + [CHAIN_IDs.MAINNET]: "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", + [CHAIN_IDs.OPTIMISM]: "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", + [CHAIN_IDs.POLYGON]: "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", + [CHAIN_IDs.WORLD_CHAIN]: "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", + [CHAIN_IDs.ZK_SYNC]: "0xB007dFe9A6b70e1AB5BD5E97C22e47C5e0c0B8D8", + [CHAIN_IDs.ZORA]: "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", + }, + spokePoolPeripheryProxyAddresses: { + [CHAIN_IDs.ARBITRUM]: "0xAa074de443aee6725EA5aC45FF2add1dC8366485", + [CHAIN_IDs.BASE]: "0xAa074de443aee6725EA5aC45FF2add1dC8366485", + [CHAIN_IDs.BLAST]: "0xAa074de443aee6725EA5aC45FF2add1dC8366485", + [CHAIN_IDs.MAINNET]: "0xAa074de443aee6725EA5aC45FF2add1dC8366485", + [CHAIN_IDs.OPTIMISM]: "0xAa074de443aee6725EA5aC45FF2add1dC8366485", + [CHAIN_IDs.POLYGON]: "0xAa074de443aee6725EA5aC45FF2add1dC8366485", + [CHAIN_IDs.WORLD_CHAIN]: "0xAa074de443aee6725EA5aC45FF2add1dC8366485", + [CHAIN_IDs.ZK_SYNC]: "0xB007dFe9A6b70e1AB5BD5E97C22e47C5e0c0B8D8", + [CHAIN_IDs.ZORA]: "0xAa074de443aee6725EA5aC45FF2add1dC8366485", }, routes: transformChainConfigs(enabledMainnetChainConfigs), }, @@ -172,9 +177,8 @@ const enabledRoutes = { universalSwapAndBridgeAddresses: { uniswap: {}, }, - spokePoolPeripheryAddresses: { - "uniswap-universalRouter": {}, - }, + spokePoolPeripheryAddresses: {}, + spokePoolPeripheryProxyAddresses: {}, routes: transformChainConfigs(enabledSepoliaChainConfigs), }, } as const; @@ -406,11 +410,11 @@ async function generateRoutes(hubPoolChainId = 1) { Record > ), - spokePoolPeripheryAddresses: checksumAddressesOfNestedMap( - config.spokePoolPeripheryAddresses as Record< - string, - Record - > + spokePoolPeripheryAddresses: checksumAddressOfMap( + config.spokePoolPeripheryAddresses as Record + ), + spokePoolPeripheryProxyAddresses: checksumAddressOfMap( + config.spokePoolPeripheryProxyAddresses as Record ), routes: config.routes.flatMap((route) => transformBridgeRoute(route, config.hubPoolChain) @@ -665,19 +669,20 @@ function getBridgedUsdcSymbol(chainId: number) { } } +function checksumAddressOfMap(map: Record) { + return Object.entries(map).reduce( + (acc, [key, value]) => ({ ...acc, [key]: utils.getAddress(value) }), + {} + ); +} + function checksumAddressesOfNestedMap( nestedMap: Record> ) { return Object.entries(nestedMap).reduce( (acc, [key, value]) => ({ ...acc, - [key]: Object.entries(value).reduce( - (acc, [chainId, address]) => ({ - ...acc, - [chainId]: utils.getAddress(address as string), - }), - {} - ), + [key]: checksumAddressOfMap(value), }), {} ); diff --git a/scripts/tests/_swap-utils.ts b/scripts/tests/_swap-utils.ts new file mode 100644 index 000000000..48d0fa14f --- /dev/null +++ b/scripts/tests/_swap-utils.ts @@ -0,0 +1,222 @@ +import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants"; +import { ethers, Wallet } from "ethers"; +import dotenv from "dotenv"; +import axios from "axios"; +import { getProvider } from "../../api/_utils"; +dotenv.config(); + +export const { SWAP_API_BASE_URL = "http://localhost:3000" } = process.env; + +export const depositor = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D"; +export const originChainId = CHAIN_IDs.OPTIMISM; +export const destinationChainId = CHAIN_IDs.ARBITRUM; +export const anyDestinationOutputTokens = { + [CHAIN_IDs.ARBITRUM]: "0x74885b4D524d497261259B38900f54e6dbAd2210", // APE +}; +export const MIN_OUTPUT_CASES = [ + // B2B + { + labels: ["B2B", "MIN_OUTPUT", "USDC - USDC"], + params: { + amount: ethers.utils.parseUnits("1", 6).toString(), + tradeType: "minOutput", + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[originChainId], + originChainId, + outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[destinationChainId], + destinationChainId, + depositor, + }, + }, + { + labels: ["B2B", "MIN_OUTPUT", "Native ETH - Native ETH"], + params: { + amount: ethers.utils.parseUnits("0.001", 18).toString(), + tradeType: "minOutput", + inputToken: ethers.constants.AddressZero, + originChainId, + outputToken: ethers.constants.AddressZero, + destinationChainId, + depositor, + }, + }, + { + labels: ["B2B", "MIN_OUTPUT", "USDC - Native ETH"], + params: { + amount: ethers.utils.parseUnits("0.001", 18).toString(), + tradeType: "minOutput", + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[originChainId], + originChainId, + outputToken: ethers.constants.AddressZero, + destinationChainId, + depositor, + }, + }, + { + labels: ["B2B", "MIN_OUTPUT", "Native ETH - USDC"], + params: { + amount: ethers.utils.parseUnits("3", 6).toString(), + tradeType: "minOutput", + inputToken: ethers.constants.AddressZero, + originChainId, + outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[destinationChainId], + destinationChainId, + depositor, + }, + }, + // B2A + { + labels: ["B2A", "MIN_OUTPUT", "USDC - APE"], + params: { + amount: ethers.utils.parseUnits("3", 18).toString(), + tradeType: "minOutput", + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[originChainId], + originChainId, + outputToken: anyDestinationOutputTokens[destinationChainId], + destinationChainId, + depositor, + }, + }, + { + labels: ["B2A", "MIN_OUTPUT", "USDC - Native ETH"], + params: { + amount: ethers.utils.parseUnits("0.001", 18).toString(), + tradeType: "minOutput", + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[originChainId], + originChainId, + outputToken: ethers.constants.AddressZero, + destinationChainId, + depositor, + }, + }, + // A2B + { + labels: ["A2B", "MIN_OUTPUT", "bridged USDC - USDC"], + params: { + amount: ethers.utils.parseUnits("1", 6).toString(), + tradeType: "minOutput", + inputToken: + TOKEN_SYMBOLS_MAP["USDC.e"].addresses[originChainId] || + TOKEN_SYMBOLS_MAP.USDbC.addresses[originChainId], + originChainId, + outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[destinationChainId], + destinationChainId, + depositor, + }, + }, + { + labels: ["A2B", "MIN_OUTPUT", "USDC - WETH"], + params: { + amount: ethers.utils.parseUnits("0.001", 18).toString(), + tradeType: "minOutput", + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[originChainId], + originChainId, + outputToken: TOKEN_SYMBOLS_MAP.WETH.addresses[destinationChainId], + destinationChainId, + depositor, + }, + }, + { + labels: ["A2B", "MIN_OUTPUT", "WETH - USDC"], + params: { + amount: ethers.utils.parseUnits("1", 6).toString(), + tradeType: "minOutput", + inputToken: TOKEN_SYMBOLS_MAP.WETH.addresses[originChainId], + originChainId, + outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[destinationChainId], + destinationChainId, + depositor, + }, + }, + // A2A + { + labels: ["A2A", "MIN_OUTPUT", "bridged USDC - APE"], + params: { + amount: ethers.utils.parseUnits("1", 18).toString(), + tradeType: "minOutput", + inputToken: + TOKEN_SYMBOLS_MAP["USDC.e"].addresses[originChainId] || + TOKEN_SYMBOLS_MAP.USDbC.addresses[originChainId], + originChainId, + outputToken: anyDestinationOutputTokens[destinationChainId], // APE Coin + destinationChainId, + depositor, + }, + }, +]; +export const EXACT_OUTPUT_CASES = MIN_OUTPUT_CASES.map((testCase) => ({ + labels: testCase.labels.map((label) => label.replace("MIN", "EXACT")), + params: { + ...testCase.params, + tradeType: "exactOutput", + }, +})); + +export function filterTestCases( + testCases: { + labels: string[]; + params: { [key: string]: any }; + }[], + filterString: string +) { + const labelsToFilter = filterString ? filterString.split(",") : []; + const filteredTestCases = testCases.filter((testCase) => { + const matches = labelsToFilter.filter((label) => + testCase.labels + .map((label) => label.toLowerCase()) + .includes(label.toLowerCase()) + ); + return matches.length === labelsToFilter.length; + }); + return filteredTestCases; +} + +export async function swap(slug: "approval" | "permit") { + 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, + }); + console.log(response.data); + + if (process.env.DEV_WALLET_PK) { + const wallet = new Wallet(process.env.DEV_WALLET_PK!).connect( + getProvider(testCase.params.originChainId) + ); + + if (response.data.approvalTxns) { + console.log("Approval needed..."); + let step = 1; + for (const approvalTxn of response.data.approvalTxns) { + const stepLabel = `(${step}/${response.data.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: response.data.swapTx.to, + data: response.data.swapTx.data, + value: response.data.swapTx.value, + gasLimit: response.data.swapTx.gas, + gasPrice: response.data.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 a32311ccd..05162cba1 100644 --- a/scripts/tests/swap-allowance.ts +++ b/scripts/tests/swap-allowance.ts @@ -1,222 +1,11 @@ -import axios from "axios"; -import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants"; -import { ethers, Wallet } from "ethers"; -import dotenv from "dotenv"; -import { getProvider } from "../../api/_utils"; -dotenv.config(); +import { swap } from "./_swap-utils"; -/** - * Manual test script for the swap API. Should be converted to a proper test suite. - */ - -const depositor = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D"; -const originChainId = CHAIN_IDs.OPTIMISM; -const destinationChainId = CHAIN_IDs.ARBITRUM; -const anyDestinationOutputTokens = { - [CHAIN_IDs.ARBITRUM]: "0x74885b4D524d497261259B38900f54e6dbAd2210", // APE -}; - -const MIN_OUTPUT_CASES = [ - // B2B - { - labels: ["B2B", "MIN_OUTPUT", "USDC - USDC"], - params: { - amount: ethers.utils.parseUnits("1", 6).toString(), - tradeType: "minOutput", - inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[originChainId], - originChainId, - outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[destinationChainId], - destinationChainId, - depositor, - }, - }, - { - labels: ["B2B", "MIN_OUTPUT", "Native ETH - Native ETH"], - params: { - amount: ethers.utils.parseUnits("0.001", 18).toString(), - tradeType: "minOutput", - inputToken: ethers.constants.AddressZero, - originChainId, - outputToken: ethers.constants.AddressZero, - destinationChainId, - depositor, - }, - }, - { - labels: ["B2B", "MIN_OUTPUT", "USDC - Native ETH"], - params: { - amount: ethers.utils.parseUnits("0.001", 18).toString(), - tradeType: "minOutput", - inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[originChainId], - originChainId, - outputToken: ethers.constants.AddressZero, - destinationChainId, - depositor, - }, - }, - { - labels: ["B2B", "MIN_OUTPUT", "Native ETH - USDC"], - params: { - amount: ethers.utils.parseUnits("3", 6).toString(), - tradeType: "minOutput", - inputToken: ethers.constants.AddressZero, - originChainId, - outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[destinationChainId], - destinationChainId, - depositor, - }, - }, - // B2A - { - labels: ["B2A", "MIN_OUTPUT", "USDC - APE"], - params: { - amount: ethers.utils.parseUnits("3", 18).toString(), - tradeType: "minOutput", - inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[originChainId], - originChainId, - outputToken: anyDestinationOutputTokens[destinationChainId], - destinationChainId, - depositor, - }, - }, - { - labels: ["B2A", "MIN_OUTPUT", "USDC - Native ETH"], - params: { - amount: ethers.utils.parseUnits("0.001", 18).toString(), - tradeType: "minOutput", - inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[originChainId], - originChainId, - outputToken: ethers.constants.AddressZero, - destinationChainId, - depositor, - }, - }, - // A2B - { - labels: ["A2B", "MIN_OUTPUT", "bridged USDC - USDC"], - params: { - amount: ethers.utils.parseUnits("1", 6).toString(), - tradeType: "minOutput", - inputToken: - TOKEN_SYMBOLS_MAP["USDC.e"].addresses[originChainId] || - TOKEN_SYMBOLS_MAP.USDbC.addresses[originChainId], - originChainId, - outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[destinationChainId], - destinationChainId, - depositor, - }, - }, - { - labels: ["A2B", "MIN_OUTPUT", "USDC - WETH"], - params: { - amount: ethers.utils.parseUnits("0.001", 18).toString(), - tradeType: "minOutput", - inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[originChainId], - originChainId, - outputToken: TOKEN_SYMBOLS_MAP.WETH.addresses[destinationChainId], - destinationChainId, - depositor, - }, - }, - { - labels: ["A2B", "MIN_OUTPUT", "WETH - USDC"], - params: { - amount: ethers.utils.parseUnits("1", 6).toString(), - tradeType: "minOutput", - inputToken: TOKEN_SYMBOLS_MAP.WETH.addresses[originChainId], - originChainId, - outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[destinationChainId], - destinationChainId, - depositor, - }, - }, - // A2A - { - labels: ["A2A", "MIN_OUTPUT", "bridged USDC - APE"], - params: { - amount: ethers.utils.parseUnits("1", 18).toString(), - tradeType: "minOutput", - inputToken: - TOKEN_SYMBOLS_MAP["USDC.e"].addresses[originChainId] || - TOKEN_SYMBOLS_MAP.USDbC.addresses[originChainId], - originChainId, - outputToken: anyDestinationOutputTokens[destinationChainId], // APE Coin - destinationChainId, - depositor, - }, - }, -]; -const EXACT_OUTPUT_CASES = MIN_OUTPUT_CASES.map((testCase) => ({ - labels: testCase.labels.map((label) => label.replace("MIN", "EXACT")), - params: { - ...testCase.params, - tradeType: "exactOutput", - }, -})); - -async function swap() { - const filterString = process.argv[2]; - const testCases = [...MIN_OUTPUT_CASES, ...EXACT_OUTPUT_CASES]; - const labelsToFilter = filterString ? filterString.split(",") : []; - const filteredTestCases = testCases.filter((testCase) => { - const matches = labelsToFilter.filter((label) => - testCase.labels - .map((label) => label.toLowerCase()) - .includes(label.toLowerCase()) - ); - return matches.length === labelsToFilter.length; - }); - for (const testCase of filteredTestCases) { - console.log("\nTest case:", testCase.labels.join(" ")); - console.log("Params:", testCase.params); - const response = await axios.get( - `http://localhost:3000/api/swap/allowance`, - { - params: testCase.params, - } - ); - console.log(response.data); - - if (process.env.DEV_WALLET_PK) { - const wallet = new Wallet(process.env.DEV_WALLET_PK!).connect( - getProvider(testCase.params.originChainId) - ); - - if (response.data.approvalTxns) { - console.log("Approval needed..."); - let step = 1; - for (const approvalTxn of response.data.approvalTxns) { - const stepLabel = `(${step}/${response.data.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: response.data.swapTx.to, - data: response.data.swapTx.data, - value: response.data.swapTx.value, - gasLimit: response.data.swapTx.gas, - gasPrice: response.data.swapTx.gasPrice, - }); - console.log("Tx hash: ", tx.hash); - await tx.wait(); - console.log("Tx mined"); - } catch (e) { - console.error("Tx reverted", e); - } - } - } +async function swapWithAllowance() { + console.log("Swapping with allowance..."); + await swap("approval"); } -swap() +swapWithAllowance() .then(() => console.log("Done")) .catch((e) => { console.error(e); diff --git a/scripts/tests/swap-permit.ts b/scripts/tests/swap-permit.ts new file mode 100644 index 000000000..29c8296eb --- /dev/null +++ b/scripts/tests/swap-permit.ts @@ -0,0 +1,15 @@ +import { swap } from "./_swap-utils"; + +async function swapWithPermit() { + console.log("Swapping with permit..."); + await swap("permit"); +} + +swapWithPermit() + .then(() => console.log("Done")) + .catch((e) => { + console.error(e); + if (e.response?.data) { + console.log("Tx for debug sim:", e.response.data.transaction); + } + }); diff --git a/src/data/routes_11155111_0x14224e63716afAcE30C9a417E0542281869f7d9e.json b/src/data/routes_11155111_0x14224e63716afAcE30C9a417E0542281869f7d9e.json index c70c802f3..0a883bab8 100644 --- a/src/data/routes_11155111_0x14224e63716afAcE30C9a417E0542281869f7d9e.json +++ b/src/data/routes_11155111_0x14224e63716afAcE30C9a417E0542281869f7d9e.json @@ -16,9 +16,8 @@ "universalSwapAndBridgeAddresses": { "uniswap": {} }, - "spokePoolPeripheryAddresses": { - "uniswap-universalRouter": {} - }, + "spokePoolPeripheryAddresses": {}, + "spokePoolPeripheryProxyAddresses": {}, "routes": [ { "fromChain": 11155111, diff --git a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json index ed6a01693..a4e5abbfc 100644 --- a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json +++ b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json @@ -39,21 +39,26 @@ } }, "spokePoolPeripheryAddresses": { - "uniswap-swapRouter02": { - "1": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", - "10": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", - "137": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", - "324": "0xB007dFe9A6b70e1AB5BD5E97C22e47C5e0c0B8D8", - "480": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", - "8453": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", - "42161": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", - "81457": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", - "7777777": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e" - }, - "uniswap-universalRouter": { - "10": "0xaED9bBFdCC63219d77D54F8427aeb84C2Be46c5f", - "42161": "0xaED9bBFdCC63219d77D54F8427aeb84C2Be46c5f" - } + "1": "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", + "10": "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", + "137": "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", + "324": "0xB007dFe9A6b70e1AB5BD5E97C22e47C5e0c0B8D8", + "480": "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", + "8453": "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", + "42161": "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", + "81457": "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", + "7777777": "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a" + }, + "spokePoolPeripheryProxyAddresses": { + "1": "0xAa074de443aee6725EA5aC45FF2add1dC8366485", + "10": "0xAa074de443aee6725EA5aC45FF2add1dC8366485", + "137": "0xAa074de443aee6725EA5aC45FF2add1dC8366485", + "324": "0xB007dFe9A6b70e1AB5BD5E97C22e47C5e0c0B8D8", + "480": "0xAa074de443aee6725EA5aC45FF2add1dC8366485", + "8453": "0xAa074de443aee6725EA5aC45FF2add1dC8366485", + "42161": "0xAa074de443aee6725EA5aC45FF2add1dC8366485", + "81457": "0xAa074de443aee6725EA5aC45FF2add1dC8366485", + "7777777": "0xAa074de443aee6725EA5aC45FF2add1dC8366485" }, "routes": [ { From d043df0e1f37dd4c5699e9e228cb2c4e1729eab3 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Sat, 28 Dec 2024 08:50:16 +0100 Subject: [PATCH 37/47] feat: base handler for `POST /relay` (#1345) * refactor: deposit and swap types * feat: add base /relay handler --- api/_dexes/utils.ts | 55 +++++++++- api/_permit.ts | 18 ---- api/_spoke-pool-periphery.ts | 44 +------- api/_utils.ts | 8 +- api/relay/_utils.ts | 175 ++++++++++++++++++++++++++++++++ api/relay/index.ts | 65 ++++++++++++ api/swap/_utils.ts | 14 +++ api/swap/approval/_utils.ts | 84 ++++++++------- api/swap/permit/_utils.ts | 115 +++++++++++++-------- api/swap/permit/index.ts | 12 ++- scripts/tests/_swap-utils.ts | 43 +------- scripts/tests/swap-allowance.ts | 44 +++++++- scripts/tests/swap-permit.ts | 42 +++++++- 13 files changed, 521 insertions(+), 198 deletions(-) create mode 100644 api/relay/_utils.ts create mode 100644 api/relay/index.ts diff --git a/api/_dexes/utils.ts b/api/_dexes/utils.ts index f8d0675d6..1546c9e61 100644 --- a/api/_dexes/utils.ts +++ b/api/_dexes/utils.ts @@ -1,4 +1,4 @@ -import { BigNumber, constants } from "ethers"; +import { BigNumber, BigNumberish, constants } from "ethers"; import { utils } from "@across-protocol/sdk"; import { SpokePool } from "@across-protocol/contracts/dist/typechain"; @@ -24,7 +24,9 @@ import { isOutputTokenBridgeable, getSpokePool, } from "../_utils"; - +import { GAS_SPONSOR_ADDRESS } from "../relay/_utils"; +import { SpokePoolV3PeripheryInterface } from "../_typechain/SpokePoolV3Periphery"; +import { TransferType } from "../_spoke-pool-periphery"; export type CrossSwapType = (typeof CROSS_SWAP_TYPE)[keyof typeof CROSS_SWAP_TYPE]; @@ -195,7 +197,11 @@ export function getFallbackRecipient(crossSwap: CrossSwap) { } export async function extractDepositDataStruct( - crossSwapQuotes: CrossSwapQuotes + crossSwapQuotes: CrossSwapQuotes, + submissionFees?: { + amount: BigNumberish; + recipient: string; + } ) { const originChainId = crossSwapQuotes.crossSwap.inputToken.chainId; const destinationChainId = crossSwapQuotes.crossSwap.outputToken.chainId; @@ -204,7 +210,7 @@ export async function extractDepositDataStruct( const refundAddress = crossSwapQuotes.crossSwap.refundAddress ?? crossSwapQuotes.crossSwap.depositor; - const deposit = { + const baseDepositData = { depositor: crossSwapQuotes.crossSwap.refundOnOrigin ? refundAddress : crossSwapQuotes.crossSwap.depositor, @@ -226,7 +232,46 @@ export async function extractDepositDataStruct( crossSwapQuotes.bridgeQuote.suggestedFees.exclusivityDeadline, message, }; - return deposit; + return { + inputAmount: baseDepositData.inputAmount, + baseDepositData, + submissionFees: submissionFees || { + amount: "0", + recipient: GAS_SPONSOR_ADDRESS, + }, + }; +} + +export async function extractSwapAndDepositDataStruct( + crossSwapQuotes: CrossSwapQuotes, + submissionFees?: { + amount: BigNumberish; + recipient: string; + } +): Promise { + const { originSwapQuote, contracts } = crossSwapQuotes; + const { originRouter } = contracts; + if (!originSwapQuote || !originRouter) { + throw new Error( + "Can not extract 'SwapAndDepositDataStruct' without originSwapQuote and originRouter" + ); + } + + const { baseDepositData, submissionFees: _submissionFees } = + await extractDepositDataStruct(crossSwapQuotes, submissionFees); + return { + submissionFees: submissionFees || _submissionFees, + depositData: baseDepositData, + swapToken: originSwapQuote.tokenIn.address, + swapTokenAmount: originSwapQuote.maximumAmountIn, + minExpectedInputTokenAmount: originSwapQuote.minAmountOut, + routerCalldata: originSwapQuote.swapTx.data, + exchange: originRouter.address, + transferType: + originRouter.name === "UniswapV3UniversalRouter" + ? TransferType.Transfer + : TransferType.Approval, + }; } async function getFillDeadline(spokePool: SpokePool): Promise { diff --git a/api/_permit.ts b/api/_permit.ts index f03f3f799..9f2521fcc 100644 --- a/api/_permit.ts +++ b/api/_permit.ts @@ -87,24 +87,6 @@ export async function getPermitTypedData(params: { domainSeparator, eip712: { types: { - EIP712Domain: [ - { - name: "name", - type: "string", - }, - { - name: "version", - type: "string", - }, - { - name: "chainId", - type: "uint256", - }, - { - name: "verifyingContract", - type: "address", - }, - ], Permit: [ { name: "owner", diff --git a/api/_spoke-pool-periphery.ts b/api/_spoke-pool-periphery.ts index a399f75ca..58928c5eb 100644 --- a/api/_spoke-pool-periphery.ts +++ b/api/_spoke-pool-periphery.ts @@ -1,28 +1,9 @@ -import { BigNumber } from "ethers"; -import { extractDepositDataStruct } from "./_dexes/utils"; import { SpokePoolPeripheryProxy__factory } from "./_typechain/factories/SpokePoolPeripheryProxy__factory"; import { SpokePoolV3Periphery__factory } from "./_typechain/factories/SpokePoolV3Periphery__factory"; import { ENABLED_ROUTES, getProvider } from "./_utils"; +import { SpokePoolV3PeripheryInterface } from "./_typechain/SpokePoolV3Periphery"; const sharedEIP712Types = { - EIP712Domain: [ - { - name: "name", - type: "string", - }, - { - name: "version", - type: "string", - }, - { - name: "chainId", - type: "uint256", - }, - { - name: "verifyingContract", - type: "address", - }, - ], Fees: [ { name: "amount", @@ -133,14 +114,7 @@ export function getSpokePoolPeripheryProxy(address: string, chainId: number) { } export async function getDepositTypedData(params: { - depositData: { - submissionFees: { - amount: BigNumber; - recipient: string; - }; - baseDepositData: Awaited>; - inputAmount: BigNumber; - }; + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct; chainId: number; }) { const spokePoolPeriphery = getSpokePoolPeriphery( @@ -185,19 +159,7 @@ export async function getDepositTypedData(params: { } export async function getSwapAndDepositTypedData(params: { - swapAndDepositData: { - submissionFees: { - amount: BigNumber; - recipient: string; - }; - depositData: Awaited>; - swapToken: string; - exchange: string; - transferType: TransferType; - swapTokenAmount: BigNumber; - minExpectedInputTokenAmount: BigNumber; - routerCalldata: string; - }; + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct; chainId: number; }) { const spokePoolPeriphery = getSpokePoolPeriphery( diff --git a/api/_utils.ts b/api/_utils.ts index 1fe12c9ea..c271413f6 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -1487,7 +1487,7 @@ export function validAddressOrENS() { export function positiveIntStr() { return define("positiveIntStr", (value) => { - return Number.isInteger(Number(value)) && Number(value) > 0; + return Number.isInteger(Number(value)) && Number(value) >= 0; }); } @@ -1503,6 +1503,12 @@ export function boolStr() { }); } +export function hexString() { + return define("hexString", (value) => { + return utils.isHexString(value); + }); +} + /** * Returns the cushion for a given token symbol and route. If no route is specified, the cushion for the token symbol * @param symbol The token symbol 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/api/swap/_utils.ts b/api/swap/_utils.ts index 055b7093d..bc5eda0ba 100644 --- a/api/swap/_utils.ts +++ b/api/swap/_utils.ts @@ -236,3 +236,17 @@ export async function calculateCrossSwapFees( destinationSwapFees, }; } + +export function stringifyBigNumProps(value: object): object { + return Object.fromEntries( + Object.entries(value).map(([key, value]) => { + if (value instanceof BigNumber) { + return [key, value.toString()]; + } + if (typeof value === "object" && value !== null) { + return [key, stringifyBigNumProps(value)]; + } + return [key, value]; + }) + ); +} diff --git a/api/swap/approval/_utils.ts b/api/swap/approval/_utils.ts index da8b19800..15f0715f7 100644 --- a/api/swap/approval/_utils.ts +++ b/api/swap/approval/_utils.ts @@ -6,9 +6,11 @@ import { getSpokePool } from "../../_utils"; import { getSpokePoolPeriphery, getSpokePoolPeripheryProxy, - TransferType, } from "../../_spoke-pool-periphery"; -import { extractDepositDataStruct } from "../../_dexes/utils"; +import { + extractDepositDataStruct, + extractSwapAndDepositDataStruct, +} from "../../_dexes/utils"; import { getUniversalSwapAndBridge } from "../../_swap-and-bridge"; export async function buildCrossSwapTxForAllowanceHolder( @@ -19,8 +21,6 @@ export async function buildCrossSwapTxForAllowanceHolder( const { originSwapEntryPoint, originRouter, depositEntryPoint } = contracts; const originChainId = crossSwap.inputToken.chainId; - const deposit = await extractDepositDataStruct(crossSwapQuotes); - let tx: PopulatedTransaction; let toAddress: string; @@ -31,25 +31,17 @@ export async function buildCrossSwapTxForAllowanceHolder( `'originSwapEntryPoint' and 'originRouter' need to be defined for origin swap quotes` ); } + + const swapAndDepositData = + await extractSwapAndDepositDataStruct(crossSwapQuotes); + if (originSwapEntryPoint.name === "SpokePoolPeripheryProxy") { const spokePoolPeripheryProxy = getSpokePoolPeripheryProxy( originSwapEntryPoint.address, originChainId ); tx = await spokePoolPeripheryProxy.populateTransaction.swapAndBridge( - { - submissionFees: { - amount: 0, - recipient: crossSwap.depositor, - }, - depositData: deposit, - swapToken: originSwapQuote.tokenIn.address, - exchange: originRouter.address, - transferType: TransferType.Approval, - swapTokenAmount: originSwapQuote.maximumAmountIn, - minExpectedInputTokenAmount: originSwapQuote.minAmountOut, - routerCalldata: originSwapQuote.swapTx.data, - } + swapAndDepositData // TODO: Add payable modifier to SpokePoolPeripheryProxy swapAndBridge function // { // value: crossSwap.isInputNative ? originSwapQuote.maximumAmountIn : 0, @@ -68,9 +60,11 @@ export async function buildCrossSwapTxForAllowanceHolder( originSwapQuote.maximumAmountIn, originSwapQuote.minAmountOut, { - ...deposit, + ...swapAndDepositData.depositData, + exclusivityDeadline: + swapAndDepositData.depositData.exclusivityParameter, // Typo in the contract - destinationChainid: deposit.destinationChainId, + destinationChainid: swapAndDepositData.depositData.destinationChainId, } ); toAddress = universalSwapAndBridge.address; @@ -88,26 +82,28 @@ export async function buildCrossSwapTxForAllowanceHolder( ); } + const { baseDepositData } = await extractDepositDataStruct(crossSwapQuotes); + if (depositEntryPoint.name === "SpokePoolPeriphery") { const spokePoolPeriphery = getSpokePoolPeriphery( depositEntryPoint.address, originChainId ); tx = await spokePoolPeriphery.populateTransaction.deposit( - deposit.recipient, - deposit.inputToken, - // deposit.outputToken, // TODO: allow for output token in periphery contract - deposit.inputAmount, - deposit.outputAmount, - deposit.destinationChainId, - deposit.exclusiveRelayer, - deposit.quoteTimestamp, - deposit.fillDeadline, - deposit.exclusivityDeadline, - deposit.message, + baseDepositData.recipient, + baseDepositData.inputToken, + // baseDepositData.outputToken, // TODO: allow for output token in periphery contract + baseDepositData.inputAmount, + baseDepositData.outputAmount, + baseDepositData.destinationChainId, + baseDepositData.exclusiveRelayer, + baseDepositData.quoteTimestamp, + baseDepositData.fillDeadline, + baseDepositData.exclusivityDeadline, + baseDepositData.message, { value: crossSwapQuotes.crossSwap.isInputNative - ? deposit.inputAmount + ? baseDepositData.inputAmount : 0, } ); @@ -115,21 +111,21 @@ export async function buildCrossSwapTxForAllowanceHolder( } else if (depositEntryPoint.name === "SpokePool") { const spokePool = getSpokePool(originChainId); tx = await spokePool.populateTransaction.depositV3( - deposit.depositor, - deposit.recipient, - deposit.inputToken, - deposit.outputToken, - deposit.inputAmount, - deposit.outputAmount, - deposit.destinationChainId, - deposit.exclusiveRelayer, - deposit.quoteTimestamp, - deposit.fillDeadline, - deposit.exclusivityDeadline, - deposit.message, + baseDepositData.depositor, + baseDepositData.recipient, + baseDepositData.inputToken, + baseDepositData.outputToken, + baseDepositData.inputAmount, + baseDepositData.outputAmount, + baseDepositData.destinationChainId, + baseDepositData.exclusiveRelayer, + baseDepositData.quoteTimestamp, + baseDepositData.fillDeadline, + baseDepositData.exclusivityDeadline, + baseDepositData.message, { value: crossSwapQuotes.crossSwap.isInputNative - ? deposit.inputAmount + ? baseDepositData.inputAmount : 0, } ); diff --git a/api/swap/permit/_utils.ts b/api/swap/permit/_utils.ts index 49a231945..d1bb441e1 100644 --- a/api/swap/permit/_utils.ts +++ b/api/swap/permit/_utils.ts @@ -1,3 +1,5 @@ +import { BigNumberish } from "ethers"; + import { CrossSwapQuotes, DepositEntryPointContract, @@ -7,29 +9,54 @@ import { getPermitTypedData } from "../../_permit"; import { getDepositTypedData, getSwapAndDepositTypedData, - TransferType, } from "../../_spoke-pool-periphery"; -import { extractDepositDataStruct } from "../../_dexes/utils"; -import { BigNumber } from "ethers"; +import { + extractDepositDataStruct, + extractSwapAndDepositDataStruct, +} from "../../_dexes/utils"; +import { SpokePoolV3PeripheryInterface } from "../../_typechain/SpokePoolV3Periphery"; +import { stringifyBigNumProps } from "../_utils"; -export async function buildPermitTxPayload( - crossSwapQuotes: CrossSwapQuotes, - permitDeadline: number -) { +export async function buildPermitTxPayload({ + crossSwapQuotes, + permitDeadline, + submissionFees, +}: { + crossSwapQuotes: CrossSwapQuotes; + permitDeadline: number; + submissionFees?: { + amount: BigNumberish; + recipient: string; + }; +}) { const { originSwapQuote, bridgeQuote, crossSwap, contracts } = crossSwapQuotes; const originChainId = crossSwap.inputToken.chainId; const { originSwapEntryPoint, depositEntryPoint, originRouter } = contracts; - const baseDepositData = await extractDepositDataStruct(crossSwapQuotes); - let entryPointContract: | DepositEntryPointContract | OriginSwapEntryPointContract; let getDepositTypedDataPromise: | ReturnType | ReturnType; - let methodName: string; + let methodNameAndArgsWithoutSignatures: + | { + methodName: "depositWithPermit"; + argsWithoutSignatures: { + signatureOwner: string; + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct; + deadline: BigNumberish; + }; + } + | { + methodName: "swapAndBridgeWithPermit"; + argsWithoutSignatures: { + signatureOwner: string; + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct; + deadline: BigNumberish; + }; + }; if (originSwapQuote) { if (!originSwapEntryPoint) { @@ -50,28 +77,21 @@ export async function buildPermitTxPayload( ); } + const swapAndDepositData = + await extractSwapAndDepositDataStruct(crossSwapQuotes); entryPointContract = originSwapEntryPoint; getDepositTypedDataPromise = getSwapAndDepositTypedData({ - swapAndDepositData: { - // TODO: Make this dynamic - submissionFees: { - amount: BigNumber.from(0), - recipient: crossSwapQuotes.crossSwap.depositor, - }, - depositData: baseDepositData, - swapToken: originSwapQuote.tokenIn.address, - swapTokenAmount: originSwapQuote.maximumAmountIn, - minExpectedInputTokenAmount: originSwapQuote.minAmountOut, - routerCalldata: originSwapQuote.swapTx.data, - exchange: originRouter.address, - transferType: - originRouter.name === "UniswapV3UniversalRouter" - ? TransferType.Transfer - : TransferType.Approval, - }, + swapAndDepositData: swapAndDepositData, chainId: originChainId, }); - methodName = "swapAndBridgeWithPermit"; + methodNameAndArgsWithoutSignatures = { + methodName: "swapAndBridgeWithPermit", + argsWithoutSignatures: { + signatureOwner: crossSwap.depositor, + swapAndDepositData, + deadline: permitDeadline, + }, + }; } else { if (!depositEntryPoint) { throw new Error( @@ -84,21 +104,23 @@ export async function buildPermitTxPayload( `Permit is not supported for deposit entry point contract '${depositEntryPoint.name}'` ); } - + const depositDataStruct = await extractDepositDataStruct( + crossSwapQuotes, + submissionFees + ); entryPointContract = depositEntryPoint; getDepositTypedDataPromise = getDepositTypedData({ - depositData: { - // TODO: Make this dynamic - submissionFees: { - amount: BigNumber.from(0), - recipient: crossSwap.depositor, - }, - baseDepositData, - inputAmount: BigNumber.from(bridgeQuote.inputAmount), - }, + depositData: depositDataStruct, chainId: originChainId, }); - methodName = "depositWithPermit"; + methodNameAndArgsWithoutSignatures = { + methodName: "depositWithPermit", + argsWithoutSignatures: { + signatureOwner: crossSwap.depositor, + depositData: depositDataStruct, + deadline: permitDeadline, + }, + }; } const [permitTypedData, depositTypedData] = await Promise.all([ @@ -115,13 +137,22 @@ export async function buildPermitTxPayload( ]); return { eip712: { - permit: permitTypedData.eip712, - deposit: depositTypedData.eip712, + permit: { + ...permitTypedData.eip712, + message: stringifyBigNumProps(permitTypedData.eip712.message), + }, + deposit: { + ...depositTypedData.eip712, + message: stringifyBigNumProps(depositTypedData.eip712.message), + }, }, swapTx: { chainId: originChainId, to: entryPointContract.address, - methodName, + methodName: methodNameAndArgsWithoutSignatures.methodName, + argsWithoutSignatures: stringifyBigNumProps( + methodNameAndArgsWithoutSignatures.argsWithoutSignatures + ), }, }; } diff --git a/api/swap/permit/index.ts b/api/swap/permit/index.ts index bb141c6d0..3738d340d 100644 --- a/api/swap/permit/index.ts +++ b/api/swap/permit/index.ts @@ -9,6 +9,7 @@ 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"; export const PermitSwapQueryParamsSchema = type({ permitDeadline: optional(positiveIntStr()), @@ -85,10 +86,15 @@ const handler = async ( quoteFetchStrategies ); // Build tx for permit - const crossSwapTxForPermit = await buildPermitTxPayload( + const crossSwapTxForPermit = await buildPermitTxPayload({ crossSwapQuotes, - permitDeadline - ); + permitDeadline, + // FIXME: Calculate proper fees + submissionFees: { + amount: "0", + recipient: GAS_SPONSOR_ADDRESS, + }, + }); const responseJson = crossSwapTxForPermit; diff --git a/scripts/tests/_swap-utils.ts b/scripts/tests/_swap-utils.ts index 48d0fa14f..d0b776c92 100644 --- a/scripts/tests/_swap-utils.ts +++ b/scripts/tests/_swap-utils.ts @@ -1,8 +1,7 @@ import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants"; -import { ethers, Wallet } from "ethers"; +import { ethers } from "ethers"; import dotenv from "dotenv"; import axios from "axios"; -import { getProvider } from "../../api/_utils"; dotenv.config(); export const { SWAP_API_BASE_URL = "http://localhost:3000" } = process.env; @@ -170,7 +169,7 @@ export function filterTestCases( return filteredTestCases; } -export async function swap(slug: "approval" | "permit") { +export async function fetchSwapQuote(slug: "approval" | "permit") { const filterString = process.argv[2]; const testCases = [...MIN_OUTPUT_CASES, ...EXACT_OUTPUT_CASES]; const filteredTestCases = filterTestCases(testCases, filterString); @@ -181,42 +180,6 @@ export async function swap(slug: "approval" | "permit") { params: testCase.params, }); console.log(response.data); - - if (process.env.DEV_WALLET_PK) { - const wallet = new Wallet(process.env.DEV_WALLET_PK!).connect( - getProvider(testCase.params.originChainId) - ); - - if (response.data.approvalTxns) { - console.log("Approval needed..."); - let step = 1; - for (const approvalTxn of response.data.approvalTxns) { - const stepLabel = `(${step}/${response.data.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: response.data.swapTx.to, - data: response.data.swapTx.data, - value: response.data.swapTx.value, - gasLimit: response.data.swapTx.gas, - gasPrice: response.data.swapTx.gasPrice, - }); - console.log("Tx hash: ", tx.hash); - await tx.wait(); - console.log("Tx mined"); - } catch (e) { - console.error("Tx reverted", e); - } - } + return response.data; } } diff --git a/scripts/tests/swap-allowance.ts b/scripts/tests/swap-allowance.ts index 05162cba1..91bbdad27 100644 --- a/scripts/tests/swap-allowance.ts +++ b/scripts/tests/swap-allowance.ts @@ -1,8 +1,48 @@ -import { swap } from "./_swap-utils"; +import { Wallet } from "ethers"; + +import { getProvider } from "../../api/_utils"; +import { fetchSwapQuote } from "./_swap-utils"; async function swapWithAllowance() { console.log("Swapping with allowance..."); - await swap("approval"); + const swapQuote = await fetchSwapQuote("approval"); + + if (process.env.DEV_WALLET_PK) { + const wallet = new Wallet(process.env.DEV_WALLET_PK!).connect( + getProvider(swapQuote.params.originChainId) + ); + + 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); + } + } } swapWithAllowance() 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() From 3e650d2ffa1e44d69cc9580ad75002323294655a Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Mon, 30 Dec 2024 13:13:13 +0100 Subject: [PATCH 38/47] feat: gelato and local signers (#1346) * refactor: deposit and swap types * feat: add base /relay handler * feat: add qstash and gelato strategy * re-introduce requestHash * fixup * fixup * fix: addresses * fixup * fix: process message * feat: local signers strategy * fixup * fixup * fixup * fix: local signer strategy * cleanup * fix: error message * fixup * review requests --- api/_cache.ts | 7 + api/_spoke-pool-periphery.ts | 38 ++ api/_utils.ts | 4 +- api/relay/_queue.ts | 46 ++ api/relay/_strategies/gelato.ts | 92 ++++ api/relay/_strategies/index.ts | 11 + api/relay/_strategies/local-signers.ts | 92 ++++ api/relay/_types.ts | 19 + api/relay/_utils.ts | 191 +++++++- api/relay/index.ts | 43 +- api/relay/jobs/process.ts | 127 +++++ api/relay/status.ts | 45 ++ api/swap/approval/_utils.ts | 16 +- api/swap/permit/_utils.ts | 16 +- api/swap/permit/index.ts | 8 +- package.json | 1 + scripts/generate-routes.ts | 36 +- scripts/tests/swap-permit.ts | 20 +- ...6fA914353c44b2E33eBE05f21846F1048bEda.json | 36 +- vercel.json | 3 + yarn.lock | 432 ++---------------- 21 files changed, 805 insertions(+), 478 deletions(-) create mode 100644 api/relay/_queue.ts create mode 100644 api/relay/_strategies/gelato.ts create mode 100644 api/relay/_strategies/index.ts create mode 100644 api/relay/_strategies/local-signers.ts create mode 100644 api/relay/_types.ts create mode 100644 api/relay/jobs/process.ts create mode 100644 api/relay/status.ts diff --git a/api/_cache.ts b/api/_cache.ts index 2380cbc62..d637dc667 100644 --- a/api/_cache.ts +++ b/api/_cache.ts @@ -61,6 +61,13 @@ export class RedisCache implements interfaces.CachingMechanismInterface { throw error; } } + + async del(key: string) { + if (!this.client) { + return; + } + await this.client.del(key); + } } export const redisCache = new RedisCache(); diff --git a/api/_spoke-pool-periphery.ts b/api/_spoke-pool-periphery.ts index 58928c5eb..38c7605ba 100644 --- a/api/_spoke-pool-periphery.ts +++ b/api/_spoke-pool-periphery.ts @@ -222,3 +222,41 @@ export async function getSwapAndDepositTypedData(params: { }, }; } + +export function encodeDepositWithPermitCalldata(args: { + signatureOwner: string; + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct; + deadline: number; + permitSignature: string; + depositDataSignature: string; +}) { + return SpokePoolV3Periphery__factory.createInterface().encodeFunctionData( + "depositWithPermit", + [ + args.signatureOwner, + args.depositData, + args.deadline, + args.permitSignature, + args.depositDataSignature, + ] + ); +} + +export function encodeSwapAndBridgeWithPermitCalldata(args: { + signatureOwner: string; + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct; + deadline: number; + permitSignature: string; + swapAndDepositDataSignature: string; +}) { + return SpokePoolV3Periphery__factory.createInterface().encodeFunctionData( + "swapAndBridgeWithPermit", + [ + args.signatureOwner, + args.swapAndDepositData, + args.deadline, + args.permitSignature, + args.swapAndDepositDataSignature, + ] + ); +} diff --git a/api/_utils.ts b/api/_utils.ts index c271413f6..238c8687f 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -201,8 +201,8 @@ export const getLogger = (): LoggingUtility => { * Resolves the current vercel endpoint dynamically * @returns A valid URL of the current endpoint in vercel */ -export const resolveVercelEndpoint = () => { - if (process.env.REACT_APP_VERCEL_API_BASE_URL_OVERRIDE) { +export const resolveVercelEndpoint = (omitOverride = false) => { + if (!omitOverride && process.env.REACT_APP_VERCEL_API_BASE_URL_OVERRIDE) { return process.env.REACT_APP_VERCEL_API_BASE_URL_OVERRIDE; } const url = process.env.VERCEL_URL ?? "across.to"; diff --git a/api/relay/_queue.ts b/api/relay/_queue.ts new file mode 100644 index 000000000..aac70b346 --- /dev/null +++ b/api/relay/_queue.ts @@ -0,0 +1,46 @@ +import { Client } from "@upstash/qstash"; + +import { RelayRequest, RelayStrategy, RelayStrategyName } from "./_types"; +import { resolveVercelEndpoint } from "../_utils"; + +const client = new Client({ + token: process.env.QSTASH_TOKEN!, +}); + +export async function pushRelayRequestToQueue({ + request, + strategy, +}: { + request: RelayRequest; + strategy: RelayStrategy; +}) { + const strategyName = strategy.strategyName; + const queue = getRelayRequestQueue(strategyName, request.chainId); + await queue.upsert({ + parallelism: strategy.queueParallelism, + }); + + const baseUrl = resolveVercelEndpoint(true); + const response = await queue.enqueueJSON({ + retries: 3, + contentBasedDeduplication: true, + headers: new Headers({ + "Retry-After": "1", + }), + url: `${baseUrl}/api/relay/jobs/process`, + body: { + request, + strategyName, + }, + }); + return response; +} + +function getRelayRequestQueue( + strategyName: RelayStrategyName, + chainId: number +) { + return client.queue({ + queueName: `relay-request-queue-${chainId}-${strategyName}`, + }); +} diff --git a/api/relay/_strategies/gelato.ts b/api/relay/_strategies/gelato.ts new file mode 100644 index 000000000..bbeca011f --- /dev/null +++ b/api/relay/_strategies/gelato.ts @@ -0,0 +1,92 @@ +import axios from "axios"; +import { RelayRequest, RelayStrategy } from "../_types"; +import { encodeCalldataForRelayRequest } from "../_utils"; + +const GELATO_API_KEY = process.env.GELATO_API_KEY; + +export function getGelatoStrategy(): RelayStrategy { + return { + strategyName: "gelato", + queueParallelism: 2, + relay: async (request: RelayRequest) => { + const encodedCalldata = encodeCalldataForRelayRequest(request); + + const taskId = await relayWithGelatoApi({ + chainId: request.chainId, + target: request.to, + data: encodedCalldata, + }); + + let txHash: string | undefined; + + while (true) { + const taskStatus = await getGelatoTaskStatus(taskId); + + if ( + ["Cancelled", "NotFound", "ExecReverted", "Blacklisted"].includes( + taskStatus.taskState + ) + ) { + throw new Error( + `Can not relay request via Gelato due to task state ${taskStatus.taskState}` + ); + } + + if (taskStatus.transactionHash) { + txHash = taskStatus.transactionHash; + break; + } + + await new Promise((resolve) => setTimeout(resolve, 1_000)); + } + + return txHash; + }, + }; +} + +const gelatoBaseUrl = "https://api.gelato.digital"; + +async function relayWithGelatoApi({ + chainId, + target, + data, +}: { + chainId: number; + target: string; + data: string; +}) { + if (!GELATO_API_KEY) { + throw new Error("Can not call Gelato API: key is not set"); + } + + const response = await axios.post( + `${gelatoBaseUrl}/relays/v2/sponsored-call`, + { + chainId, + target, + data, + sponsorApiKey: GELATO_API_KEY, + } + ); + + return response.data.taskId as string; +} + +async function getGelatoTaskStatus(taskId: string) { + const response = await axios.get<{ + task: { + taskState: + | "CheckPending" + | "ExecPending" + | "ExecSuccess" + | "ExecReverted" + | "WaitingForConfirmation" + | "Blacklisted" + | "Cancelled" + | "NotFound"; + transactionHash?: string; + }; + }>(`${gelatoBaseUrl}/tasks/status/${taskId}`); + return response.data.task; +} diff --git a/api/relay/_strategies/index.ts b/api/relay/_strategies/index.ts new file mode 100644 index 000000000..9b621c0eb --- /dev/null +++ b/api/relay/_strategies/index.ts @@ -0,0 +1,11 @@ +import { RelayStrategy, RelayStrategyName } from "../_types"; +import { getGelatoStrategy } from "./gelato"; +import { getLocalSignersStrategy } from "./local-signers"; + +const gelatoStrategy = getGelatoStrategy(); +const localSignersStrategy = getLocalSignersStrategy(); + +export const strategiesByName = { + [gelatoStrategy.strategyName]: gelatoStrategy, + [localSignersStrategy.strategyName]: localSignersStrategy, +} as Record; diff --git a/api/relay/_strategies/local-signers.ts b/api/relay/_strategies/local-signers.ts new file mode 100644 index 000000000..d895f1b82 --- /dev/null +++ b/api/relay/_strategies/local-signers.ts @@ -0,0 +1,92 @@ +import { Wallet, utils } from "ethers"; + +import { RelayRequest, RelayStrategy } from "../_types"; +import { encodeCalldataForRelayRequest } from "../_utils"; +import { redisCache } from "../../_cache"; +import { getProvider } from "../../_utils"; + +const localSignerPrivateKeys = + process.env.LOCAL_SIGNER_PRIVATE_KEYS!.split(","); +const balanceAlertThreshold = utils.parseEther("0.000001"); // TODO: Refine value + +export function getLocalSignersStrategy(): RelayStrategy { + return { + strategyName: "local-signers", + queueParallelism: 1, // TODO: Should be dynamic based on the number of local signers + relay: async (request: RelayRequest) => { + const encodedCalldata = encodeCalldataForRelayRequest(request); + + if (localSignerPrivateKeys.length === 0) { + throw new Error( + "Can not relay tx via local signers: No local signers found" + ); + } + + for (const signerPrivateKey of localSignerPrivateKeys) { + const provider = getProvider(request.chainId); + const wallet = new Wallet(signerPrivateKey, provider); + try { + await lockSigner(wallet.address, request.chainId); + + const balance = await wallet.getBalance(); + if (balance.lt(balanceAlertThreshold)) { + // TODO: Send PD alert + } + + const txRequest = { + chainId: request.chainId, + to: request.to, + data: encodedCalldata, + from: wallet.address, + }; + const tx = await wallet.sendTransaction(txRequest); + const receipt = await tx.wait(); + return receipt.transactionHash; + } catch (error) { + if (error instanceof SignerLockedError) { + continue; + } + throw error; + } finally { + await unlockSigner(wallet.address, request.chainId); + } + } + + throw new Error( + "Can not relay tx via local signers: All local signers are locked" + ); + }, + }; +} + +async function lockSigner(signerAddress: string, chainId: number) { + const lockKey = getLockKey(signerAddress, chainId); + const lockValue = await redisCache.get(lockKey); + + if (lockValue) { + throw new SignerLockedError(signerAddress, chainId); + } + + await redisCache.set(lockKey, "true", 30); +} + +async function unlockSigner(signerAddress: string, chainId: number) { + const lockKey = getLockKey(signerAddress, chainId); + + const lockValue = await redisCache.get(lockKey); + if (!lockValue) { + return; + } + + await redisCache.del(lockKey); +} + +function getLockKey(signerAddress: string, chainId: number) { + return `signer-lock:${signerAddress}:${chainId}`; +} + +class SignerLockedError extends Error { + constructor(signerAddress: string, chainId: number) { + super(`Signer ${signerAddress} on chain ${chainId} is already locked`); + } +} diff --git a/api/relay/_types.ts b/api/relay/_types.ts new file mode 100644 index 000000000..839934d49 --- /dev/null +++ b/api/relay/_types.ts @@ -0,0 +1,19 @@ +import { validateMethodArgs } from "./_utils"; + +export type RelayStrategyName = "gelato" | "local-signers"; + +export type RelayRequest = { + chainId: number; + to: string; + methodNameAndArgs: ReturnType; + signatures: { + permit: string; + deposit: string; + }; +}; + +export type RelayStrategy = { + strategyName: RelayStrategyName; + queueParallelism: number; + relay: (request: RelayRequest) => Promise; +}; diff --git a/api/relay/_utils.ts b/api/relay/_utils.ts index f46783d63..82e41ab49 100644 --- a/api/relay/_utils.ts +++ b/api/relay/_utils.ts @@ -5,11 +5,17 @@ import { hexString, positiveIntStr, validAddress } from "../_utils"; import { getPermitTypedData } from "../_permit"; import { InvalidParamError } from "../_errors"; import { + encodeDepositWithPermitCalldata, + encodeSwapAndBridgeWithPermitCalldata, getDepositTypedData, getSwapAndDepositTypedData, } from "../_spoke-pool-periphery"; +import { RelayRequest } from "./_types"; +import { redisCache } from "../_cache"; -export const GAS_SPONSOR_ADDRESS = "0x0000000000000000000000000000000000000000"; +export const GAS_SPONSOR_ADDRESS = + process.env.GAS_SPONSOR_ADDRESS ?? + "0x0000000000000000000000000000000000000000"; const SubmissionFeesSchema = type({ amount: positiveIntStr(), @@ -79,21 +85,12 @@ export function validateMethodArgs(methodName: string, args: any) { 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; +export async function verifySignatures({ + methodNameAndArgs, + signatures, + chainId, + to, +}: RelayRequest) { const { methodName, args } = methodNameAndArgs; let signatureOwner: string; @@ -107,14 +104,14 @@ export async function verifySignatures(params: { signatureOwner = _signatureOwner; getPermitTypedDataPromise = getPermitTypedData({ tokenAddress: depositData.baseDepositData.inputToken, - chainId: originChainId, + chainId, ownerAddress: signatureOwner, - spenderAddress: entryPointContractAddress, + spenderAddress: to, // SpokePoolV3Periphery address value: depositData.inputAmount, deadline: Number(deadline), }); getDepositTypedDataPromise = getDepositTypedData({ - chainId: originChainId, + chainId, depositData, }); } else if (methodName === "swapAndBridgeWithPermit") { @@ -126,14 +123,14 @@ export async function verifySignatures(params: { signatureOwner = _signatureOwner; getPermitTypedDataPromise = getPermitTypedData({ tokenAddress: swapAndDepositData.swapToken, - chainId: originChainId, + chainId, ownerAddress: signatureOwner, - spenderAddress: entryPointContractAddress, + spenderAddress: to, // SpokePoolV3Periphery address value: swapAndDepositData.swapTokenAmount, deadline: Number(deadline), }); getDepositTypedDataPromise = getSwapAndDepositTypedData({ - chainId: originChainId, + chainId, swapAndDepositData, }); } else { @@ -173,3 +170,153 @@ export async function verifySignatures(params: { }); } } + +export function encodeCalldataForRelayRequest(request: RelayRequest) { + let encodedCalldata: string; + + if (request.methodNameAndArgs.methodName === "depositWithPermit") { + encodedCalldata = encodeDepositWithPermitCalldata({ + ...request.methodNameAndArgs.args, + deadline: Number(request.methodNameAndArgs.args.deadline), + depositDataSignature: request.signatures.deposit, + permitSignature: request.signatures.permit, + }); + } else if ( + request.methodNameAndArgs.methodName === "swapAndBridgeWithPermit" + ) { + encodedCalldata = encodeSwapAndBridgeWithPermitCalldata({ + ...request.methodNameAndArgs.args, + deadline: Number(request.methodNameAndArgs.args.deadline), + swapAndDepositDataSignature: request.signatures.deposit, + permitSignature: request.signatures.permit, + }); + } + // TODO: Add cases for `withAuth` and `withPermit2` + else { + throw new Error(`Can not encode calldata for relay request`); + } + + 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; + messageId: string; + } + | { + status: "success"; + request: RelayRequest; + txHash: string; + messageId: string; + } + | { + status: "failure"; + request: RelayRequest; + error: Error; + messageId: string; + } + | { + status: "unknown"; + }; +// TODO: Refine value +const cachedRelayRequestTTL = 5 * 60 * 60 * 24; // 5 days + +export async function getCachedRelayRequest( + requestOrHash: RelayRequest | string +): Promise { + const cachedRelayRequest = await redisCache.get( + getRelayRequestCacheKey(requestOrHash) + ); + + if (!cachedRelayRequest) { + return { + status: "unknown", + }; + } + + return cachedRelayRequest; +} + +export async function setCachedRelayRequestPending(params: { + messageId: string; + request: RelayRequest; +}) { + await redisCache.set( + getRelayRequestCacheKey(params.request), + { + status: "pending", + messageId: params.messageId, + request: params.request, + }, + cachedRelayRequestTTL + ); +} + +export async function setCachedRelayRequestFailure(params: { + 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 === "success") { + throw new Error( + "Can not set 'failure' status for request that is already successful" + ); + } + + await redisCache.set( + getRelayRequestCacheKey(params.request), + { + status: "failure", + messageId: cachedRelayRequest.messageId, + request: cachedRelayRequest.request, + error: params.error, + }, + cachedRelayRequestTTL + ); +} + +export async function setCachedRelayRequestSuccess(params: { + request: RelayRequest; + txHash: string; +}) { + const cachedRelayRequest = await getCachedRelayRequest(params.request); + + if (!cachedRelayRequest || cachedRelayRequest.status === "unknown") { + throw new Error("Request not found in cache"); + } + + await redisCache.set( + getRelayRequestCacheKey(params.request), + { + status: "success", + messageId: cachedRelayRequest.messageId, + request: cachedRelayRequest.request, + txHash: params.txHash, + }, + cachedRelayRequestTTL + ); +} + +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 1716a2acc..1bd44d14f 100644 --- a/api/relay/index.ts +++ b/api/relay/index.ts @@ -5,11 +5,17 @@ import { handleErrorCondition } from "../_errors"; import { getLogger, hexString, validAddress } from "../_utils"; import { allowedMethodNames, + getRelayRequestHash, + setCachedRelayRequestPending, validateMethodArgs, verifySignatures, } from "./_utils"; +import { strategiesByName } from "./_strategies"; +import { CHAIN_IDs } from "../_constants"; +import { pushRelayRequestToQueue } from "./_queue"; +import { RelayRequest } from "./_types"; -const BaseRelayRequestBodySchema = object({ +export const BaseRelayRequestBodySchema = object({ chainId: number(), to: validAddress(), methodName: enums(allowedMethodNames), @@ -20,6 +26,11 @@ const BaseRelayRequestBodySchema = object({ }), }); +const strategies = { + default: strategiesByName.gelato, + [CHAIN_IDs.ARBITRUM]: strategiesByName["local-signers"], +}; + export default async function handler( request: VercelRequest, response: VercelResponse @@ -45,19 +56,35 @@ export default async function handler( ); // Verify signatures - const { signatures } = request.body; + const { signatures, chainId, to } = request.body; await verifySignatures({ methodNameAndArgs, signatures, - originChainId: request.body.chainId, - entryPointContractAddress: request.body.to, + chainId, + to, + }); + + // Push request to queue + const strategy = strategies[chainId] ?? strategies.default; + const relayRequest: RelayRequest = { + chainId, + to, + methodNameAndArgs, + signatures, + }; + const queueResponse = await pushRelayRequestToQueue({ + request: relayRequest, + strategy, }); - // TODO: Execute transaction based on configured strategies + // Store requestId in database + await setCachedRelayRequestPending({ + messageId: queueResponse.messageId, + request: relayRequest, + }); - return response.status(200).json({ - success: true, - // Add relevant response data + response.status(200).json({ + requestHash: getRelayRequestHash(relayRequest), }); } catch (error) { return handleErrorCondition("api/relay", response, logger, error); diff --git a/api/relay/jobs/process.ts b/api/relay/jobs/process.ts new file mode 100644 index 000000000..0d3973bf8 --- /dev/null +++ b/api/relay/jobs/process.ts @@ -0,0 +1,127 @@ +import { VercelRequest, VercelResponse } from "@vercel/node"; +import { assert, enums, number, object, type } from "superstruct"; +import { Receiver } from "@upstash/qstash"; + +import { handleErrorCondition, InvalidParamError } from "../../_errors"; +import { getLogger, hexString, validAddress } from "../../_utils"; +import { + validateMethodArgs, + verifySignatures, + setCachedRelayRequestSuccess, + setCachedRelayRequestFailure, + getCachedRelayRequest, + allowedMethodNames, +} from "../_utils"; +import { RelayRequest, RelayStrategyName } from "../_types"; +import { strategiesByName } from "../_strategies"; + +const messageReceiver = new Receiver({ + currentSigningKey: process.env.QSTASH_CURRENT_SIGNING_KEY!, + nextSigningKey: process.env.QSTASH_NEXT_SIGNING_KEY!, +}); + +const RelayProcessJobBodySchema = type({ + strategyName: enums(Object.keys(strategiesByName)), + request: type({ + chainId: number(), + to: validAddress(), + methodNameAndArgs: type({ + methodName: enums(allowedMethodNames), + args: object(), + }), + signatures: object({ + permit: hexString(), + deposit: hexString(), + }), + }), +}); + +export default async function handler(req: VercelRequest, res: VercelResponse) { + const logger = getLogger(); + logger.debug({ + at: "Relay/jobs/process", + message: "Request body", + body: req.body, + }); + + try { + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + // Verify message comes from QSTASH + const isValid = await messageReceiver.verify({ + signature: (req.headers["upstash-signature"] || + req.headers["Upstash-Signature"]) as string, + body: JSON.stringify(req.body), + }); + + if (!isValid) { + return res.status(401).json({ error: "Unauthorized" }); + } + + assert(req.body, RelayProcessJobBodySchema); + const { request, strategyName } = req.body; + + // Validate method-specific request body + const methodNameAndArgs = validateMethodArgs( + request.methodNameAndArgs.methodName, + request.methodNameAndArgs.args + ); + + // Verify user signatures + const { signatures, chainId, to } = request; + await verifySignatures({ + methodNameAndArgs, + signatures, + chainId, + to, + }); + + const strategy = strategiesByName[strategyName as RelayStrategyName]; + const relayRequest: RelayRequest = { + chainId, + to, + methodNameAndArgs, + signatures, + }; + + // Get cached request + const cachedRequest = await getCachedRelayRequest(relayRequest); + + if ( + !cachedRequest || + cachedRequest.status === "unknown" || + cachedRequest.status === "success" + ) { + throw new InvalidParamError({ + param: "request", + message: "Request not found in cache or succeeded already", + }); + } + + const { messageId } = cachedRequest; + + // Handle request via strategy + try { + const txHash = await strategy.relay(relayRequest); + // Store requestId in database + await setCachedRelayRequestSuccess({ + request: relayRequest, + txHash, + }); + return res.status(200).json({ + messageId, + txHash, + }); + } catch (error) { + await setCachedRelayRequestFailure({ + request: relayRequest, + error: error as Error, + }); + throw error; + } + } catch (error) { + return handleErrorCondition("relay/jobs/process", res, logger, error); + } +} diff --git a/api/relay/status.ts b/api/relay/status.ts new file mode 100644 index 000000000..0a029adf4 --- /dev/null +++ b/api/relay/status.ts @@ -0,0 +1,45 @@ +import { VercelResponse } from "@vercel/node"; +import { assert, type, Infer } from "superstruct"; + +import { handleErrorCondition, InputError } from "../_errors"; +import { getLogger, hexString } from "../_utils"; +import { getCachedRelayRequest } from "./_utils"; +import { TypedVercelRequest } from "../_types"; + +const RelayRequestStatusSchema = type({ + requestHash: hexString(), +}); + +type RelayRequestStatusType = Infer; + +export default async function handler( + request: TypedVercelRequest, + response: VercelResponse +) { + const logger = getLogger(); + logger.debug({ + at: "Relay/status", + message: "Request params", + params: request.query, + }); + + try { + assert(request.query, RelayRequestStatusSchema); + + const cachedRelayRequest = await getCachedRelayRequest( + request.query.requestHash + ); + + if (cachedRelayRequest.status === "unknown") { + throw new InputError({ + message: `Request with hash ${request.query.requestHash} is unknown`, + code: "INVALID_PARAM", + param: "requestHash", + }); + } + + response.status(200).json(cachedRelayRequest); + } catch (error) { + return handleErrorCondition("api/relay/status", response, logger, error); + } +} diff --git a/api/swap/approval/_utils.ts b/api/swap/approval/_utils.ts index 15f0715f7..25af6c5e7 100644 --- a/api/swap/approval/_utils.ts +++ b/api/swap/approval/_utils.ts @@ -84,7 +84,10 @@ export async function buildCrossSwapTxForAllowanceHolder( const { baseDepositData } = await extractDepositDataStruct(crossSwapQuotes); - if (depositEntryPoint.name === "SpokePoolPeriphery") { + if ( + depositEntryPoint.name === "SpokePoolPeriphery" && + crossSwapQuotes.crossSwap.isInputNative + ) { const spokePoolPeriphery = getSpokePoolPeriphery( depositEntryPoint.address, originChainId @@ -92,7 +95,6 @@ export async function buildCrossSwapTxForAllowanceHolder( tx = await spokePoolPeriphery.populateTransaction.deposit( baseDepositData.recipient, baseDepositData.inputToken, - // baseDepositData.outputToken, // TODO: allow for output token in periphery contract baseDepositData.inputAmount, baseDepositData.outputAmount, baseDepositData.destinationChainId, @@ -102,13 +104,15 @@ export async function buildCrossSwapTxForAllowanceHolder( baseDepositData.exclusivityDeadline, baseDepositData.message, { - value: crossSwapQuotes.crossSwap.isInputNative - ? baseDepositData.inputAmount - : 0, + value: baseDepositData.inputAmount, } ); toAddress = spokePoolPeriphery.address; - } else if (depositEntryPoint.name === "SpokePool") { + } else if ( + depositEntryPoint.name === "SpokePool" || + (depositEntryPoint.name === "SpokePoolPeriphery" && + !crossSwapQuotes.crossSwap.isInputNative) + ) { const spokePool = getSpokePool(originChainId); tx = await spokePool.populateTransaction.depositV3( baseDepositData.depositor, diff --git a/api/swap/permit/_utils.ts b/api/swap/permit/_utils.ts index d1bb441e1..b6136691b 100644 --- a/api/swap/permit/_utils.ts +++ b/api/swap/permit/_utils.ts @@ -15,7 +15,6 @@ import { extractSwapAndDepositDataStruct, } from "../../_dexes/utils"; import { SpokePoolV3PeripheryInterface } from "../../_typechain/SpokePoolV3Periphery"; -import { stringifyBigNumProps } from "../_utils"; export async function buildPermitTxPayload({ crossSwapQuotes, @@ -137,22 +136,15 @@ export async function buildPermitTxPayload({ ]); return { eip712: { - permit: { - ...permitTypedData.eip712, - message: stringifyBigNumProps(permitTypedData.eip712.message), - }, - deposit: { - ...depositTypedData.eip712, - message: stringifyBigNumProps(depositTypedData.eip712.message), - }, + permit: permitTypedData.eip712, + deposit: depositTypedData.eip712, }, swapTx: { chainId: originChainId, to: entryPointContract.address, methodName: methodNameAndArgsWithoutSignatures.methodName, - argsWithoutSignatures: stringifyBigNumProps( - methodNameAndArgsWithoutSignatures.argsWithoutSignatures - ), + argsWithoutSignatures: + methodNameAndArgsWithoutSignatures.argsWithoutSignatures, }, }; } diff --git a/api/swap/permit/index.ts b/api/swap/permit/index.ts index 3738d340d..579c4106f 100644 --- a/api/swap/permit/index.ts +++ b/api/swap/permit/index.ts @@ -4,7 +4,11 @@ import { assert, Infer, optional, type } from "superstruct"; import { TypedVercelRequest } from "../../_types"; import { getLogger, handleErrorCondition, positiveIntStr } from "../../_utils"; import { getCrossSwapQuotes } from "../../_dexes/cross-swap-service"; -import { handleBaseSwapQueryParams, BaseSwapQueryParams } from "../_utils"; +import { + handleBaseSwapQueryParams, + BaseSwapQueryParams, + stringifyBigNumProps, +} from "../_utils"; import { getSwapRouter02Strategy } from "../../_dexes/uniswap/swap-router-02"; import { InvalidParamError } from "../../_errors"; import { buildPermitTxPayload } from "./_utils"; @@ -96,7 +100,7 @@ const handler = async ( }, }); - const responseJson = crossSwapTxForPermit; + const responseJson = stringifyBigNumProps(crossSwapTxForPermit); logger.debug({ at: "Swap/permit", diff --git a/package.json b/package.json index b0afbcc24..52ba8024c 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@uniswap/sdk-core": "^5.9.0", "@uniswap/smart-order-router": "^4.7.8", "@uniswap/v3-sdk": "^3.18.1", + "@upstash/qstash": "^2.7.20", "@vercel/kv": "^2.0.0", "@web3-onboard/coinbase": "^2.4.1", "@web3-onboard/core": "^2.21.2", diff --git a/scripts/generate-routes.ts b/scripts/generate-routes.ts index d60be8cec..c90ae91b0 100644 --- a/scripts/generate-routes.ts +++ b/scripts/generate-routes.ts @@ -126,26 +126,26 @@ const enabledRoutes = { }, }, spokePoolPeripheryAddresses: { - [CHAIN_IDs.ARBITRUM]: "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", - [CHAIN_IDs.BASE]: "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", - [CHAIN_IDs.BLAST]: "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", - [CHAIN_IDs.MAINNET]: "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", - [CHAIN_IDs.OPTIMISM]: "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", - [CHAIN_IDs.POLYGON]: "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", - [CHAIN_IDs.WORLD_CHAIN]: "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", - [CHAIN_IDs.ZK_SYNC]: "0xB007dFe9A6b70e1AB5BD5E97C22e47C5e0c0B8D8", - [CHAIN_IDs.ZORA]: "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", + [CHAIN_IDs.ARBITRUM]: "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", + [CHAIN_IDs.BASE]: "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", + [CHAIN_IDs.BLAST]: "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", + [CHAIN_IDs.MAINNET]: "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", + [CHAIN_IDs.OPTIMISM]: "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", + [CHAIN_IDs.POLYGON]: "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", + [CHAIN_IDs.WORLD_CHAIN]: "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", + [CHAIN_IDs.ZK_SYNC]: "0xA081fbEafed82313d8C656ac6BA6cdd8198C8FaA", + [CHAIN_IDs.ZORA]: "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", }, spokePoolPeripheryProxyAddresses: { - [CHAIN_IDs.ARBITRUM]: "0xAa074de443aee6725EA5aC45FF2add1dC8366485", - [CHAIN_IDs.BASE]: "0xAa074de443aee6725EA5aC45FF2add1dC8366485", - [CHAIN_IDs.BLAST]: "0xAa074de443aee6725EA5aC45FF2add1dC8366485", - [CHAIN_IDs.MAINNET]: "0xAa074de443aee6725EA5aC45FF2add1dC8366485", - [CHAIN_IDs.OPTIMISM]: "0xAa074de443aee6725EA5aC45FF2add1dC8366485", - [CHAIN_IDs.POLYGON]: "0xAa074de443aee6725EA5aC45FF2add1dC8366485", - [CHAIN_IDs.WORLD_CHAIN]: "0xAa074de443aee6725EA5aC45FF2add1dC8366485", - [CHAIN_IDs.ZK_SYNC]: "0xB007dFe9A6b70e1AB5BD5E97C22e47C5e0c0B8D8", - [CHAIN_IDs.ZORA]: "0xAa074de443aee6725EA5aC45FF2add1dC8366485", + [CHAIN_IDs.ARBITRUM]: "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", + [CHAIN_IDs.BASE]: "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", + [CHAIN_IDs.BLAST]: "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", + [CHAIN_IDs.MAINNET]: "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", + [CHAIN_IDs.OPTIMISM]: "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", + [CHAIN_IDs.POLYGON]: "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", + [CHAIN_IDs.WORLD_CHAIN]: "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", + [CHAIN_IDs.ZK_SYNC]: "0x2604C295565e4cd82CB8aa07c3ab5ed611E34f0E", + [CHAIN_IDs.ZORA]: "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", }, routes: transformChainConfigs(enabledMainnetChainConfigs), }, diff --git a/scripts/tests/swap-permit.ts b/scripts/tests/swap-permit.ts index ecc9a9ea3..ab052e48e 100644 --- a/scripts/tests/swap-permit.ts +++ b/scripts/tests/swap-permit.ts @@ -13,12 +13,6 @@ async function swapWithPermit() { 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, @@ -40,6 +34,20 @@ async function swapWithPermit() { 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)); + } } } diff --git a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json index a4e5abbfc..174e15542 100644 --- a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json +++ b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json @@ -39,26 +39,26 @@ } }, "spokePoolPeripheryAddresses": { - "1": "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", - "10": "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", - "137": "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", - "324": "0xB007dFe9A6b70e1AB5BD5E97C22e47C5e0c0B8D8", - "480": "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", - "8453": "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", - "42161": "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", - "81457": "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a", - "7777777": "0xF5E3419d9107dE8da3E16ccb5e305686FAC2Cc1a" + "1": "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", + "10": "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", + "137": "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", + "324": "0xA081fbEafed82313d8C656ac6BA6cdd8198C8FaA", + "480": "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", + "8453": "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", + "42161": "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", + "81457": "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", + "7777777": "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3" }, "spokePoolPeripheryProxyAddresses": { - "1": "0xAa074de443aee6725EA5aC45FF2add1dC8366485", - "10": "0xAa074de443aee6725EA5aC45FF2add1dC8366485", - "137": "0xAa074de443aee6725EA5aC45FF2add1dC8366485", - "324": "0xB007dFe9A6b70e1AB5BD5E97C22e47C5e0c0B8D8", - "480": "0xAa074de443aee6725EA5aC45FF2add1dC8366485", - "8453": "0xAa074de443aee6725EA5aC45FF2add1dC8366485", - "42161": "0xAa074de443aee6725EA5aC45FF2add1dC8366485", - "81457": "0xAa074de443aee6725EA5aC45FF2add1dC8366485", - "7777777": "0xAa074de443aee6725EA5aC45FF2add1dC8366485" + "1": "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", + "10": "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", + "137": "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", + "324": "0x2604C295565e4cd82CB8aa07c3ab5ed611E34f0E", + "480": "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", + "8453": "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", + "42161": "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", + "81457": "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", + "7777777": "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8" }, "routes": [ { diff --git a/vercel.json b/vercel.json index 00a7f48ec..9ac06f42b 100644 --- a/vercel.json +++ b/vercel.json @@ -20,6 +20,9 @@ }, "api/cron-ping-endpoints.ts": { "maxDuration": 90 + }, + "api/relay/jobs/process.ts": { + "maxDuration": 90 } }, "rewrites": [ diff --git a/yarn.lock b/yarn.lock index 2ce25fdaf..e31430a44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -102,7 +102,6 @@ superstruct "^0.15.4" tslib "^2.6.2" viem "^2.21.15" - viem "^2.21.15" "@adraffy/ens-normalize@1.10.0": version "1.10.0" @@ -119,11 +118,6 @@ resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz#42cc67c5baa407ac25059fcd7d405cc5ecdb0c33" integrity sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg== -"@adraffy/ens-normalize@^1.10.1": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz#42cc67c5baa407ac25059fcd7d405cc5ecdb0c33" - integrity sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg== - "@amplitude/ampli@^1.34.0": version "1.34.0" resolved "https://registry.yarnpkg.com/@amplitude/ampli/-/ampli-1.34.0.tgz#878bdeddc392ff24d707f8137834f9213098dd8e" @@ -1337,13 +1331,6 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@^7.25.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" - integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== - dependencies: - regenerator-runtime "^0.14.0" - "@babel/template@^7.22.15", "@babel/template@^7.22.5", "@babel/template@^7.3.3": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" @@ -1472,40 +1459,6 @@ bn.js "^5.1.2" buffer-layout "^1.2.0" -"@coral-xyz/anchor-errors@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@coral-xyz/anchor-errors/-/anchor-errors-0.30.1.tgz#bdfd3a353131345244546876eb4afc0e125bec30" - integrity sha512-9Mkradf5yS5xiLWrl9WrpjqOrAV+/W2RQHDlbnAZBivoGpOs1ECjoDCkVk4aRG8ZdiFiB8zQEVlxf+8fKkmSfQ== - -"@coral-xyz/anchor@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.30.1.tgz#17f3e9134c28cd0ea83574c6bab4e410bcecec5d" - integrity sha512-gDXFoF5oHgpriXAaLpxyWBHdCs8Awgf/gLHIo6crv7Aqm937CNdY+x+6hoj7QR5vaJV7MxWSQ0NGFzL3kPbWEQ== - dependencies: - "@coral-xyz/anchor-errors" "^0.30.1" - "@coral-xyz/borsh" "^0.30.1" - "@noble/hashes" "^1.3.1" - "@solana/web3.js" "^1.68.0" - bn.js "^5.1.2" - bs58 "^4.0.1" - buffer-layout "^1.2.2" - camelcase "^6.3.0" - cross-fetch "^3.1.5" - crypto-hash "^1.3.0" - eventemitter3 "^4.0.7" - pako "^2.0.3" - snake-case "^3.0.4" - superstruct "^0.15.4" - toml "^3.0.0" - -"@coral-xyz/borsh@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.30.1.tgz#869d8833abe65685c72e9199b8688477a4f6b0e3" - integrity sha512-aaxswpPrCFKl8vZTbxLssA2RvwX2zmKLlRCIktJOwW+VpVwYtXRtlWiIP+c2pPRKneiTiWCN2GEMSH9j1zTlWQ== - dependencies: - bn.js "^5.1.2" - buffer-layout "^1.2.0" - "@dabh/diagnostics@^2.0.2": version "2.0.3" resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" @@ -3914,13 +3867,6 @@ dependencies: "@noble/hashes" "1.5.0" -"@noble/curves@1.6.0", "@noble/curves@^1.4.0", "@noble/curves@^1.4.2", "@noble/curves@^1.6.0", "@noble/curves@~1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.6.0.tgz#be5296ebcd5a1730fccea4786d420f87abfeb40b" - integrity sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ== - dependencies: - "@noble/hashes" "1.5.0" - "@noble/hashes@1.2.0", "@noble/hashes@~1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" @@ -3941,11 +3887,6 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA== -"@noble/hashes@1.5.0", "@noble/hashes@^1.3.1", "@noble/hashes@^1.5.0", "@noble/hashes@~1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" - integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA== - "@noble/hashes@^1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" @@ -4987,11 +4928,6 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1" integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg== -"@scure/base@~1.1.7", "@scure/base@~1.1.8": - version "1.1.9" - resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1" - integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg== - "@scure/bip32@1.1.5": version "1.1.5" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.1.5.tgz#d2ccae16dcc2e75bc1d75f5ef3c66a338d1ba300" @@ -5028,15 +4964,6 @@ "@noble/hashes" "~1.5.0" "@scure/base" "~1.1.7" -"@scure/bip32@1.5.0", "@scure/bip32@^1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.5.0.tgz#dd4a2e1b8a9da60e012e776d954c4186db6328e6" - integrity sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw== - dependencies: - "@noble/curves" "~1.6.0" - "@noble/hashes" "~1.5.0" - "@scure/base" "~1.1.7" - "@scure/bip39@1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.1.tgz#b54557b2e86214319405db819c4b6a370cf340c5" @@ -5069,14 +4996,6 @@ "@noble/hashes" "~1.5.0" "@scure/base" "~1.1.8" -"@scure/bip39@1.4.0", "@scure/bip39@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.4.0.tgz#664d4f851564e2e1d4bffa0339f9546ea55960a6" - integrity sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw== - dependencies: - "@noble/hashes" "~1.5.0" - "@scure/base" "~1.1.8" - "@sentry/browser@7.37.2": version "7.37.2" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.37.2.tgz#355dd28ad12677d63e0b12c5209d12b3f98ac3a4" @@ -6424,13 +6343,6 @@ dependencies: tslib "^2.8.0" -"@swc/helpers@^0.5.11": - version "0.5.15" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7" - integrity sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g== - dependencies: - tslib "^2.8.0" - "@synthetixio/ethereum-wallet-mock@0.0.1-alpha.7": version "0.0.1-alpha.7" resolved "https://registry.yarnpkg.com/@synthetixio/ethereum-wallet-mock/-/ethereum-wallet-mock-0.0.1-alpha.7.tgz#313abfab9176d9da71ec69c40a96fbcd334f6a42" @@ -6918,13 +6830,6 @@ dependencies: "@types/node" "*" -"@types/connect@^3.4.33": - version "3.4.38" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" - integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== - dependencies: - "@types/node" "*" - "@types/cross-spawn@^6.0.2": version "6.0.4" resolved "https://registry.yarnpkg.com/@types/cross-spawn/-/cross-spawn-6.0.4.tgz#e658d29e2308a01f48b7b30fd8cdf07aeb2e5a82" @@ -7231,7 +7136,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== -"@types/node@^12.0.0", "@types/node@^12.12.54", "@types/node@^12.12.6": "@types/node@^12.0.0", "@types/node@^12.12.54", "@types/node@^12.12.6": version "12.20.55" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" @@ -7437,11 +7341,6 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== -"@types/uuid@^8.3.4": - version "8.3.4" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" - integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== - "@types/webpack-env@^1.17.0": version "1.18.0" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.18.0.tgz#ed6ecaa8e5ed5dfe8b2b3d00181702c9925f13fb" @@ -7461,20 +7360,6 @@ dependencies: "@types/node" "*" -"@types/ws@^7.4.4": - version "7.4.7" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" - integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== - dependencies: - "@types/node" "*" - -"@types/ws@^8.2.2": - version "8.5.13" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.13.tgz#6414c280875e2691d0d1e080b05addbf5cb91e20" - integrity sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA== - dependencies: - "@types/node" "*" - "@types/ws@^8.5.5": version "8.5.5" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.5.tgz#af587964aa06682702ee6dcbc7be41a80e4b28eb" @@ -7501,13 +7386,6 @@ dependencies: "@types/yargs-parser" "*" -"@types/yargs@^17.0.33": - version "17.0.33" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.33.tgz#8c32303da83eec050a84b3c7ae7b9f922d13e32d" - integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== - dependencies: - "@types/yargs-parser" "*" - "@types/yargs@^17.0.8": version "17.0.24" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.24.tgz#b3ef8d50ad4aa6aecf6ddc97c580a00f5aa11902" @@ -7984,6 +7862,15 @@ tiny-invariant "^1.1.0" tiny-warning "^1.0.3" +"@upstash/qstash@^2.7.20": + version "2.7.20" + resolved "https://registry.yarnpkg.com/@upstash/qstash/-/qstash-2.7.20.tgz#7027829be8f2087d35ab179ec6c9bd9ee5154a64" + integrity sha512-fdh5df4BuJwSUhXv33g/Eaj7gG+fu3tBoXyGE5hg4zEZX9fwUY/nuc0uDlLWh54L66JfMdESVOjIaiLa1cgL1Q== + dependencies: + crypto-js ">=4.2.0" + jose "^5.2.3" + neverthrow "^7.0.1" + "@upstash/redis@^1.31.3": version "1.34.0" resolved "https://registry.yarnpkg.com/@upstash/redis/-/redis-1.34.0.tgz#f32cd53ebeeafbba7eca10f8597a573d5a2fed0d" @@ -8652,14 +8539,6 @@ JSONStream@^1.3.5: jsonparse "^1.2.0" through ">=2.2.7 <3" -JSONStream@^1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" - integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== - dependencies: - jsonparse "^1.2.0" - through ">=2.2.7 <3" - abab@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" @@ -8697,11 +8576,6 @@ abitype@1.0.6, abitype@^1.0.6: resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.6.tgz#76410903e1d88e34f1362746e2d407513c38565b" integrity sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A== -abitype@1.0.6, abitype@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.6.tgz#76410903e1d88e34f1362746e2d407513c38565b" - integrity sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A== - abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -8820,13 +8694,6 @@ agentkeepalive@^4.5.0: dependencies: humanize-ms "^1.2.1" -agentkeepalive@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" - integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== - dependencies: - humanize-ms "^1.2.1" - aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -10224,13 +10091,6 @@ bigint-buffer@^1.1.5: dependencies: bindings "^1.3.0" -bigint-buffer@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/bigint-buffer/-/bigint-buffer-1.1.5.tgz#d038f31c8e4534c1f8d0015209bf34b4fa6dd442" - integrity sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA== - dependencies: - bindings "^1.3.0" - bignumber.js@*, bignumber.js@^9.0.0, bignumber.js@^9.0.1, bignumber.js@^9.0.2: version "9.1.2" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" @@ -10264,7 +10124,6 @@ binary@~0.3.0: buffers "~0.1.1" chainsaw "~0.1.0" -bindings@^1.3.0, bindings@^1.5.0: bindings@^1.3.0, bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -10390,15 +10249,6 @@ borsh@^0.7.0: bs58 "^4.0.0" text-encoding-utf-8 "^1.0.2" -borsh@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/borsh/-/borsh-0.7.0.tgz#6e9560d719d86d90dc589bca60ffc8a6c51fec2a" - integrity sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA== - dependencies: - bn.js "^5.2.0" - bs58 "^4.0.0" - text-encoding-utf-8 "^1.0.2" - bowser@^2.11.0: version "2.11.0" resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" @@ -10613,13 +10463,6 @@ bs58@^6.0.0: dependencies: base-x "^5.0.0" -bs58@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/bs58/-/bs58-6.0.0.tgz#a2cda0130558535dd281a2f8697df79caaf425d8" - integrity sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw== - dependencies: - base-x "^5.0.0" - bs58check@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" @@ -10684,11 +10527,6 @@ buffer-layout@^1.2.0, buffer-layout@^1.2.2: resolved "https://registry.yarnpkg.com/buffer-layout/-/buffer-layout-1.2.2.tgz#b9814e7c7235783085f9ca4966a0cfff112259d5" integrity sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA== -buffer-layout@^1.2.0, buffer-layout@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/buffer-layout/-/buffer-layout-1.2.2.tgz#b9814e7c7235783085f9ca4966a0cfff112259d5" - integrity sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA== - buffer-reverse@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-reverse/-/buffer-reverse-1.0.1.tgz#49283c8efa6f901bc01fa3304d06027971ae2f60" @@ -10704,7 +10542,6 @@ buffer-xor@^1.0.3: resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ== -buffer@6.0.3, buffer@^6.0.1, buffer@^6.0.3, buffer@~6.0.3: buffer@6.0.3, buffer@^6.0.1, buffer@^6.0.3, buffer@~6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" @@ -10884,7 +10721,6 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -camelcase@^6.0.0, camelcase@^6.2.0, camelcase@^6.3.0: camelcase@^6.0.0, camelcase@^6.2.0, camelcase@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" @@ -11583,12 +11419,6 @@ commander@^12.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== -commander@^2.11.0, commander@^2.15.0, commander@^2.19.0, commander@^2.20.3: -commander@^12.1.0: - version "12.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" - integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== - commander@^2.11.0, commander@^2.15.0, commander@^2.19.0, commander@^2.20.3: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -11877,13 +11707,6 @@ cross-fetch@^3.1.5: dependencies: node-fetch "^2.6.12" -cross-fetch@^3.1.5: - version "3.1.8" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" - integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== - dependencies: - node-fetch "^2.6.12" - cross-fetch@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" @@ -11956,12 +11779,7 @@ crypto-hash@^1.3.0: resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247" integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg== -crypto-hash@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247" - integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg== - -crypto-js@^4.2.0: +crypto-js@>=4.2.0, crypto-js@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== @@ -12289,11 +12107,6 @@ delay@^5.0.0: resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== -delay@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" - integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -12510,14 +12323,6 @@ dot-case@^3.0.4: no-case "^3.0.4" tslib "^2.0.3" -dot-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" - integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - dot-prop@^5.0.0: version "5.3.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" @@ -12977,7 +12782,6 @@ es6-iterator@^2.0.3: es5-ext "^0.10.35" es6-symbol "^3.1.1" -es6-promise@4.2.8, es6-promise@^4.0.3, es6-promise@^4.2.8: es6-promise@4.2.8, es6-promise@^4.0.3, es6-promise@^4.2.8: version "4.2.8" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" @@ -12990,13 +12794,6 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" -es6-promisify@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" - integrity sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ== - dependencies: - es6-promise "^4.0.3" - es6-symbol@^3.1.1, es6-symbol@^3.1.3: version "3.1.4" resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.4.tgz#f4e7d28013770b4208ecbf3e0bf14d3bcb557b8c" @@ -13978,11 +13775,6 @@ eventemitter3@5.0.1, eventemitter3@^5.0.1: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== -eventemitter3@5.0.1, eventemitter3@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" - integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== - eventemitter3@^4.0.0, eventemitter3@^4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -14215,11 +14007,6 @@ eyes@^0.1.8: resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ== -eyes@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" - integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ== - fake-merkle-patricia-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/fake-merkle-patricia-tree/-/fake-merkle-patricia-tree-1.0.1.tgz#4b8c3acfb520afadf9860b1f14cd8ce3402cddd3" @@ -14280,11 +14067,6 @@ fast-stable-stringify@^1.0.0: resolved "https://registry.yarnpkg.com/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz#5c5543462b22aeeefd36d05b34e51c78cb86d313" integrity sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag== -fast-stable-stringify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz#5c5543462b22aeeefd36d05b34e51c78cb86d313" - integrity sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag== - fast-text-encoding@^1.0.0, fast-text-encoding@^1.0.3: version "1.0.6" resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" @@ -15924,13 +15706,6 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -humanize-ms@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" - integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== - dependencies: - ms "^2.0.0" - husky@^8.0.0: version "8.0.1" resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.1.tgz#511cb3e57de3e3190514ae49ed50f6bc3f50b3e9" @@ -16893,11 +16668,6 @@ isomorphic-ws@^4.0.1: resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== -isomorphic-ws@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" - integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== - isows@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.3.tgz#93c1cf0575daf56e7120bab5c8c448b0809d0d74" @@ -16913,11 +16683,6 @@ isows@1.0.6: resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.6.tgz#0da29d706fa51551c663c627ace42769850f86e7" integrity sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw== -isows@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.6.tgz#0da29d706fa51551c663c627ace42769850f86e7" - integrity sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw== - isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -17100,24 +16865,6 @@ jayson@^4.1.1: uuid "^8.3.2" ws "^7.5.10" -jayson@^4.1.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/jayson/-/jayson-4.1.2.tgz#443c26a8658703e0b2e881117b09395d88b6982e" - integrity sha512-5nzMWDHy6f+koZOuYsArh2AXs73NfWYVlFyJJuCedr93GpY+Ku8qq10ropSXVfHK+H0T6paA88ww+/dV+1fBNA== - dependencies: - "@types/connect" "^3.4.33" - "@types/node" "^12.12.54" - "@types/ws" "^7.4.4" - JSONStream "^1.3.5" - commander "^2.20.3" - delay "^5.0.0" - es6-promisify "^5.0.0" - eyes "^0.1.8" - isomorphic-ws "^4.0.1" - json-stringify-safe "^5.0.1" - uuid "^8.3.2" - ws "^7.5.10" - jest-changed-files@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.5.0.tgz#e88786dca8bf2aa899ec4af7644e16d9dcf9b23e" @@ -17536,11 +17283,6 @@ jinx-rust@0.1.6: resolved "https://registry.yarnpkg.com/jinx-rust/-/jinx-rust-0.1.6.tgz#c7bce55d97bfbad76a9b930c01fe6a8629a170d7" integrity sha512-qP+wtQL1PrDDFwtPKhNGtjWOmijCrKdfUHWTV2G/ikxfjrh+cjdvkQTmny9RAsVF0jiui9m+F0INWu4cuRcZeQ== -jinx-rust@0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/jinx-rust/-/jinx-rust-0.1.6.tgz#c7bce55d97bfbad76a9b930c01fe6a8629a170d7" - integrity sha512-qP+wtQL1PrDDFwtPKhNGtjWOmijCrKdfUHWTV2G/ikxfjrh+cjdvkQTmny9RAsVF0jiui9m+F0INWu4cuRcZeQ== - joi@17.9.1: version "17.9.1" resolved "https://registry.yarnpkg.com/joi/-/joi-17.9.1.tgz#74899b9fa3646904afa984a11df648eca66c9018" @@ -17557,6 +17299,11 @@ jose@^4.9.3: resolved "https://registry.yarnpkg.com/jose/-/jose-4.9.3.tgz#890abd3f26725fe0f2aa720bc2f7835702b624db" integrity sha512-f8E/z+T3Q0kA9txzH2DKvH/ds2uggcw0m3vVPSB9HrSkrQ7mojjifvS7aR8cw+lQl2Fcmx9npwaHpM/M3GD8UQ== +jose@^5.2.3: + version "5.9.6" + resolved "https://registry.yarnpkg.com/jose/-/jose-5.9.6.tgz#77f1f901d88ebdc405e57cce08d2a91f47521883" + integrity sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ== + joycon@^3.0.1: version "3.1.1" resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" @@ -17808,7 +17555,6 @@ json-stable-stringify@^1.0.1: jsonify "^0.0.1" object-keys "^1.1.1" -json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -17876,11 +17622,6 @@ jsonparse@^1.2.0: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== -jsonparse@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" - integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== - jsonschema@^1.2.4: version "1.4.1" resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.4.1.tgz#cc4c3f0077fb4542982973d8a083b6b34f482dab" @@ -18514,13 +18255,6 @@ lower-case@^2.0.2: dependencies: tslib "^2.0.3" -lower-case@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" - integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== - dependencies: - tslib "^2.0.3" - lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" @@ -19161,7 +18895,6 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.0.0, ms@^2.1.1: ms@2.1.3, ms@^2.0.0, ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" @@ -19438,6 +19171,11 @@ neo-async@^2.5.0, neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +neverthrow@^7.0.1: + version "7.2.0" + resolved "https://registry.yarnpkg.com/neverthrow/-/neverthrow-7.2.0.tgz#76fa0a6cf1f6d59f0770df461c92b8b270910694" + integrity sha512-iGBUfFB7yPczHHtA8dksKTJ9E8TESNTAx1UQWW6TzMF280vo9jdPYpLUXrMN1BCkPdHFdNG3fxOt2CUad8KhAw== + next-tick@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" @@ -19463,14 +19201,6 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -no-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" - integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== - dependencies: - lower-case "^2.0.2" - tslib "^2.0.3" - node-addon-api@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" @@ -19522,7 +19252,6 @@ node-fetch@^1.0.1, node-fetch@~1.7.1: encoding "^0.1.11" is-stream "^1.0.1" -node-fetch@^2.0.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7, node-fetch@^2.6.9, node-fetch@^2.7.0: node-fetch@^2.0.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7, node-fetch@^2.6.9, node-fetch@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" @@ -19957,19 +19686,6 @@ ox@0.1.2: abitype "^1.0.6" eventemitter3 "5.0.1" -ox@0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ox/-/ox-0.1.2.tgz#0f791be2ccabeaf4928e6d423498fe1c8094e560" - integrity sha512-ak/8K0Rtphg9vnRJlbOdaX9R7cmxD2MiSthjWGaQdMk3D7hrAlDoM+6Lxn7hN52Za3vrXfZ7enfke/5WjolDww== - dependencies: - "@adraffy/ens-normalize" "^1.10.1" - "@noble/curves" "^1.6.0" - "@noble/hashes" "^1.5.0" - "@scure/bip32" "^1.5.0" - "@scure/bip39" "^1.4.0" - abitype "^1.0.6" - eventemitter3 "5.0.1" - p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" @@ -20074,11 +19790,6 @@ pako@^2.0.3: resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== -pako@^2.0.3: - version "2.1.0" - resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" - integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== - pako@~0.2.0: version "0.2.9" resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" @@ -20674,20 +20385,11 @@ prettier-plugin-rust@^0.1.9: jinx-rust "0.1.6" prettier "^2.7.1" -prettier-plugin-rust@^0.1.9: - version "0.1.9" - resolved "https://registry.yarnpkg.com/prettier-plugin-rust/-/prettier-plugin-rust-0.1.9.tgz#1a93b035743fa02a006b4980a1035a260ea9e501" - integrity sha512-n1DTTJQaHMdnoG/+nKUvBm3EKsMVWsYES2UPCiOPiZdBrmuAO/pX++m7L3+Hz3uuhtddpH0HRKHB2F3jbtJBOQ== - dependencies: - jinx-rust "0.1.6" - prettier "^2.7.1" - prettier@3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a" integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA== -prettier@^2.7.1, prettier@^2.8.0: prettier@^2.7.1, prettier@^2.8.0: version "2.8.8" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" @@ -22095,22 +21797,6 @@ rpc-websockets@^9.0.2: bufferutil "^4.0.1" utf-8-validate "^5.0.2" -rpc-websockets@^9.0.2: - version "9.0.4" - resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-9.0.4.tgz#9d8ee82533b5d1e13d9ded729e3e38d0d8fa083f" - integrity sha512-yWZWN0M+bivtoNLnaDbtny4XchdAIF5Q4g/ZsC5UC61Ckbp0QczwO8fg44rV3uYmY4WHd+EZQbn90W1d8ojzqQ== - dependencies: - "@swc/helpers" "^0.5.11" - "@types/uuid" "^8.3.4" - "@types/ws" "^8.2.2" - buffer "^6.0.3" - eventemitter3 "^5.0.1" - uuid "^8.3.2" - ws "^8.5.0" - optionalDependencies: - bufferutil "^4.0.1" - utf-8-validate "^5.0.2" - run-async@^2.4.0, run-async@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" @@ -22667,14 +22353,6 @@ snake-case@^3.0.4: dot-case "^3.0.4" tslib "^2.0.3" -snake-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" - integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== - dependencies: - dot-case "^3.0.4" - tslib "^2.0.3" - snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -23088,7 +22766,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -23123,6 +22801,15 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -23197,7 +22884,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -23225,6 +22912,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -23340,11 +23034,6 @@ superstruct@^2.0.2: resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-2.0.2.tgz#3f6d32fbdc11c357deff127d591a39b996300c54" integrity sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A== -superstruct@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-2.0.2.tgz#3f6d32fbdc11c357deff127d591a39b996300c54" - integrity sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A== - supports-color@8.1.1, supports-color@^8.0.0, supports-color@^8.1.0: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" @@ -23596,11 +23285,6 @@ text-encoding-utf-8@^1.0.2: resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== -text-encoding-utf-8@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" - integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== - text-hex@1.0.x: version "1.0.0" resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" @@ -23657,7 +23341,6 @@ through2@^2.0.3: readable-stream "~2.3.6" xtend "~4.0.1" -"through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8: "through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -23818,11 +23501,6 @@ toml@^3.0.0: resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== -toml@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" - integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== - tough-cookie@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2" @@ -24000,11 +23678,6 @@ tslib@^2.8.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== -tslib@^2.8.0: - version "2.8.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" - integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== - tsort@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/tsort/-/tsort-0.0.1.tgz#e2280f5e817f8bf4275657fd0f9aebd44f5a2786" @@ -25722,14 +25395,6 @@ webauthn-p256@0.0.10: "@noble/curves" "^1.4.0" "@noble/hashes" "^1.4.0" -webauthn-p256@0.0.10: - version "0.0.10" - resolved "https://registry.yarnpkg.com/webauthn-p256/-/webauthn-p256-0.0.10.tgz#877e75abe8348d3e14485932968edf3325fd2fdd" - integrity sha512-EeYD+gmIT80YkSIDb2iWq0lq2zbHo1CxHlQTeJ+KkCILWpVy3zASH3ByD4bopzfk0uCwXxLqKGLqp2W4O28VFA== - dependencies: - "@noble/curves" "^1.4.0" - "@noble/hashes" "^1.4.0" - webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -25960,7 +25625,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -25995,6 +25660,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -26056,11 +25730,6 @@ ws@8.18.0, ws@^8.5.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== -ws@8.18.0, ws@^8.5.0: - version "8.18.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" - integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== - ws@^3.0.0: version "3.3.3" resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" @@ -26094,11 +25763,6 @@ ws@^7.5.10: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== -ws@^7.5.10: - version "7.5.10" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" - integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== - ws@^8.11.0, ws@^8.13.0, ws@^8.2.3: version "8.17.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea" From 05d05f855ec32f704c4f7a0fe4e7f10d59e7ba7e Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Mon, 30 Dec 2024 13:19:25 +0100 Subject: [PATCH 39/47] fix: update zksync periphery addresses --- scripts/generate-routes.ts | 4 ++-- .../routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/generate-routes.ts b/scripts/generate-routes.ts index c90ae91b0..27036a365 100644 --- a/scripts/generate-routes.ts +++ b/scripts/generate-routes.ts @@ -133,7 +133,7 @@ const enabledRoutes = { [CHAIN_IDs.OPTIMISM]: "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", [CHAIN_IDs.POLYGON]: "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", [CHAIN_IDs.WORLD_CHAIN]: "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", - [CHAIN_IDs.ZK_SYNC]: "0xA081fbEafed82313d8C656ac6BA6cdd8198C8FaA", + [CHAIN_IDs.ZK_SYNC]: "0xDFD7f7AC8F2331C4E83A43E73aB7579e736AC1Bf", [CHAIN_IDs.ZORA]: "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", }, spokePoolPeripheryProxyAddresses: { @@ -144,7 +144,7 @@ const enabledRoutes = { [CHAIN_IDs.OPTIMISM]: "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", [CHAIN_IDs.POLYGON]: "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", [CHAIN_IDs.WORLD_CHAIN]: "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", - [CHAIN_IDs.ZK_SYNC]: "0x2604C295565e4cd82CB8aa07c3ab5ed611E34f0E", + [CHAIN_IDs.ZK_SYNC]: "0x793Ff9Cd09819C537500dFcEB6F61861c1B80dCD", [CHAIN_IDs.ZORA]: "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", }, routes: transformChainConfigs(enabledMainnetChainConfigs), diff --git a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json index 174e15542..65db5d3c8 100644 --- a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json +++ b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json @@ -42,7 +42,7 @@ "1": "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", "10": "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", "137": "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", - "324": "0xA081fbEafed82313d8C656ac6BA6cdd8198C8FaA", + "324": "0xDFD7f7AC8F2331C4E83A43E73aB7579e736AC1Bf", "480": "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", "8453": "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", "42161": "0xED7Bf315Ba2E9Db86b766b8AaC48502298dfe7d3", @@ -53,7 +53,7 @@ "1": "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", "10": "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", "137": "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", - "324": "0x2604C295565e4cd82CB8aa07c3ab5ed611E34f0E", + "324": "0x793Ff9Cd09819C537500dFcEB6F61861c1B80dCD", "480": "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", "8453": "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", "42161": "0x2d5E44b66bD40267fb816c9537E026545bEbbAC8", From 1d1399cdb50e7481e77fa41727157c05b976fb69 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Fri, 3 Jan 2025 10:05:03 +0100 Subject: [PATCH 40/47] fix: stringifyBigNumProps --- api/relay/index.ts | 2 +- api/swap/_utils.ts | 28 ++++++++++++++++++++-------- scripts/tests/_swap-utils.ts | 2 +- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/api/relay/index.ts b/api/relay/index.ts index 1bd44d14f..4a1d98105 100644 --- a/api/relay/index.ts +++ b/api/relay/index.ts @@ -28,7 +28,7 @@ export const BaseRelayRequestBodySchema = object({ const strategies = { default: strategiesByName.gelato, - [CHAIN_IDs.ARBITRUM]: strategiesByName["local-signers"], + [CHAIN_IDs.WORLD_CHAIN]: strategiesByName["local-signers"], }; export default async function handler( diff --git a/api/swap/_utils.ts b/api/swap/_utils.ts index bc5eda0ba..3b42f41f0 100644 --- a/api/swap/_utils.ts +++ b/api/swap/_utils.ts @@ -237,16 +237,28 @@ export async function calculateCrossSwapFees( }; } -export function stringifyBigNumProps(value: object): object { +export function stringifyBigNumProps(value: T): T { + if (Array.isArray(value)) { + return value.map((element) => { + if (element instanceof BigNumber) { + return element.toString(); + } + if (typeof element === "object" && element !== null) { + return stringifyBigNumProps(element); + } + return element; + }) as T; + } + return Object.fromEntries( - Object.entries(value).map(([key, value]) => { - if (value instanceof BigNumber) { - return [key, value.toString()]; + Object.entries(value).map(([key, val]) => { + if (val instanceof BigNumber) { + return [key, val.toString()]; } - if (typeof value === "object" && value !== null) { - return [key, stringifyBigNumProps(value)]; + if (typeof val === "object" && val !== null) { + return [key, stringifyBigNumProps(val)]; } - return [key, value]; + return [key, val]; }) - ); + ) as T; } diff --git a/scripts/tests/_swap-utils.ts b/scripts/tests/_swap-utils.ts index d0b776c92..a5598bfb8 100644 --- a/scripts/tests/_swap-utils.ts +++ b/scripts/tests/_swap-utils.ts @@ -179,7 +179,7 @@ export async function fetchSwapQuote(slug: "approval" | "permit") { const response = await axios.get(`${SWAP_API_BASE_URL}/api/swap/${slug}`, { params: testCase.params, }); - console.log(response.data); + console.log(JSON.stringify(response.data, null, 2)); return response.data; } } From 463573b3fc1e4daf5aad1bd0d986a89f2f1e7878 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Fri, 3 Jan 2025 18:33:55 +0100 Subject: [PATCH 41/47] fix: gelato status error --- api/relay/_strategies/gelato.ts | 62 ++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/api/relay/_strategies/gelato.ts b/api/relay/_strategies/gelato.ts index bbeca011f..9fbf20ca4 100644 --- a/api/relay/_strategies/gelato.ts +++ b/api/relay/_strategies/gelato.ts @@ -27,9 +27,7 @@ export function getGelatoStrategy(): RelayStrategy { taskStatus.taskState ) ) { - throw new Error( - `Can not relay request via Gelato due to task state ${taskStatus.taskState}` - ); + throw new GelatoTaskStatusError(taskStatus); } if (taskStatus.transactionHash) { @@ -73,20 +71,50 @@ async function relayWithGelatoApi({ return response.data.taskId as string; } +type TaskStatus = { + taskState: + | "CheckPending" + | "ExecPending" + | "ExecSuccess" + | "ExecReverted" + | "WaitingForConfirmation" + | "Blacklisted" + | "Cancelled" + | "NotFound"; + chainId: number; + taskId: string; + creationDate: string; + lastCheckDate?: string; + lastCheckMessage?: string; + transactionHash?: string; + blockNumber?: number; + executionDate?: string; + gasUsed?: string; + effectiveGasPrice?: string; +}; + async function getGelatoTaskStatus(taskId: string) { - const response = await axios.get<{ - task: { - taskState: - | "CheckPending" - | "ExecPending" - | "ExecSuccess" - | "ExecReverted" - | "WaitingForConfirmation" - | "Blacklisted" - | "Cancelled" - | "NotFound"; - transactionHash?: string; - }; - }>(`${gelatoBaseUrl}/tasks/status/${taskId}`); + const response = await axios.get<{ task: TaskStatus }>( + `${gelatoBaseUrl}/tasks/status/${taskId}` + ); return response.data.task; } + +class GelatoTaskStatusError extends Error { + taskStatus: TaskStatus; + + constructor(taskStatus: TaskStatus) { + super( + `Can not relay request via Gelato due to task state ${taskStatus.taskState}` + ); + this.taskStatus = taskStatus; + } + + toJSON() { + return { + name: this.name, + message: this.message, + taskStatus: this.taskStatus, + }; + } +} From 87a0f449a67cc3ca1b43f7c1cd625983bfddd246 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp <51655063+gsteenkamp89@users.noreply.github.com> Date: Mon, 6 Jan 2025 11:16:05 +0200 Subject: [PATCH 42/47] support transfer with auth (#1341) --- api/_abis.ts | 30 +++++ api/_spoke-pool-periphery.ts | 46 +++++++ api/_transfer-with-auth.ts | 124 +++++++++++++++++++ api/_utils.ts | 13 +- api/relay/_types.ts | 2 +- api/relay/_utils.ts | 224 +++++++++++++++++++++++++++++------ api/swap/auth/_utils.ts | 167 ++++++++++++++++++++++++++ api/swap/auth/index.ts | 122 +++++++++++++++++++ scripts/tests/_swap-utils.ts | 4 +- scripts/tests/swap-auth.ts | 69 +++++++++++ scripts/tests/swap-permit.ts | 10 +- 11 files changed, 765 insertions(+), 46 deletions(-) create mode 100644 api/_transfer-with-auth.ts create mode 100644 api/swap/auth/_utils.ts create mode 100644 api/swap/auth/index.ts create mode 100644 scripts/tests/swap-auth.ts diff --git a/api/_abis.ts b/api/_abis.ts index 3dd4fc41f..8fe7a37ff 100644 --- a/api/_abis.ts +++ b/api/_abis.ts @@ -126,3 +126,33 @@ export const ERC20_PERMIT_ABI = [ type: "function", }, ]; + +export const ERC_TRANSFER_WITH_AUTH_ABI = [ + { + inputs: [], + stateMutability: "view", + type: "function", + name: "name", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + }, + { + inputs: [], + name: "version", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "DOMAIN_SEPARATOR", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, +]; diff --git a/api/_spoke-pool-periphery.ts b/api/_spoke-pool-periphery.ts index 38c7605ba..14807851f 100644 --- a/api/_spoke-pool-periphery.ts +++ b/api/_spoke-pool-periphery.ts @@ -260,3 +260,49 @@ export function encodeSwapAndBridgeWithPermitCalldata(args: { ] ); } + +export function encodeDepositWithAuthCalldata(args: { + signatureOwner: string; + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct; + validAfter: number; + validBefore: number; + nonce: string; + receiveWithAuthSignature: string; + depositDataSignature: string; +}) { + return SpokePoolV3Periphery__factory.createInterface().encodeFunctionData( + "depositWithAuthorization", + [ + args.signatureOwner, + args.depositData, + args.validAfter, + args.validBefore, + args.nonce, + args.receiveWithAuthSignature, + args.depositDataSignature, + ] + ); +} + +export function encodeSwapAndBridgeWithAuthCalldata(args: { + signatureOwner: string; + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct; + validAfter: number; + validBefore: number; + nonce: string; + receiveWithAuthSignature: string; + swapAndDepositDataSignature: string; +}) { + return SpokePoolV3Periphery__factory.createInterface().encodeFunctionData( + "swapAndBridgeWithAuthorization", + [ + args.signatureOwner, + args.swapAndDepositData, + args.validAfter, + args.validBefore, + args.nonce, + args.receiveWithAuthSignature, + args.swapAndDepositDataSignature, + ] + ); +} diff --git a/api/_transfer-with-auth.ts b/api/_transfer-with-auth.ts new file mode 100644 index 000000000..a14a3b6d2 --- /dev/null +++ b/api/_transfer-with-auth.ts @@ -0,0 +1,124 @@ +import { BigNumberish, ethers } from "ethers"; +import { getProvider } from "./_utils"; +import { ERC_TRANSFER_WITH_AUTH_ABI } from "./_abis"; +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, + ] + ) + ); +} + +export async function getReceiveWithAuthTypedData(params: { + tokenAddress: string; + chainId: number; + ownerAddress: string; + spenderAddress: string; + value: BigNumberish; + validBefore: number; + nonce: string; + validAfter?: number; + eip712DomainVersion?: number; +}) { + const provider = getProvider(params.chainId); + + const erc20Permit = new ethers.Contract( + params.tokenAddress, + ERC_TRANSFER_WITH_AUTH_ABI, + provider + ); + + const [nameResult, versionFromContractResult, domainSeparatorResult] = + await Promise.allSettled([ + erc20Permit.name(), + erc20Permit.version(), + erc20Permit.DOMAIN_SEPARATOR(), + ]); + + if ( + nameResult.status === "rejected" || + domainSeparatorResult.status === "rejected" + ) { + const error = + nameResult.status === "rejected" + ? nameResult.reason + : domainSeparatorResult.status === "rejected" + ? domainSeparatorResult.reason + : new Error("Unknown error"); + throw new Error( + `Contract ${params.tokenAddress} does not support transfer with authorization`, + { + cause: error, + } + ); + } + + const name = nameResult.value; + const versionFromContract = + versionFromContractResult.status === "fulfilled" + ? versionFromContractResult.value + : undefined; + const domainSeparator = domainSeparatorResult.value; + + const eip712DomainVersion = [1, 2, "1", "2"].includes(versionFromContract) + ? Number(versionFromContract) + : params.eip712DomainVersion || 1; + + const domainSeparatorHash = hashDomainSeparator({ + name, + version: eip712DomainVersion, + chainId: params.chainId, + verifyingContract: params.tokenAddress, + }); + + if (domainSeparator !== domainSeparatorHash) { + throw new Error("EIP712 domain separator mismatch"); + } + + return { + domainSeparator, + eip712: { + types: { + ReceiveWithAuthorization: [ + { name: "from", type: "address" }, + { name: "to", type: "address" }, + { name: "value", type: "uint256" }, + { name: "validAfter", type: "uint256" }, + { name: "validBefore", type: "uint256" }, + { name: "nonce", type: "bytes32" }, + ], + }, + domain: { + name, + version: eip712DomainVersion.toString(), + chainId: params.chainId, + verifyingContract: params.tokenAddress, + }, + primaryType: "ReceiveWithAuthorization", + message: { + from: params.ownerAddress, + to: params.spenderAddress, + value: String(params.value), + validAfter: params.validAfter ?? 0, + validBefore: params.validBefore, + nonce: params.nonce, // non-sequential nonce, random 32 byte hex string + }, + }, + }; +} diff --git a/api/_utils.ts b/api/_utils.ts index 238c8687f..48992fb4f 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -34,6 +34,7 @@ import { Infer, integer, min, + size, string, Struct, } from "superstruct"; @@ -1509,6 +1510,10 @@ export function hexString() { }); } +export function bytes32() { + return size(hexString(), 66); // inclusive of "0x" +} + /** * Returns the cushion for a given token symbol and route. If no route is specified, the cushion for the token symbol * @param symbol The token symbol @@ -2250,14 +2255,16 @@ export async function getMaxFeePerGas(chainId: number): Promise { * const res = await axios.get(`${base_url}?${queryString}`) * ``` */ - export function buildSearchParams( - params: Record> + params: Record< + string, + number | string | boolean | Array + > ): string { const searchParams = new URLSearchParams(); for (const key in params) { const value = params[key]; - if (!value) continue; + if (value === undefined || value === null) continue; if (Array.isArray(value)) { value.forEach((val) => searchParams.append(key, String(val))); } else { diff --git a/api/relay/_types.ts b/api/relay/_types.ts index 839934d49..95ea1076c 100644 --- a/api/relay/_types.ts +++ b/api/relay/_types.ts @@ -7,7 +7,7 @@ export type RelayRequest = { to: string; methodNameAndArgs: ReturnType; signatures: { - permit: string; + permit: string; // use this for all auth signatures deposit: string; }; }; diff --git a/api/relay/_utils.ts b/api/relay/_utils.ts index 82e41ab49..de5465602 100644 --- a/api/relay/_utils.ts +++ b/api/relay/_utils.ts @@ -1,17 +1,20 @@ import { assert, Infer, type } from "superstruct"; import { utils } from "ethers"; -import { hexString, positiveIntStr, validAddress } from "../_utils"; +import { bytes32, hexString, positiveIntStr, validAddress } from "../_utils"; import { getPermitTypedData } from "../_permit"; import { InvalidParamError } from "../_errors"; import { + encodeDepositWithAuthCalldata, encodeDepositWithPermitCalldata, + encodeSwapAndBridgeWithAuthCalldata, encodeSwapAndBridgeWithPermitCalldata, getDepositTypedData, getSwapAndDepositTypedData, } from "../_spoke-pool-periphery"; import { RelayRequest } from "./_types"; import { redisCache } from "../_cache"; +import { getReceiveWithAuthTypedData } from "../_transfer-with-auth"; export const GAS_SPONSOR_ADDRESS = process.env.GAS_SPONSOR_ADDRESS ?? @@ -47,13 +50,15 @@ const SwapAndDepositDataSchema = type({ routerCalldata: hexString(), }); +const DepositDataSchema = type({ + submissionFees: SubmissionFeesSchema, + baseDepositData: BaseDepositDataSchema, + inputAmount: positiveIntStr(), +}); + export const DepositWithPermitArgsSchema = type({ signatureOwner: validAddress(), - depositData: type({ - submissionFees: SubmissionFeesSchema, - baseDepositData: BaseDepositDataSchema, - inputAmount: positiveIntStr(), - }), + depositData: DepositDataSchema, deadline: positiveIntStr(), }); @@ -63,12 +68,33 @@ export const SwapAndDepositWithPermitArgsSchema = type({ deadline: positiveIntStr(), }); +export const DepositWithAuthArgsSchema = type({ + signatureOwner: validAddress(), + depositData: DepositDataSchema, + validAfter: positiveIntStr(), + validBefore: positiveIntStr(), + nonce: bytes32(), +}); + +export const SwapAndDepositWithAuthArgsSchema = type({ + signatureOwner: validAddress(), + swapAndDepositData: SwapAndDepositDataSchema, + validAfter: positiveIntStr(), + validBefore: positiveIntStr(), + nonce: bytes32(), +}); + export const allowedMethodNames = [ "depositWithPermit", "swapAndBridgeWithPermit", -]; - -export function validateMethodArgs(methodName: string, args: any) { + "depositWithAuthorization", + "swapAndBridgeWithAuthorization", +] as const; + +export function validateMethodArgs( + methodName: (typeof allowedMethodNames)[number], + args: any +) { if (methodName === "depositWithPermit") { assert(args, DepositWithPermitArgsSchema); return { @@ -81,6 +107,18 @@ export function validateMethodArgs(methodName: string, args: any) { args: args as Infer, methodName, } as const; + } else if (methodName === "depositWithAuthorization") { + assert(args, DepositWithAuthArgsSchema); + return { + args: args as Infer, + methodName, + } as const; + } else if (methodName === "swapAndBridgeWithAuthorization") { + assert(args, SwapAndDepositWithAuthArgsSchema); + return { + args: args as Infer, + methodName, + } as const; } throw new Error(`Invalid method name: ${methodName}`); } @@ -94,10 +132,15 @@ export async function verifySignatures({ const { methodName, args } = methodNameAndArgs; let signatureOwner: string; - let getPermitTypedDataPromise: ReturnType; + let getPermitTypedDataPromise: + | ReturnType + | undefined; let getDepositTypedDataPromise: ReturnType< typeof getDepositTypedData | typeof getSwapAndDepositTypedData >; + let getReceiveWithAuthTypedDataPromise: + | ReturnType + | undefined; if (methodName === "depositWithPermit") { const { signatureOwner: _signatureOwner, deadline, depositData } = args; @@ -133,41 +176,123 @@ export async function verifySignatures({ chainId, swapAndDepositData, }); + } else if (methodName === "depositWithAuthorization") { + const { + signatureOwner: _signatureOwner, + validAfter, + validBefore, + nonce, + depositData, + } = args; + signatureOwner = _signatureOwner; + getReceiveWithAuthTypedDataPromise = getReceiveWithAuthTypedData({ + tokenAddress: depositData.baseDepositData.inputToken, + chainId, + ownerAddress: signatureOwner, + spenderAddress: to, // SpokePoolV3Periphery address + value: depositData.inputAmount, + validAfter: Number(validAfter), + validBefore: Number(validBefore), + nonce, + }); + getDepositTypedDataPromise = getDepositTypedData({ + chainId, + depositData, + }); + } else if (methodName === "swapAndBridgeWithAuthorization") { + const { + signatureOwner: _signatureOwner, + validAfter, + validBefore, + nonce, + swapAndDepositData, + } = args; + signatureOwner = _signatureOwner; + getReceiveWithAuthTypedDataPromise = getReceiveWithAuthTypedData({ + tokenAddress: swapAndDepositData.swapToken, + chainId, + ownerAddress: signatureOwner, + spenderAddress: to, // SpokePoolV3Periphery address + value: swapAndDepositData.swapTokenAmount, + validAfter: Number(validAfter), + validBefore: Number(validBefore), + nonce, + }); + getDepositTypedDataPromise = getSwapAndDepositTypedData({ + chainId, + swapAndDepositData, + }); } else { throw new Error( `Can not verify signatures for invalid method name: ${methodName}` ); } - const [permitTypedData, depositTypedData] = await Promise.all([ - getPermitTypedDataPromise, - getDepositTypedDataPromise, - ]); + if (getPermitTypedDataPromise) { + const [permitTypedData, depositTypedData] = await Promise.all([ + getPermitTypedDataPromise, + getDepositTypedDataPromise, + ]); + + const recoveredPermitSignerAddress = utils.verifyTypedData( + permitTypedData.eip712.domain, + permitTypedData.eip712.types, + permitTypedData.eip712.message, + signatures.permit + ); - 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", - }); - } + 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", - }); + 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", + }); + } + } else if (getReceiveWithAuthTypedDataPromise) { + const [authTypedData, depositTypedData] = await Promise.all([ + getReceiveWithAuthTypedDataPromise, + getDepositTypedDataPromise, + ]); + + const recoveredAuthSignerAddress = utils.verifyTypedData( + authTypedData.eip712.domain, + authTypedData.eip712.types, + authTypedData.eip712.message, + signatures.permit + ); + + if (recoveredAuthSignerAddress !== signatureOwner) { + throw new InvalidParamError({ + message: "Invalid Authorization 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", + }); + } } } @@ -190,12 +315,33 @@ export function encodeCalldataForRelayRequest(request: RelayRequest) { swapAndDepositDataSignature: request.signatures.deposit, permitSignature: request.signatures.permit, }); + } else if ( + request.methodNameAndArgs.methodName === "depositWithAuthorization" + ) { + encodedCalldata = encodeDepositWithAuthCalldata({ + ...request.methodNameAndArgs.args, + validAfter: Number(request.methodNameAndArgs.args.validAfter), + validBefore: Number(request.methodNameAndArgs.args.validBefore), + nonce: request.methodNameAndArgs.args.nonce, + receiveWithAuthSignature: request.signatures.permit, + depositDataSignature: request.signatures.deposit, + }); + } else if ( + request.methodNameAndArgs.methodName === "swapAndBridgeWithAuthorization" + ) { + encodedCalldata = encodeSwapAndBridgeWithAuthCalldata({ + ...request.methodNameAndArgs.args, + validAfter: Number(request.methodNameAndArgs.args.validAfter), + validBefore: Number(request.methodNameAndArgs.args.validBefore), + nonce: request.methodNameAndArgs.args.nonce, + receiveWithAuthSignature: request.signatures.permit, + swapAndDepositDataSignature: request.signatures.deposit, + }); } - // TODO: Add cases for `withAuth` and `withPermit2` + // TODO: Add cases for `withPermit2` else { throw new Error(`Can not encode calldata for relay request`); } - return encodedCalldata; } diff --git a/api/swap/auth/_utils.ts b/api/swap/auth/_utils.ts new file mode 100644 index 000000000..323e7a43a --- /dev/null +++ b/api/swap/auth/_utils.ts @@ -0,0 +1,167 @@ +import { + CrossSwapQuotes, + DepositEntryPointContract, + OriginSwapEntryPointContract, +} from "../../_dexes/types"; +import { getReceiveWithAuthTypedData } from "../../_transfer-with-auth"; +import { + getDepositTypedData, + getSwapAndDepositTypedData, +} from "../../_spoke-pool-periphery"; +import { + extractDepositDataStruct, + extractSwapAndDepositDataStruct, +} from "../../_dexes/utils"; +import { BigNumberish, BytesLike, utils } from "ethers"; +import { SpokePoolV3PeripheryInterface } from "../../_typechain/SpokePoolV3Periphery"; + +export async function buildAuthTxPayload({ + crossSwapQuotes, + authDeadline, + authStart = 0, + submissionFees, +}: { + crossSwapQuotes: CrossSwapQuotes; + authDeadline: number; + authStart?: number; + submissionFees?: { + amount: BigNumberish; + recipient: string; + }; +}) { + const { originSwapQuote, bridgeQuote, crossSwap, contracts } = + crossSwapQuotes; + const originChainId = crossSwap.inputToken.chainId; + const { originSwapEntryPoint, depositEntryPoint, originRouter } = contracts; + + let entryPointContract: + | DepositEntryPointContract + | OriginSwapEntryPointContract; + let getDepositTypedDataPromise: + | ReturnType + | ReturnType; + let methodNameAndArgsWithoutSignatures: + | { + methodName: "depositWithAuthorization"; + argsWithoutSignatures: { + signatureOwner: string; + depositData: SpokePoolV3PeripheryInterface.DepositDataStruct; + validAfter: BigNumberish; + validBefore: BigNumberish; + nonce: BytesLike; + }; + } + | { + methodName: "swapAndBridgeWithAuthorization"; + argsWithoutSignatures: { + signatureOwner: string; + swapAndDepositData: SpokePoolV3PeripheryInterface.SwapAndDepositDataStruct; + validAfter: BigNumberish; + validBefore: BigNumberish; + nonce: BytesLike; + }; + }; + + // random non-sequesntial nonce + const nonce = utils.hexlify(utils.randomBytes(32)); + + const validAfter = authStart; + const validBefore = authDeadline; + + if (originSwapQuote) { + if (!originSwapEntryPoint) { + throw new Error( + `'originSwapEntryPoint' needs to be defined for origin swap quotes` + ); + } + // Only SpokePoolPeriphery supports transfer with auth + if (originSwapEntryPoint.name !== "SpokePoolPeriphery") { + throw new Error( + `Transfer with auth is not supported for origin swap entry point contract '${originSwapEntryPoint.name}'` + ); + } + + if (!originRouter) { + throw new Error( + `'originRouter' needs to be defined for origin swap quotes` + ); + } + const swapAndDepositData = + await extractSwapAndDepositDataStruct(crossSwapQuotes); + entryPointContract = originSwapEntryPoint; + + getDepositTypedDataPromise = getSwapAndDepositTypedData({ + swapAndDepositData: swapAndDepositData, + chainId: originChainId, + }); + methodNameAndArgsWithoutSignatures = { + methodName: "swapAndBridgeWithAuthorization", + argsWithoutSignatures: { + signatureOwner: crossSwap.depositor, + swapAndDepositData, + validAfter, + validBefore, + nonce, + }, + }; + } else { + if (!depositEntryPoint) { + throw new Error( + `'depositEntryPoint' needs to be defined for bridge quotes` + ); + } + + if (depositEntryPoint.name !== "SpokePoolPeriphery") { + throw new Error( + `auth is not supported for deposit entry point contract '${depositEntryPoint.name}'` + ); + } + const depositDataStruct = await extractDepositDataStruct( + crossSwapQuotes, + submissionFees + ); + entryPointContract = depositEntryPoint; + getDepositTypedDataPromise = getDepositTypedData({ + depositData: depositDataStruct, + chainId: originChainId, + }); + methodNameAndArgsWithoutSignatures = { + methodName: "depositWithAuthorization", + argsWithoutSignatures: { + signatureOwner: crossSwap.depositor, + depositData: depositDataStruct, + validAfter, + validBefore, + nonce, + }, + }; + } + + const [authTypedData, depositTypedData] = await Promise.all([ + getReceiveWithAuthTypedData({ + tokenAddress: + originSwapQuote?.tokenIn.address || bridgeQuote.inputToken.address, + chainId: originChainId, + ownerAddress: crossSwap.depositor, + spenderAddress: entryPointContract.address, + value: originSwapQuote?.maximumAmountIn || bridgeQuote.inputAmount, + nonce, + validAfter, + validBefore, + }), + getDepositTypedDataPromise, + ]); + + return { + eip712: { + receiveWithAuthorization: authTypedData.eip712, + deposit: depositTypedData.eip712, + }, + + swapTx: { + chainId: originChainId, + to: entryPointContract.address, + ...methodNameAndArgsWithoutSignatures, + }, + }; +} diff --git a/api/swap/auth/index.ts b/api/swap/auth/index.ts new file mode 100644 index 000000000..21b919b66 --- /dev/null +++ b/api/swap/auth/index.ts @@ -0,0 +1,122 @@ +import { VercelResponse } from "@vercel/node"; +import { assert, Infer, optional, type } from "superstruct"; + +import { TypedVercelRequest } from "../../_types"; +import { getLogger, handleErrorCondition, positiveIntStr } from "../../_utils"; +import { getCrossSwapQuotes } from "../../_dexes/cross-swap-service"; +import { + handleBaseSwapQueryParams, + BaseSwapQueryParams, + stringifyBigNumProps, +} from "../_utils"; +import { getSwapRouter02Strategy } from "../../_dexes/uniswap/swap-router-02"; +import { InvalidParamError } from "../../_errors"; +import { QuoteFetchStrategies } from "../../_dexes/utils"; +import { buildAuthTxPayload } from "./_utils"; +import { GAS_SPONSOR_ADDRESS } from "../../relay/_utils"; +import * as sdk from "@across-protocol/sdk"; + +export const authSwapQueryParamsSchema = type({ + authDeadline: optional(positiveIntStr()), +}); + +export type authSwapQueryParams = Infer; + +const DEFAULT_AUTH_DEADLINE = sdk.utils.getCurrentTime() + 60 * 60 * 24 * 365; // 1 year + +// For auth-based flows, we have to use the `SpokePoolPeriphery` as an entry point +const quoteFetchStrategies: QuoteFetchStrategies = { + default: getSwapRouter02Strategy("SpokePoolPeriphery"), +}; + +const handler = async ( + request: TypedVercelRequest, + response: VercelResponse +) => { + const logger = getLogger(); + logger.debug({ + at: "Swap/auth", + message: "Query data", + query: request.query, + }); + try { + // `/swap/auth` specific params validation + const { + authDeadline: _authDeadline, + authStart: _authStart, + ...restQuery + } = request.query; + assert( + { + authDeadline: _authDeadline, + }, + authSwapQueryParamsSchema + ); + const authDeadline = Number(_authDeadline ?? DEFAULT_AUTH_DEADLINE); + const authStart = Number(_authStart ?? sdk.utils.getCurrentTime()); + + if (authDeadline < Math.floor(Date.now() / 1000)) { + throw new InvalidParamError({ + message: + "auth deadline must be a UNIX timestamp (seconds) in the future", + param: "authDeadline", + }); + } + + // `/swap` specific params validation + quote generation + const { + isInputNative, + isOutputNative, + inputToken, + outputToken, + amount, + amountType, + refundOnOrigin, + refundAddress, + recipient, + depositor, + slippageTolerance, + } = await handleBaseSwapQueryParams(restQuery); + + const crossSwapQuotes = await getCrossSwapQuotes( + { + amount, + inputToken, + outputToken, + depositor, + recipient: recipient || depositor, + slippageTolerance: Number(slippageTolerance), + type: amountType, + refundOnOrigin, + refundAddress, + isInputNative, + isOutputNative, + }, + quoteFetchStrategies + ); + // Build tx for auth + const crossSwapTxForAuth = await buildAuthTxPayload({ + crossSwapQuotes, + authDeadline, + authStart, + // FIXME: Calculate proper fees + submissionFees: { + amount: "0", + recipient: GAS_SPONSOR_ADDRESS, + }, + }); + + const responseJson = stringifyBigNumProps(crossSwapTxForAuth); + + logger.debug({ + at: "Swap/auth", + message: "Response data", + responseJson, + }); + response.status(200).json(responseJson); + } catch (error: unknown) { + return handleErrorCondition("swap/auth", response, logger, error); + } +}; + +export default handler; diff --git a/scripts/tests/_swap-utils.ts b/scripts/tests/_swap-utils.ts index a5598bfb8..6b4f52e4f 100644 --- a/scripts/tests/_swap-utils.ts +++ b/scripts/tests/_swap-utils.ts @@ -169,7 +169,7 @@ export function filterTestCases( return filteredTestCases; } -export async function fetchSwapQuote(slug: "approval" | "permit") { +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); @@ -180,6 +180,6 @@ export async function fetchSwapQuote(slug: "approval" | "permit") { params: testCase.params, }); console.log(JSON.stringify(response.data, null, 2)); - return response.data; + return response.data as T; } } diff --git a/scripts/tests/swap-auth.ts b/scripts/tests/swap-auth.ts new file mode 100644 index 000000000..31a6e2467 --- /dev/null +++ b/scripts/tests/swap-auth.ts @@ -0,0 +1,69 @@ +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>; + +async function swapWithAuth() { + console.log("Swapping with auth..."); + const swapQuote = await fetchSwapQuote("auth"); + + if (!swapQuote) { + console.log("No Quote"); + return; + } + + if (process.env.DEV_WALLET_PK) { + const wallet = new Wallet(process.env.DEV_WALLET_PK!).connect( + 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)); + } + } +} + +swapWithAuth() + .then(() => console.log("Done")) + .catch((e) => { + console.error(e); + if (e.response?.data) { + console.log("Tx for debug sim:", e.response.data.transaction); + } + }); diff --git a/scripts/tests/swap-permit.ts b/scripts/tests/swap-permit.ts index ab052e48e..ce3c4e409 100644 --- a/scripts/tests/swap-permit.ts +++ b/scripts/tests/swap-permit.ts @@ -2,11 +2,19 @@ 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>; + async function swapWithPermit() { console.log("Swapping with permit..."); - const swapQuote = await fetchSwapQuote("permit"); + const swapQuote = await fetchSwapQuote("permit"); + + if (!swapQuote) { + console.log("No Quote"); + return; + } if (process.env.DEV_WALLET_PK) { const wallet = new Wallet(process.env.DEV_WALLET_PK!).connect( From 73b19f8b787fa4751ff9a9efa7ddcdb2f12c041a Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Mon, 6 Jan 2025 23:50:00 +0100 Subject: [PATCH 43/47] feat: unified swap endpoint (#1353) * feat: unified swap endpoint * refactor: change DEFAULT_PERMIT_DEADLINE to a constant in PermitSwapQueryParamsSchema * fixup --- api/_permit.ts | 128 ++++++++++++++++++++++--------------- api/_transfer-with-auth.ts | 104 +++++++++++++++++++----------- api/swap/index.ts | 80 +++++++++++++++++++++++ 3 files changed, 226 insertions(+), 86 deletions(-) create mode 100644 api/swap/index.ts diff --git a/api/_permit.ts b/api/_permit.ts index 9f2521fcc..6d189d8a7 100644 --- a/api/_permit.ts +++ b/api/_permit.ts @@ -3,6 +3,20 @@ import { BigNumberish, ethers } from "ethers"; import { getProvider } from "./_utils"; import { ERC20_PERMIT_ABI } from "./_abis"; +export class PermitNotSupportedError extends Error { + constructor(tokenAddress: string, cause?: Error) { + super(`ERC-20 contract ${tokenAddress} does not support permit`, { + cause, + }); + } +} + +export class PermitDomainSeparatorMismatchError extends Error { + constructor(tokenAddress: string) { + super(`Permit EIP712 domain separator mismatch for ${tokenAddress}`); + } +} + export async function getPermitTypedData(params: { tokenAddress: string; chainId: number; @@ -11,6 +25,62 @@ export async function getPermitTypedData(params: { value: BigNumberish; deadline: number; eip712DomainVersion?: number; +}) { + const { name, domainSeparator, eip712DomainVersion, nonce } = + await getPermitArgsFromContract(params); + + return { + domainSeparator, + eip712: { + types: { + Permit: [ + { + name: "owner", + type: "address", + }, + { + name: "spender", + type: "address", + }, + { + name: "value", + type: "uint256", + }, + { + name: "nonce", + type: "uint256", + }, + { + name: "deadline", + type: "uint256", + }, + ], + }, + primaryType: "Permit", + domain: { + name, + version: eip712DomainVersion.toString(), + chainId: params.chainId, + verifyingContract: params.tokenAddress, + }, + message: { + owner: params.ownerAddress, + spender: params.spenderAddress, + value: String(params.value), + nonce: String(nonce), + deadline: String(params.deadline), + }, + }, + }; +} + +export async function getPermitArgsFromContract(params: { + tokenAddress: string; + chainId: number; + ownerAddress: string; + spenderAddress: string; + value: BigNumberish; + eip712DomainVersion?: number; }) { const provider = getProvider(params.chainId); const erc20Permit = new ethers.Contract( @@ -44,21 +114,16 @@ export async function getPermitTypedData(params: { : domainSeparatorResult.status === "rejected" ? domainSeparatorResult.reason : new Error("Unknown error"); - throw new Error( - `ERC-20 contract ${params.tokenAddress} does not support permit`, - { - cause: error, - } - ); + throw new PermitNotSupportedError(params.tokenAddress, error); } - const name = nameResult.value; + const name: string = nameResult.value; const versionFromContract = versionFromContractResult.status === "fulfilled" ? versionFromContractResult.value : undefined; - const nonce = nonceResult.value; - const domainSeparator = domainSeparatorResult.value; + const nonce: BigNumberish = nonceResult.value; + const domainSeparator: string = domainSeparatorResult.value; const eip712DomainVersion = [1, 2, "1", "2"].includes(versionFromContract) ? Number(versionFromContract) @@ -80,50 +145,13 @@ export async function getPermitTypedData(params: { ); if (domainSeparator !== domainSeparatorHash) { - throw new Error("EIP712 domain separator mismatch"); + throw new PermitDomainSeparatorMismatchError(params.tokenAddress); } return { + name, domainSeparator, - eip712: { - types: { - Permit: [ - { - name: "owner", - type: "address", - }, - { - name: "spender", - type: "address", - }, - { - name: "value", - type: "uint256", - }, - { - name: "nonce", - type: "uint256", - }, - { - name: "deadline", - type: "uint256", - }, - ], - }, - primaryType: "Permit", - domain: { - name, - version: eip712DomainVersion.toString(), - chainId: params.chainId, - verifyingContract: params.tokenAddress, - }, - message: { - owner: params.ownerAddress, - spender: params.spenderAddress, - value: String(params.value), - nonce: String(nonce), - deadline: String(params.deadline), - }, - }, + eip712DomainVersion, + nonce, }; } diff --git a/api/_transfer-with-auth.ts b/api/_transfer-with-auth.ts index a14a3b6d2..650a51992 100644 --- a/api/_transfer-with-auth.ts +++ b/api/_transfer-with-auth.ts @@ -3,6 +3,25 @@ import { getProvider } from "./_utils"; import { ERC_TRANSFER_WITH_AUTH_ABI } from "./_abis"; import { utils } from "ethers"; +export class TransferWithAuthNotSupportedError extends Error { + constructor(tokenAddress: string, cause?: Error) { + super( + `ERC-20 contract ${tokenAddress} does not support transfer with authorization`, + { + cause, + } + ); + } +} + +export class TransferWithAuthDomainSeparatorMismatchError extends Error { + constructor(tokenAddress: string) { + super( + `Transfer with authorization EIP712 domain separator mismatch for ${tokenAddress}` + ); + } +} + export function hashDomainSeparator(params: { name: string; version: string | number; @@ -35,6 +54,49 @@ export async function getReceiveWithAuthTypedData(params: { nonce: string; validAfter?: number; eip712DomainVersion?: number; +}) { + const { name, domainSeparator, eip712DomainVersion } = + await getReceiveWithAuthArgsFromContract(params); + + return { + domainSeparator, + eip712: { + types: { + ReceiveWithAuthorization: [ + { name: "from", type: "address" }, + { name: "to", type: "address" }, + { name: "value", type: "uint256" }, + { name: "validAfter", type: "uint256" }, + { name: "validBefore", type: "uint256" }, + { name: "nonce", type: "bytes32" }, + ], + }, + domain: { + name, + version: eip712DomainVersion.toString(), + chainId: params.chainId, + verifyingContract: params.tokenAddress, + }, + primaryType: "ReceiveWithAuthorization", + message: { + from: params.ownerAddress, + to: params.spenderAddress, + value: String(params.value), + validAfter: params.validAfter ?? 0, + validBefore: params.validBefore, + nonce: params.nonce, // non-sequential nonce, random 32 byte hex string + }, + }, + }; +} + +export async function getReceiveWithAuthArgsFromContract(params: { + tokenAddress: string; + chainId: number; + ownerAddress: string; + spenderAddress: string; + value: BigNumberish; + eip712DomainVersion?: number; }) { const provider = getProvider(params.chainId); @@ -61,20 +123,15 @@ export async function getReceiveWithAuthTypedData(params: { : domainSeparatorResult.status === "rejected" ? domainSeparatorResult.reason : new Error("Unknown error"); - throw new Error( - `Contract ${params.tokenAddress} does not support transfer with authorization`, - { - cause: error, - } - ); + throw new TransferWithAuthNotSupportedError(params.tokenAddress, error); } - const name = nameResult.value; + const name: string = nameResult.value; const versionFromContract = versionFromContractResult.status === "fulfilled" ? versionFromContractResult.value : undefined; - const domainSeparator = domainSeparatorResult.value; + const domainSeparator: string = domainSeparatorResult.value; const eip712DomainVersion = [1, 2, "1", "2"].includes(versionFromContract) ? Number(versionFromContract) @@ -88,37 +145,12 @@ export async function getReceiveWithAuthTypedData(params: { }); if (domainSeparator !== domainSeparatorHash) { - throw new Error("EIP712 domain separator mismatch"); + throw new TransferWithAuthDomainSeparatorMismatchError(params.tokenAddress); } return { + name, domainSeparator, - eip712: { - types: { - ReceiveWithAuthorization: [ - { name: "from", type: "address" }, - { name: "to", type: "address" }, - { name: "value", type: "uint256" }, - { name: "validAfter", type: "uint256" }, - { name: "validBefore", type: "uint256" }, - { name: "nonce", type: "bytes32" }, - ], - }, - domain: { - name, - version: eip712DomainVersion.toString(), - chainId: params.chainId, - verifyingContract: params.tokenAddress, - }, - primaryType: "ReceiveWithAuthorization", - message: { - from: params.ownerAddress, - to: params.spenderAddress, - value: String(params.value), - validAfter: params.validAfter ?? 0, - validBefore: params.validBefore, - nonce: params.nonce, // non-sequential nonce, random 32 byte hex string - }, - }, + eip712DomainVersion, }; } diff --git a/api/swap/index.ts b/api/swap/index.ts new file mode 100644 index 000000000..d97798590 --- /dev/null +++ b/api/swap/index.ts @@ -0,0 +1,80 @@ +import { VercelResponse } from "@vercel/node"; + +import { TypedVercelRequest } from "../_types"; +import { + getLogger, + handleErrorCondition, + resolveVercelEndpoint, +} from "../_utils"; +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 }); +} +const swapFlowTypeToHandler = { + permit: makeSwapHandler("permit"), + "transfer-with-auth": makeSwapHandler("auth"), + approval: makeSwapHandler("approval"), +}; + +export default async function handler( + request: TypedVercelRequest, + response: VercelResponse +) { + const logger = getLogger(); + logger.debug({ + at: "Swap", + message: "Query data", + query: request.query, + }); + try { + // `/swap` only validate shared base params + const { inputToken, amount, recipient, depositor } = + await handleBaseSwapQueryParams(request.query); + + // Determine swap flow by checking if required args and methods are supported + let swapFlowType: SwapFlowType; + const args = { + tokenAddress: inputToken.address, + chainId: inputToken.chainId, + ownerAddress: depositor, + spenderAddress: recipient || depositor, + value: amount, + }; + const [permitArgsResult, transferWithAuthArgsResult] = + await Promise.allSettled([ + getPermitArgsFromContract(args), + getReceiveWithAuthArgsFromContract(args), + ]); + + if (permitArgsResult.status === "fulfilled") { + swapFlowType = "permit"; + } else if (transferWithAuthArgsResult.status === "fulfilled") { + swapFlowType = "transfer-with-auth"; + } else { + swapFlowType = "approval"; + } + + const handler = swapFlowTypeToHandler[swapFlowType]; + const responseJson = await handler(request.query); + const enrichedResponseJson = { + ...responseJson, + swapFlowType, + }; + + logger.debug({ + at: "Swap", + message: "Response data", + responseJson: enrichedResponseJson, + }); + response.status(200).json(enrichedResponseJson); + } catch (error: unknown) { + return handleErrorCondition("swap", response, logger, error); + } +} From 8593c507f4488921e95fe431a37688d9daeb2575 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Tue, 7 Jan 2025 11:53:07 +0100 Subject: [PATCH 44/47] refactor: cleanup + align swap response formats (#1355) * refactor: use common hash domain separator * feat: align response format of all swap handlers * test: add unified swap endpoint script --- api/_eip712.ts | 23 +++++++ api/_permit.ts | 21 ++---- api/_transfer-with-auth.ts | 24 +------ api/swap/_utils.ts | 114 ++++++++++++++++++++++++++++++++ api/swap/approval/_utils.ts | 2 +- api/swap/approval/index.ts | 90 +++++-------------------- api/swap/auth/_utils.ts | 5 +- api/swap/auth/index.ts | 25 ++++++- api/swap/index.ts | 8 +-- api/swap/permit/_utils.ts | 2 + api/swap/permit/index.ts | 25 ++++++- scripts/tests/_swap-utils.ts | 107 ++++++++++++++++++++++++++++-- scripts/tests/swap-allowance.ts | 40 +++-------- scripts/tests/swap-auth.ts | 48 ++------------ scripts/tests/swap-permit.ts | 48 ++------------ scripts/tests/swap-unified.ts | 45 +++++++++++++ 16 files changed, 382 insertions(+), 245 deletions(-) create mode 100644 api/_eip712.ts create mode 100644 scripts/tests/swap-unified.ts 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); + } + }); From b6d634ff215cfe8c5c4669ef71d24934422dbc6e Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Wed, 8 Jan 2025 15:09:24 +0100 Subject: [PATCH 45/47] fixup (#1359) --- api/_dexes/cross-swap-service.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/api/_dexes/cross-swap-service.ts b/api/_dexes/cross-swap-service.ts index d1f3dbfd9..0339adb07 100644 --- a/api/_dexes/cross-swap-service.ts +++ b/api/_dexes/cross-swap-service.ts @@ -327,7 +327,7 @@ export async function getCrossSwapQuotesForOutputA2B( type: crossSwap.type, }; // 2.1. Get origin swap quote for any input token -> bridgeable input token - const originSwapQuote = await profiler.measureAsync( + const indicativeOriginSwapQuote = await profiler.measureAsync( originStrategy.fetchFn( { ...originSwap, @@ -342,12 +342,12 @@ export async function getCrossSwapQuotesForOutputA2B( ); // 2.2. Re-fetch origin swap quote with updated input amount and EXACT_INPUT type. // This prevents leftover tokens in the SwapAndBridge contract. - let adjOriginSwapQuote = await profiler.measureAsync( + let originSwapQuote = await profiler.measureAsync( originStrategy.fetchFn( { ...originSwap, amount: addMarkupToAmount( - originSwapQuote.maximumAmountIn, + indicativeOriginSwapQuote.maximumAmountIn, indicativeQuoteBuffer ).toString(), }, @@ -355,10 +355,7 @@ export async function getCrossSwapQuotesForOutputA2B( ), "getOriginSwapQuote" ); - assertMinOutputAmount( - adjOriginSwapQuote.minAmountOut, - bridgeQuote.inputAmount - ); + assertMinOutputAmount(originSwapQuote.minAmountOut, bridgeQuote.inputAmount); return { crossSwap, From e3acd1c25ee871aac25d5e231eabd8b1d829dbe3 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Wed, 15 Jan 2025 17:27:33 +0100 Subject: [PATCH 46/47] fixup --- package.json | 2 +- yarn.lock | 45 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 18dfa81a3..4b69ee896 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@across-protocol/constants": "^3.1.30", "@across-protocol/contracts": "^3.0.25", "@across-protocol/contracts-v3.0.6": "npm:@across-protocol/contracts@3.0.6", - "@across-protocol/sdk": "^3.3.23", + "@across-protocol/sdk": "^3.4.12", "@amplitude/analytics-browser": "^2.3.5", "@balancer-labs/sdk": "1.1.6-beta.16", "@emotion/react": "^11.13.0", diff --git a/yarn.lock b/yarn.lock index d391a0b63..9dcdd2f1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7816,6 +7816,11 @@ immer "^9.0.7" lodash-es "^4.17.21" +"@uniswap/default-token-list@^11.13.0": + version "11.19.0" + resolved "https://registry.yarnpkg.com/@uniswap/default-token-list/-/default-token-list-11.19.0.tgz#12d4e40f6c79f794d3e3a71e2d4d9784fb6c967b" + integrity sha512-H/YLpxeZUrzT4Ki8mi4k5UiadREiLHg7WUqCv0Qt/VkOjX2mIBhrxCj1Wh61/J7lK0XqOjksfpm6RG1+YErPoQ== + "@uniswap/lib@1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@uniswap/lib/-/lib-1.1.1.tgz#0afd29601846c16e5d082866cbb24a9e0758e6bc" @@ -9031,6 +9036,13 @@ agent-base@^7.1.2: resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.3.tgz#29435eb821bc4194633a5b89e5bc4703bafc25a1" integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw== +agentkeepalive@^4.5.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz#35f73e94b3f40bf65f105219c623ad19c136ea6a" + integrity sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ== + dependencies: + humanize-ms "^1.2.1" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -10911,6 +10923,23 @@ bundle-require@^4.0.0: dependencies: load-tsconfig "^0.2.3" +bunyan-blackhole@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/bunyan-blackhole/-/bunyan-blackhole-1.1.1.tgz#b9208586dc0b4e47f4f713215b1bddd65e4f6257" + integrity sha512-UwzNPhbbSqbzeJhCbygqjlAY7p0ZUdv1ADXPQvDh3CA7VW3C/rCc1gaQO/8j9QL4vsKQCQZQSQIEwX+lxioPAQ== + dependencies: + stream-blackhole "^1.0.3" + +bunyan@^1.8.15: + version "1.8.15" + resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.8.15.tgz#8ce34ca908a17d0776576ca1b2f6cbd916e93b46" + integrity sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig== + optionalDependencies: + dtrace-provider "~0.8" + moment "^2.19.3" + mv "~2" + safe-json-stringify "~1" + byline@5.x: version "5.0.0" resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" @@ -12079,7 +12108,7 @@ crypto-hash@^1.3.0: resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247" integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg== -crypto-js@^4.2.0: +crypto-js@>=4.2.0, crypto-js@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== @@ -14002,7 +14031,7 @@ ethers@5.5.3: "@ethersproject/web" "5.5.1" "@ethersproject/wordlists" "5.5.0" -ethers@5.7.2, ethers@^5.0.13, ethers@^5.5.4, ethers@^5.6.8, ethers@^5.7.1, ethers@^5.7.2: +ethers@5.7.2, ethers@^5.0.13, ethers@^5.5.4, ethers@^5.6.8, ethers@^5.7.0, ethers@^5.7.1, ethers@^5.7.2: version "5.7.2" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== @@ -16852,6 +16881,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +isnumber@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isnumber/-/isnumber-1.0.0.tgz#0e3f9759b581d99dd85086f0ec2a74909cfadd01" + integrity sha512-JLiSz/zsZcGFXPrB4I/AGBvtStkt+8QmksyZBZnVXnnK9XdTEyz0tX8CRYljtwYDuIuZzih6DpHQdi+3Q6zHPw== + isobject@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" @@ -21672,6 +21706,13 @@ rimraf@^5.0.5: dependencies: glob "^10.3.7" +rimraf@~2.4.0: + version "2.4.5" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da" + integrity sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ== + dependencies: + glob "^6.0.1" + rimraf@~2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" From 9f02d5d97583b3d294b84c52a557599188656d98 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Wed, 15 Jan 2025 17:44:35 +0100 Subject: [PATCH 47/47] feat: add stablecoins for a2a bridging (#1373) * feat: add stablecoins for a2a bridging * prevent infinite loop 508 * fixup * another try to prevent 508 * fix: override vercel dev typescript version * fixup * fixup * fixup * fixup * use fallback in indicative quote * use services --- api/_dexes/cross-swap-service.ts | 76 ++++++++++------ api/_dexes/uniswap/trading-api.ts | 71 ++++++++------- api/_dexes/utils.ts | 15 +++- api/swap/_utils.ts | 6 +- api/swap/approval/_service.ts | 142 ++++++++++++++++++++++++++++++ api/swap/approval/index.ts | 137 +--------------------------- api/swap/auth/_service.ts | 122 +++++++++++++++++++++++++ api/swap/auth/index.ts | 120 ++----------------------- api/swap/index.ts | 26 +++--- api/swap/permit/_service.ts | 124 ++++++++++++++++++++++++++ api/swap/permit/index.ts | 106 +--------------------- scripts/tests/_swap-utils.ts | 1 - 12 files changed, 520 insertions(+), 426 deletions(-) create mode 100644 api/swap/approval/_service.ts create mode 100644 api/swap/auth/_service.ts create mode 100644 api/swap/permit/_service.ts diff --git a/api/_dexes/cross-swap-service.ts b/api/_dexes/cross-swap-service.ts index 0339adb07..ae2f40387 100644 --- a/api/_dexes/cross-swap-service.ts +++ b/api/_dexes/cross-swap-service.ts @@ -15,6 +15,7 @@ import { buildMinOutputBridgeTokenMessage, getCrossSwapType, getQuoteFetchStrategy, + NoQuoteFoundError, PREFERRED_BRIDGE_TOKENS, QuoteFetchStrategies, } from "./utils"; @@ -376,7 +377,7 @@ export async function getCrossSwapQuotesForOutputA2A( strategies: QuoteFetchStrategies ) { const preferredBridgeTokens = PREFERRED_BRIDGE_TOKENS; - const bridgeRoutesLimit = 1; + const bridgeRoutesToCompareChunkSize = 2; const originSwapChainId = crossSwap.inputToken.chainId; const destinationSwapChainId = crossSwap.outputToken.chainId; @@ -399,36 +400,59 @@ export async function getCrossSwapQuotesForOutputA2A( const preferredBridgeRoutes = allBridgeRoutes.filter(({ toTokenSymbol }) => preferredBridgeTokens.includes(toTokenSymbol) ); - const bridgeRoutesToCompare = ( - preferredBridgeRoutes.length > 0 ? preferredBridgeRoutes : allBridgeRoutes - ).slice(0, bridgeRoutesLimit); + const bridgeRoutes = + preferredBridgeRoutes.length > 0 ? preferredBridgeRoutes : allBridgeRoutes; + + let chunkStart = 0; + while (chunkStart < bridgeRoutes.length) { + const bridgeRoutesToCompare = bridgeRoutes.slice( + chunkStart, + chunkStart + bridgeRoutesToCompareChunkSize + ); - if (bridgeRoutesToCompare.length === 0) { - throw new Error( - `No bridge routes to compare for ${originSwapChainId} -> ${destinationSwapChainId}` + if (bridgeRoutesToCompare.length === 0) { + throw new Error( + `No bridge routes to compare for ${originSwapChainId} -> ${destinationSwapChainId}` + ); + } + + const crossSwapQuotesResults = await Promise.allSettled( + bridgeRoutesToCompare.map((bridgeRoute) => + getCrossSwapQuotesForOutputByRouteA2A( + crossSwap, + bridgeRoute, + originStrategy, + destinationStrategy + ) + ) ); - } - const crossSwapQuotes = await Promise.all( - bridgeRoutesToCompare.map((bridgeRoute) => - getCrossSwapQuotesForOutputByRouteA2A( - crossSwap, - bridgeRoute, - originStrategy, - destinationStrategy + const crossSwapQuotes = crossSwapQuotesResults + .filter((result) => result.status === "fulfilled") + .map((result) => result.value); + + if (crossSwapQuotes.length === 0) { + chunkStart += bridgeRoutesToCompareChunkSize; + continue; + } + + // Compare quotes by lowest input amount + const bestCrossSwapQuote = crossSwapQuotes.reduce((prev, curr) => + prev.originSwapQuote!.maximumAmountIn.lt( + curr.originSwapQuote!.maximumAmountIn ) - ) - ); + ? prev + : curr + ); + return bestCrossSwapQuote; + } - // Compare quotes by lowest input amount - const bestCrossSwapQuote = crossSwapQuotes.reduce((prev, curr) => - prev.originSwapQuote!.maximumAmountIn.lt( - curr.originSwapQuote!.maximumAmountIn - ) - ? prev - : curr - ); - return bestCrossSwapQuote; + throw new NoQuoteFoundError({ + originSwapChainId, + inputTokenSymbol: crossSwap.inputToken.symbol, + destinationSwapChainId, + outputTokenSymbol: crossSwap.outputToken.symbol, + }); } export async function getCrossSwapQuotesForOutputByRouteA2A( diff --git a/api/_dexes/uniswap/trading-api.ts b/api/_dexes/uniswap/trading-api.ts index 8f556ab07..1a381b307 100644 --- a/api/_dexes/uniswap/trading-api.ts +++ b/api/_dexes/uniswap/trading-api.ts @@ -1,5 +1,5 @@ import { TradeType } from "@uniswap/sdk-core"; -import axios from "axios"; +import axios, { AxiosError } from "axios"; import { Swap } from "../types"; import { V2PoolInRoute, V3PoolInRoute } from "./adapter"; @@ -76,38 +76,49 @@ export async function getUniswapClassicQuoteFromApi( export async function getUniswapClassicIndicativeQuoteFromApi( swap: UniswapParamForApi, - tradeType: TradeType + tradeType: TradeType, + useFallback: boolean = true ) { - const response = await axios.post<{ - requestId: string; - input: { - amount: string; - chainId: number; - token: string; - }; - output: { - amount: string; - chainId: number; - token: string; - }; - }>( - `${UNISWAP_TRADING_API_BASE_URL}/indicative_quote`, - { - type: - tradeType === TradeType.EXACT_INPUT ? "EXACT_INPUT" : "EXACT_OUTPUT", - amount: swap.amount, - tokenInChainId: swap.tokenIn.chainId, - tokenOutChainId: swap.tokenOut.chainId, - tokenIn: swap.tokenIn.address, - tokenOut: swap.tokenOut.address, - }, - { - headers: { - "x-api-key": UNISWAP_API_KEY, + try { + const response = await axios.post<{ + requestId: string; + input: { + amount: string; + chainId: number; + token: string; + }; + output: { + amount: string; + chainId: number; + token: string; + }; + }>( + `${UNISWAP_TRADING_API_BASE_URL}/indicative_quote`, + { + type: + tradeType === TradeType.EXACT_INPUT ? "EXACT_INPUT" : "EXACT_OUTPUT", + amount: swap.amount, + tokenInChainId: swap.tokenIn.chainId, + tokenOutChainId: swap.tokenOut.chainId, + tokenIn: swap.tokenIn.address, + tokenOut: swap.tokenOut.address, }, + { + headers: { + "x-api-key": UNISWAP_API_KEY, + }, + } + ); + return response.data; + } catch (error) { + if (error instanceof AxiosError && error.response?.status === 404) { + if (useFallback) { + const { quote } = await getUniswapClassicQuoteFromApi(swap, tradeType); + return quote; + } } - ); - return response.data; + throw error; + } } export async function getUniswapClassicCalldataFromApi( diff --git a/api/_dexes/utils.ts b/api/_dexes/utils.ts index 1546c9e61..91d5917ef 100644 --- a/api/_dexes/utils.ts +++ b/api/_dexes/utils.ts @@ -50,7 +50,7 @@ export const CROSS_SWAP_TYPE = { ANY_TO_ANY: "anyToAny", } as const; -export const PREFERRED_BRIDGE_TOKENS = ["WETH"]; +export const PREFERRED_BRIDGE_TOKENS = ["WETH", "USDC", "USDT", "DAI"]; export const defaultQuoteFetchStrategy: QuoteFetchStrategy = // This will be our default strategy until the periphery contract is audited @@ -416,3 +416,16 @@ export function assertMinOutputAmount( ); } } + +export class NoQuoteFoundError extends Error { + constructor(params: { + originSwapChainId: number; + inputTokenSymbol: string; + destinationSwapChainId: number; + outputTokenSymbol: string; + }) { + super( + `No quote found for ${params.originSwapChainId} ${params.inputTokenSymbol} -> ${params.destinationSwapChainId} ${params.outputTokenSymbol}` + ); + } +} diff --git a/api/swap/_utils.ts b/api/swap/_utils.ts index ec14681b4..68d3e7310 100644 --- a/api/swap/_utils.ts +++ b/api/swap/_utils.ts @@ -288,7 +288,8 @@ export function buildBaseSwapResponseJson(params: { data: string; value?: BigNumber; gas?: BigNumber; - gasPrice: BigNumber; + maxFeePerGas?: BigNumber; + maxPriorityFeePerGas?: BigNumber; }; permitSwapTx?: AuthTxPayload | PermitTxPayload; }) { @@ -368,7 +369,8 @@ export function buildBaseSwapResponseJson(params: { data: params.approvalSwapTx.data, value: params.approvalSwapTx.value, gas: params.approvalSwapTx.gas, - gasPrice: params.approvalSwapTx.gasPrice, + maxFeePerGas: params.approvalSwapTx.maxFeePerGas, + maxPriorityFeePerGas: params.approvalSwapTx.maxPriorityFeePerGas, } : params.permitSwapTx ? params.permitSwapTx.swapTx diff --git a/api/swap/approval/_service.ts b/api/swap/approval/_service.ts new file mode 100644 index 000000000..ef29de27e --- /dev/null +++ b/api/swap/approval/_service.ts @@ -0,0 +1,142 @@ +import { BigNumber, constants } from "ethers"; + +import { getProvider, latestGasPriceCache } from "../../_utils"; +import { buildCrossSwapTxForAllowanceHolder } from "./_utils"; +import { + handleBaseSwapQueryParams, + BaseSwapQueryParams, + getApprovalTxns, + buildBaseSwapResponseJson, +} from "../_utils"; +import { getBalanceAndAllowance } from "../../_erc20"; +import { getCrossSwapQuotes } from "../../_dexes/cross-swap-service"; +import { QuoteFetchStrategies } from "../../_dexes/utils"; +import { TypedVercelRequest } from "../../_types"; +import { getSwapRouter02Strategy } from "../../_dexes/uniswap/swap-router-02"; + +// For approval-based flows, we use the `UniversalSwapAndBridge` strategy with Uniswap V3's `SwapRouter02` +const quoteFetchStrategies: QuoteFetchStrategies = { + default: getSwapRouter02Strategy("UniversalSwapAndBridge"), +}; + +export async function handleApprovalSwap( + request: TypedVercelRequest +) { + const { + integratorId, + skipOriginTxEstimation, + isInputNative, + isOutputNative, + inputToken, + outputToken, + amount, + amountType, + refundOnOrigin, + refundAddress, + recipient, + depositor, + slippageTolerance, + refundToken, + } = await handleBaseSwapQueryParams(request.query); + + const crossSwapQuotes = await getCrossSwapQuotes( + { + amount, + inputToken, + outputToken, + depositor, + recipient: recipient || depositor, + slippageTolerance: Number(slippageTolerance), + type: amountType, + refundOnOrigin, + refundAddress, + isInputNative, + isOutputNative, + }, + quoteFetchStrategies + ); + + const crossSwapTx = await buildCrossSwapTxForAllowanceHolder( + crossSwapQuotes, + integratorId + ); + + const { originSwapQuote, bridgeQuote, destinationSwapQuote, crossSwap } = + crossSwapQuotes; + + const originChainId = crossSwap.inputToken.chainId; + const inputTokenAddress = isInputNative + ? constants.AddressZero + : crossSwap.inputToken.address; + const inputAmount = + originSwapQuote?.maximumAmountIn || bridgeQuote.inputAmount; + + const { allowance, balance } = await getBalanceAndAllowance({ + chainId: originChainId, + tokenAddress: inputTokenAddress, + owner: crossSwap.depositor, + spender: crossSwapTx.to, + }); + + const isSwapTxEstimationPossible = + !skipOriginTxEstimation && + allowance.gte(inputAmount) && + balance.gte(inputAmount); + + let originTxGas: BigNumber | undefined; + let originTxGasPrice: + | { + maxFeePerGas: BigNumber; + maxPriorityFeePerGas: BigNumber; + } + | undefined; + if (isSwapTxEstimationPossible) { + const provider = getProvider(originChainId); + [originTxGas, originTxGasPrice] = await Promise.all([ + provider.estimateGas({ + ...crossSwapTx, + from: crossSwap.depositor, + }), + latestGasPriceCache(originChainId).get(), + ]); + } else { + originTxGasPrice = await latestGasPriceCache(originChainId).get(); + } + + let approvalTxns: + | { + chainId: number; + to: string; + data: string; + }[] + | undefined; + // @TODO: Allow for just enough approval amount to be set. + const approvalAmount = constants.MaxUint256; + if (allowance.lt(inputAmount)) { + approvalTxns = getApprovalTxns({ + token: crossSwap.inputToken, + spender: crossSwapTx.to, + amount: approvalAmount, + }); + } + + const responseJson = buildBaseSwapResponseJson({ + originChainId, + inputTokenAddress, + inputAmount, + approvalSwapTx: { + ...crossSwapTx, + gas: originTxGas, + maxFeePerGas: originTxGasPrice?.maxFeePerGas, + maxPriorityFeePerGas: originTxGasPrice?.maxPriorityFeePerGas, + }, + allowance, + balance, + approvalTxns, + originSwapQuote, + bridgeQuote, + destinationSwapQuote, + refundToken, + }); + return responseJson; +} diff --git a/api/swap/approval/index.ts b/api/swap/approval/index.ts index 44f87c9d6..fb9785cb8 100644 --- a/api/swap/approval/index.ts +++ b/api/swap/approval/index.ts @@ -1,30 +1,9 @@ import { VercelResponse } from "@vercel/node"; -import { BigNumber, constants } from "ethers"; import { TypedVercelRequest } from "../../_types"; -import { - getLogger, - getProvider, - handleErrorCondition, - latestGasPriceCache, - Profiler, -} from "../../_utils"; -import { buildCrossSwapTxForAllowanceHolder } from "./_utils"; -import { - handleBaseSwapQueryParams, - BaseSwapQueryParams, - getApprovalTxns, - buildBaseSwapResponseJson, -} from "../_utils"; -import { getBalanceAndAllowance } from "../../_erc20"; -import { getCrossSwapQuotes } from "../../_dexes/cross-swap-service"; -import { getSwapRouter02Strategy } from "../../_dexes/uniswap/swap-router-02"; -import { QuoteFetchStrategies } from "../../_dexes/utils"; - -// For approval-based flows, we use the `UniversalSwapAndBridge` strategy with Uniswap V3's `SwapRouter02` -const quoteFetchStrategies: QuoteFetchStrategies = { - default: getSwapRouter02Strategy("UniversalSwapAndBridge"), -}; +import { getLogger, handleErrorCondition, Profiler } from "../../_utils"; +import { BaseSwapQueryParams } from "../_utils"; +import { handleApprovalSwap } from "./_service"; const handler = async ( request: TypedVercelRequest, @@ -43,116 +22,8 @@ const handler = async ( }); const mark = profiler.start("e2e endpoint runtime"); - const { - integratorId, - skipOriginTxEstimation, - isInputNative, - isOutputNative, - inputToken, - outputToken, - amount, - amountType, - refundOnOrigin, - refundAddress, - recipient, - depositor, - slippageTolerance, - refundToken, - } = await handleBaseSwapQueryParams(request.query); - - const crossSwapQuotes = await getCrossSwapQuotes( - { - amount, - inputToken, - outputToken, - depositor, - recipient: recipient || depositor, - slippageTolerance: Number(slippageTolerance), - type: amountType, - refundOnOrigin, - refundAddress, - isInputNative, - isOutputNative, - }, - quoteFetchStrategies - ); - - const crossSwapTx = await buildCrossSwapTxForAllowanceHolder( - crossSwapQuotes, - integratorId - ); - - const { originSwapQuote, bridgeQuote, destinationSwapQuote, crossSwap } = - crossSwapQuotes; - - const originChainId = crossSwap.inputToken.chainId; - const inputTokenAddress = isInputNative - ? constants.AddressZero - : crossSwap.inputToken.address; - const inputAmount = - originSwapQuote?.maximumAmountIn || bridgeQuote.inputAmount; + const responseJson = await handleApprovalSwap(request); - const { allowance, balance } = await getBalanceAndAllowance({ - chainId: originChainId, - tokenAddress: inputTokenAddress, - owner: crossSwap.depositor, - spender: crossSwapTx.to, - }); - - const isSwapTxEstimationPossible = - !skipOriginTxEstimation && - allowance.gte(inputAmount) && - balance.gte(inputAmount); - - let originTxGas: BigNumber | undefined; - let originTxGasPrice: BigNumber | undefined; - if (isSwapTxEstimationPossible) { - const provider = getProvider(originChainId); - [originTxGas, originTxGasPrice] = await Promise.all([ - provider.estimateGas({ - ...crossSwapTx, - from: crossSwap.depositor, - }), - latestGasPriceCache(originChainId).get(), - ]); - } else { - originTxGasPrice = await latestGasPriceCache(originChainId).get(); - } - - let approvalTxns: - | { - chainId: number; - to: string; - data: string; - }[] - | undefined; - // @TODO: Allow for just enough approval amount to be set. - const approvalAmount = constants.MaxUint256; - if (allowance.lt(inputAmount)) { - approvalTxns = getApprovalTxns({ - token: crossSwap.inputToken, - spender: crossSwapTx.to, - amount: approvalAmount, - }); - } - - const responseJson = buildBaseSwapResponseJson({ - originChainId, - inputTokenAddress, - inputAmount, - approvalSwapTx: { - ...crossSwapTx, - gas: originTxGas, - gasPrice: originTxGasPrice, - }, - allowance, - balance, - approvalTxns, - originSwapQuote, - bridgeQuote, - destinationSwapQuote, - refundToken, - }); mark.stop(); logger.debug({ at: "Swap/approval", diff --git a/api/swap/auth/_service.ts b/api/swap/auth/_service.ts new file mode 100644 index 000000000..49c3dc142 --- /dev/null +++ b/api/swap/auth/_service.ts @@ -0,0 +1,122 @@ +import { assert, Infer, optional, type } from "superstruct"; +import { BigNumber } from "ethers"; +import * as sdk from "@across-protocol/sdk"; + +import { TypedVercelRequest } from "../../_types"; +import { positiveIntStr } from "../../_utils"; +import { getCrossSwapQuotes } from "../../_dexes/cross-swap-service"; +import { + handleBaseSwapQueryParams, + BaseSwapQueryParams, + buildBaseSwapResponseJson, +} from "../_utils"; +import { getSwapRouter02Strategy } from "../../_dexes/uniswap/swap-router-02"; +import { InvalidParamError } from "../../_errors"; +import { QuoteFetchStrategies } from "../../_dexes/utils"; +import { buildAuthTxPayload } from "./_utils"; +import { GAS_SPONSOR_ADDRESS } from "../../relay/_utils"; +import { getBalance } from "../../_erc20"; + +export const AuthSwapQueryParamsSchema = type({ + authDeadline: optional(positiveIntStr()), +}); + +export type AuthSwapQueryParams = Infer; + +const DEFAULT_AUTH_DEADLINE = sdk.utils.getCurrentTime() + 60 * 60 * 24 * 365; // 1 year + +// For auth-based flows, we have to use the `SpokePoolPeriphery` as an entry point +const quoteFetchStrategies: QuoteFetchStrategies = { + default: getSwapRouter02Strategy("SpokePoolPeriphery"), +}; + +export async function handleAuthSwap( + request: TypedVercelRequest +) { + const { + authDeadline: _authDeadline, + authStart: _authStart, + ...restQuery + } = request.query; + assert( + { + authDeadline: _authDeadline, + }, + AuthSwapQueryParamsSchema + ); + const authDeadline = Number(_authDeadline ?? DEFAULT_AUTH_DEADLINE); + const authStart = Number(_authStart ?? sdk.utils.getCurrentTime()); + + if (authDeadline < Math.floor(Date.now() / 1000)) { + throw new InvalidParamError({ + message: "auth deadline must be a UNIX timestamp (seconds) in the future", + param: "authDeadline", + }); + } + + // `/swap` specific params validation + quote generation + const { + isInputNative, + isOutputNative, + inputToken, + outputToken, + amount, + amountType, + refundOnOrigin, + refundAddress, + recipient, + depositor, + slippageTolerance, + refundToken, + } = await handleBaseSwapQueryParams(restQuery); + + const crossSwapQuotes = await getCrossSwapQuotes( + { + amount, + inputToken, + outputToken, + depositor, + recipient: recipient || depositor, + slippageTolerance: Number(slippageTolerance), + type: amountType, + refundOnOrigin, + refundAddress, + isInputNative, + isOutputNative, + }, + quoteFetchStrategies + ); + // Build tx for auth + const crossSwapTxForAuth = await buildAuthTxPayload({ + crossSwapQuotes, + authDeadline, + authStart, + // FIXME: Calculate proper fees + submissionFees: { + amount: "0", + recipient: GAS_SPONSOR_ADDRESS, + }, + }); + + 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), + }); + + return responseJson; +} diff --git a/api/swap/auth/index.ts b/api/swap/auth/index.ts index 8af00b28b..a7a213f8c 100644 --- a/api/swap/auth/index.ts +++ b/api/swap/auth/index.ts @@ -1,38 +1,13 @@ 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"; -import { getCrossSwapQuotes } from "../../_dexes/cross-swap-service"; -import { - handleBaseSwapQueryParams, - BaseSwapQueryParams, - buildBaseSwapResponseJson, -} from "../_utils"; -import { getSwapRouter02Strategy } from "../../_dexes/uniswap/swap-router-02"; -import { InvalidParamError } from "../../_errors"; -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"; +import { getLogger, handleErrorCondition } from "../../_utils"; +import { BaseSwapQueryParams } from "../_utils"; -export const authSwapQueryParamsSchema = type({ - authDeadline: optional(positiveIntStr()), -}); - -export type authSwapQueryParams = Infer; - -const DEFAULT_AUTH_DEADLINE = sdk.utils.getCurrentTime() + 60 * 60 * 24 * 365; // 1 year - -// For auth-based flows, we have to use the `SpokePoolPeriphery` as an entry point -const quoteFetchStrategies: QuoteFetchStrategies = { - default: getSwapRouter02Strategy("SpokePoolPeriphery"), -}; +import { handleAuthSwap, AuthSwapQueryParams } from "./_service"; const handler = async ( - request: TypedVercelRequest, + request: TypedVercelRequest, response: VercelResponse ) => { const logger = getLogger(); @@ -42,92 +17,7 @@ const handler = async ( query: request.query, }); try { - // `/swap/auth` specific params validation - const { - authDeadline: _authDeadline, - authStart: _authStart, - ...restQuery - } = request.query; - assert( - { - authDeadline: _authDeadline, - }, - authSwapQueryParamsSchema - ); - const authDeadline = Number(_authDeadline ?? DEFAULT_AUTH_DEADLINE); - const authStart = Number(_authStart ?? sdk.utils.getCurrentTime()); - - if (authDeadline < Math.floor(Date.now() / 1000)) { - throw new InvalidParamError({ - message: - "auth deadline must be a UNIX timestamp (seconds) in the future", - param: "authDeadline", - }); - } - - // `/swap` specific params validation + quote generation - const { - isInputNative, - isOutputNative, - inputToken, - outputToken, - amount, - amountType, - refundOnOrigin, - refundAddress, - recipient, - depositor, - slippageTolerance, - refundToken, - } = await handleBaseSwapQueryParams(restQuery); - - const crossSwapQuotes = await getCrossSwapQuotes( - { - amount, - inputToken, - outputToken, - depositor, - recipient: recipient || depositor, - slippageTolerance: Number(slippageTolerance), - type: amountType, - refundOnOrigin, - refundAddress, - isInputNative, - isOutputNative, - }, - quoteFetchStrategies - ); - // Build tx for auth - const crossSwapTxForAuth = await buildAuthTxPayload({ - crossSwapQuotes, - authDeadline, - authStart, - // FIXME: Calculate proper fees - submissionFees: { - amount: "0", - recipient: GAS_SPONSOR_ADDRESS, - }, - }); - - 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), - }); + const responseJson = await handleAuthSwap(request); logger.debug({ at: "Swap/auth", diff --git a/api/swap/index.ts b/api/swap/index.ts index fe0edb0ff..f337a24a3 100644 --- a/api/swap/index.ts +++ b/api/swap/index.ts @@ -1,26 +1,20 @@ import { VercelResponse } from "@vercel/node"; -import axios from "axios"; import { TypedVercelRequest } from "../_types"; -import { - getLogger, - handleErrorCondition, - resolveVercelEndpoint, -} from "../_utils"; +import { getLogger, handleErrorCondition } from "../_utils"; import { handleBaseSwapQueryParams, BaseSwapQueryParams } from "./_utils"; import { getPermitArgsFromContract } from "../_permit"; import { getReceiveWithAuthArgsFromContract } from "../_transfer-with-auth"; +import { handleApprovalSwap } from "./approval/_service"; +import { handlePermitSwap } from "./permit/_service"; +import { handleAuthSwap } from "./auth/_service"; type SwapFlowType = "permit" | "transfer-with-auth" | "approval"; -function makeSwapHandler(path: string) { - return (params: unknown) => - axios.get(`${resolveVercelEndpoint(true)}/api/swap/${path}`, { params }); -} const swapFlowTypeToHandler = { - permit: makeSwapHandler("permit"), - "transfer-with-auth": makeSwapHandler("auth"), - approval: makeSwapHandler("approval"), + permit: handlePermitSwap, + "transfer-with-auth": handleAuthSwap, + approval: handleApprovalSwap, }; export default async function handler( @@ -61,10 +55,10 @@ export default async function handler( swapFlowType = "approval"; } - const handler = swapFlowTypeToHandler[swapFlowType]; - const { data } = await handler(request.query); + const handler = swapFlowTypeToHandler[swapFlowType as SwapFlowType]; + const responseJson = await handler(request); const enrichedResponseJson = { - ...data, + ...responseJson, swapFlowType, }; diff --git a/api/swap/permit/_service.ts b/api/swap/permit/_service.ts new file mode 100644 index 000000000..0b7168fe1 --- /dev/null +++ b/api/swap/permit/_service.ts @@ -0,0 +1,124 @@ +import { BigNumber } from "ethers"; +import { assert, Infer, optional, type } from "superstruct"; + +import { TypedVercelRequest } from "../../_types"; +import { getLogger, positiveIntStr } from "../../_utils"; +import { getCrossSwapQuotes } from "../../_dexes/cross-swap-service"; +import { + handleBaseSwapQueryParams, + BaseSwapQueryParams, + 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()), +}); + +export type PermitSwapQueryParams = Infer; + +const DEFAULT_PERMIT_DEADLINE = + Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365; // 1 year + +// For permit-based flows, we have to use the `SpokePoolPeriphery` as an entry point +const quoteFetchStrategies: QuoteFetchStrategies = { + default: getSwapRouter02Strategy("SpokePoolPeriphery"), +}; + +export async function handlePermitSwap( + request: TypedVercelRequest +) { + const logger = getLogger(); + logger.debug({ + at: "Swap/permit", + message: "Query data", + query: request.query, + }); + // `/swap/permit` specific params validation + const { permitDeadline: _permitDeadline, ...restQuery } = request.query; + assert( + { + permitDeadline: _permitDeadline, + }, + PermitSwapQueryParamsSchema + ); + const permitDeadline = Number(_permitDeadline ?? DEFAULT_PERMIT_DEADLINE); + + if (permitDeadline < Math.floor(Date.now() / 1000)) { + throw new InvalidParamError({ + message: + "Permit deadline must be a UNIX timestamp (seconds) in the future", + param: "permitDeadline", + }); + } + + // `/swap` specific params validation + quote generation + const { + isInputNative, + isOutputNative, + inputToken, + outputToken, + amount, + amountType, + refundOnOrigin, + refundAddress, + recipient, + depositor, + slippageTolerance, + refundToken, + } = await handleBaseSwapQueryParams(restQuery); + + const crossSwapQuotes = await getCrossSwapQuotes( + { + amount, + inputToken, + outputToken, + depositor, + recipient: recipient || depositor, + slippageTolerance: Number(slippageTolerance), + type: amountType, + refundOnOrigin, + refundAddress, + isInputNative, + isOutputNative, + }, + quoteFetchStrategies + ); + // Build tx for permit + const crossSwapTxForPermit = await buildPermitTxPayload({ + crossSwapQuotes, + permitDeadline, + // FIXME: Calculate proper fees + submissionFees: { + amount: "0", + recipient: GAS_SPONSOR_ADDRESS, + }, + }); + + 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), + }); + + return responseJson; +} diff --git a/api/swap/permit/index.ts b/api/swap/permit/index.ts index cdf04ece7..127eb3132 100644 --- a/api/swap/permit/index.ts +++ b/api/swap/permit/index.ts @@ -1,21 +1,10 @@ import { VercelResponse } from "@vercel/node"; -import { BigNumber } from "ethers"; -import { assert, Infer, optional, type } from "superstruct"; +import { Infer, optional, type } from "superstruct"; import { TypedVercelRequest } from "../../_types"; import { getLogger, handleErrorCondition, positiveIntStr } from "../../_utils"; -import { getCrossSwapQuotes } from "../../_dexes/cross-swap-service"; -import { - handleBaseSwapQueryParams, - BaseSwapQueryParams, - 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"; +import { BaseSwapQueryParams } from "../_utils"; +import { handlePermitSwap } from "./_service"; export const PermitSwapQueryParamsSchema = type({ permitDeadline: optional(positiveIntStr()), @@ -23,14 +12,6 @@ export const PermitSwapQueryParamsSchema = type({ export type PermitSwapQueryParams = Infer; -const DEFAULT_PERMIT_DEADLINE = - Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365; // 1 year - -// For permit-based flows, we have to use the `SpokePoolPeriphery` as an entry point -const quoteFetchStrategies: QuoteFetchStrategies = { - default: getSwapRouter02Strategy("SpokePoolPeriphery"), -}; - const handler = async ( request: TypedVercelRequest, response: VercelResponse @@ -42,86 +23,7 @@ const handler = async ( query: request.query, }); try { - // `/swap/permit` specific params validation - const { permitDeadline: _permitDeadline, ...restQuery } = request.query; - assert( - { - permitDeadline: _permitDeadline, - }, - PermitSwapQueryParamsSchema - ); - const permitDeadline = Number(_permitDeadline ?? DEFAULT_PERMIT_DEADLINE); - - if (permitDeadline < Math.floor(Date.now() / 1000)) { - throw new InvalidParamError({ - message: - "Permit deadline must be a UNIX timestamp (seconds) in the future", - param: "permitDeadline", - }); - } - - // `/swap` specific params validation + quote generation - const { - isInputNative, - isOutputNative, - inputToken, - outputToken, - amount, - amountType, - refundOnOrigin, - refundAddress, - recipient, - depositor, - slippageTolerance, - refundToken, - } = await handleBaseSwapQueryParams(restQuery); - - const crossSwapQuotes = await getCrossSwapQuotes( - { - amount, - inputToken, - outputToken, - depositor, - recipient: recipient || depositor, - slippageTolerance: Number(slippageTolerance), - type: amountType, - refundOnOrigin, - refundAddress, - isInputNative, - isOutputNative, - }, - quoteFetchStrategies - ); - // Build tx for permit - const crossSwapTxForPermit = await buildPermitTxPayload({ - crossSwapQuotes, - permitDeadline, - // FIXME: Calculate proper fees - submissionFees: { - amount: "0", - recipient: GAS_SPONSOR_ADDRESS, - }, - }); - - 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), - }); + const responseJson = await handlePermitSwap(request); logger.debug({ at: "Swap/permit", diff --git a/scripts/tests/_swap-utils.ts b/scripts/tests/_swap-utils.ts index d5fcdaea1..88a2e4456 100644 --- a/scripts/tests/_swap-utils.ts +++ b/scripts/tests/_swap-utils.ts @@ -269,7 +269,6 @@ export async function signAndWaitAllowanceFlow(params: { 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();