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/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 84e17a905..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; @@ -5,22 +11,30 @@ 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: 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"; -export type SwapQuoteAndCalldata = { +export type OriginSwapQuoteAndCalldata = { minExpectedInputTokenAmount: string; routerCalldata: string; value: string; @@ -28,3 +42,33 @@ export type SwapQuoteAndCalldata = { 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 d693dc5a4..cf2fc2c53 100644 --- a/api/_dexes/uniswap.ts +++ b/api/_dexes/uniswap.ts @@ -1,49 +1,48 @@ -import { ethers } from "ethers"; +import { BigNumber, 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, + getRouteByInputTokenAndDestinationChain, + getTokenByAddress, + getBridgeQuoteForMinOutput, + getRoutesByChainIds, + getRouteByOutputTokenAndOriginChain, +} from "../_utils"; import { TOKEN_SYMBOLS_MAP } from "../_constants"; import { - AcrossSwap, - SwapQuoteAndCalldata, + buildMulticallHandlerMessage, + getMultiCallHandlerAddress, +} from "../_multicall-handler"; +import { + OriginSwapQuoteAndCalldata, Token as AcrossToken, + Swap, + CrossSwap, + SwapQuote, + CrossSwapQuotes, } 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"; +import { AMOUNT_TYPE } from "./cross-swap"; + +// 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,189 +54,520 @@ 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 - ); +/** + * 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 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 + swap.tokenIn = getMainnetToken(swap.tokenIn); + swap.tokenOut = getMainnetToken(swap.tokenOut); + + const { swapTx, minAmountOut } = await getUniswapQuote({ + ...swap, + recipient: swapAndBridgeAddress, + }); + + // 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) ); - const tokenB = new Token( - swap.acrossInputToken.chainId, - swap.acrossInputToken.address, - swap.acrossInputToken.decimals + swapTx.data = swapTx.data.replace( + swap.tokenOut.address.toLowerCase().slice(2), + initialTokenOut.address.toLowerCase().slice(2) ); - const pool = new Pool( - tokenA, - tokenB, - FeeAmount.LOW, - poolInfo.sqrtPriceX96.toString(), - poolInfo.liquidity.toString(), - poolInfo.tick + + 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: + * 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 ); - const swapRoute = new Route([pool], tokenA, tokenB); + if (!bridgeRoute) { + throw new Error( + `No bridge route found for input token ${crossSwap.inputToken.symbol} ` + + `${crossSwap.inputToken.chainId} -> ${destinationSwapChainId}` + ); + } - const amountOut = await getOutputQuote(swap, swapRoute); + 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 uncheckedTrade = Trade.createUncheckedTrade({ - route: swapRoute, - inputAmount: CurrencyAmount.fromRawAmount(tokenA, swap.swapTokenAmount), - outputAmount: CurrencyAmount.fromRawAmount(tokenB, JSBI.BigInt(amountOut)), - tradeType: TradeType.EXACT_INPUT, + 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, }); - 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, - recipient: swapAndBridgeAddress, + // 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, }; +} - const methodParameters = SwapRouter.swapCallParameters( - [uncheckedTrade], - options +/** + * 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 ); - // 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) - ); - methodParameters.calldata = methodParameters.calldata.replace( - swap.acrossInputToken.address.toLowerCase().slice(2), - initialAcrossInputToken.address.toLowerCase().slice(2) + 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 { - minExpectedInputTokenAmount: ethers.utils - .parseUnits( - uncheckedTrade.minimumAmountOut(options.slippageTolerance).toExact(), - swap.acrossInputToken.decimals - ) - .toString(), - routerCalldata: methodParameters.calldata, - value: ethers.BigNumber.from(methodParameters.value).toString(), - swapAndBridgeAddress, - dex: "uniswap", - slippage: swap.slippage, + crossSwap, + bridgeQuote, + destinationSwapQuote: undefined, + originSwapQuote, }; } -async function getOutputQuote( - swap: AcrossSwap, - route: Route +/** + * 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 provider = getProvider(route.chainId); + const originSwapChainId = crossSwap.inputToken.chainId; + const destinationSwapChainId = crossSwap.outputToken.chainId; + const allBridgeRoutes = getRoutesByChainIds( + originSwapChainId, + destinationSwapChainId + ); - const { calldata } = SwapQuoter.quoteCallParameters( - route, - CurrencyAmount.fromRawAmount( - new Token( - swap.swapToken.chainId, - swap.swapToken.address, - swap.swapToken.decimals - ), - swap.swapTokenAmount - ), - TradeType.EXACT_INPUT, - { useQuoterV2: true } + 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 ); - const quoteCallReturnData = await provider.call({ - to: QUOTER_CONTRACT_ADDRESS[swap.swapToken.chainId], - data: calldata, + 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, }); - return ethers.utils.defaultAbiCoder.decode(["uint256"], quoteCallReturnData); + // 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, + }; } -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 +export async function getUniswapQuote(swap: Swap): Promise { + const { router, options } = getSwapRouterAndOptions(swap); + + const amountCurrency = + swap.type === AMOUNT_TYPE.EXACT_INPUT ? swap.tokenIn : swap.tokenOut; + const quoteCurrency = + swap.type === AMOUNT_TYPE.EXACT_INPUT ? swap.tokenOut : swap.tokenIn; + + const route = await router.route( + CurrencyAmount.fromRawAmount( + new Token( + amountCurrency.chainId, + amountCurrency.address, + amountCurrency.decimals ), - fee: FeeAmount.LOW, - }), - IUniswapV3PoolABI.abi, - provider + swap.amount + ), + new Token( + quoteCurrency.chainId, + quoteCurrency.address, + quoteCurrency.decimals + ), + swap.type === AMOUNT_TYPE.EXACT_INPUT + ? TradeType.EXACT_INPUT + : TradeType.EXACT_OUTPUT, + options ); - 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, - })) - ); + if (!route || !route.methodParameters) { + throw new NoSwapRouteError({ + dex: "uniswap", + tokenInSymbol: swap.tokenIn.symbol, + tokenOutSymbol: swap.tokenOut.symbol, + chainId: swap.chainId, + swapType: swap.type, + }); + } + 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: { + 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: floatToPercent(params.slippageTolerance), + }; 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, }; } +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 9edde40b9..06cb97ece 100644 --- a/api/_dexes/utils.ts +++ b/api/_dexes/utils.ts @@ -1,4 +1,6 @@ -import { ENABLED_ROUTES } 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) { @@ -12,6 +14,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 +46,15 @@ export function getSwapAndBridgeAddress(dex: string, chainId: number) { return address; } +export function getSwapAndBridge(dex: string, chainId: number) { + const swapAndBridgeAddress = getSwapAndBridgeAddress(dex, chainId); + + return UniversalSwapAndBridge__factory.connect( + swapAndBridgeAddress, + getProvider(chainId) + ); +} + function _isDexSupported( dex: string ): dex is keyof typeof ENABLED_ROUTES.swapAndBridgeAddresses { 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/_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..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,7 +64,9 @@ import { MissingParamError, InvalidParamError, RouteNotEnabledError, + TokenNotFoundError, } from "./_errors"; +import { Token } from "./_dexes/types"; export { InputError, handleErrorCondition } from "./_errors"; @@ -806,6 +808,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 +1062,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. @@ -1992,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, + }, + }); + } +} diff --git a/api/swap-quote.ts b/api/swap-quote.ts index d8d626c0d..56c54d472 100644 --- a/api/swap-quote.ts +++ b/api/swap-quote.ts @@ -13,9 +13,10 @@ 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"; +import { AMOUNT_TYPE } from "./_dexes/cross-swap"; const SwapQuoteQueryParamsSchema = type({ swapToken: validAddress(), @@ -97,18 +98,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: AMOUNT_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/api/swap.ts b/api/swap.ts new file mode 100644 index 000000000..2dc0cc8a1 --- /dev/null +++ b/api/swap.ts @@ -0,0 +1,156 @@ +import { VercelResponse } from "@vercel/node"; +import { assert, Infer, type, string, optional } from "superstruct"; +import { BigNumber } from "ethers"; + +import { TypedVercelRequest } from "./_types"; +import { + getLogger, + handleErrorCondition, + positiveFloatStr, + 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: optional(positiveIntStr()), + exactInputAmount: optional(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); + + const { + inputToken: _inputTokenAddress, + outputToken: _outputTokenAddress, + exactInputAmount: _exactInputAmount, + minOutputAmount: _minOutputAmount, + originChainId: _originChainId, + destinationChainId: _destinationChainId, + recipient, + integratorId, + refundAddress, + 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 + // - 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. Build tx and return + // @TODO + + logger.debug({ + at: "Swap", + message: "Response data", + responseJson: crossSwapQuotes, + }); + response.status(200).json(crossSwapQuotes); + } catch (error: unknown) { + return handleErrorCondition("swap", response, logger, error); + } +}; + +export default handler; 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/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); 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"