From 7d3a3cc3531b9a5d75ae0093e99e7dc7f4a9d749 Mon Sep 17 00:00:00 2001 From: Louis-Amas Date: Tue, 18 Oct 2022 11:57:09 +0200 Subject: [PATCH] feat: add generic rfq implementation Resolves BACK-760. --- package.json | 3 + src/config.ts | 46 ++- src/constants.ts | 3 +- src/dex-helper/dummy-dex-helper.ts | 5 + src/dex-helper/irequest-wrapper.ts | 44 ++ src/dex/generic-rfq/example-api.test.ts | 63 +++ src/dex/generic-rfq/generic-rfq-e2e.test.ts | 38 ++ src/dex/generic-rfq/generic-rfq.ts | 213 ++++++++++ src/dex/generic-rfq/rate-fetcher.ts | 377 ++++++++++++++++++ src/dex/generic-rfq/types.ts | 74 ++++ src/dex/idex.ts | 5 +- src/dex/index.ts | 11 + .../paraswap-limit-orders.ts | 9 +- src/lib/fetcher/fetcher.ts | 138 +++++++ src/types.ts | 4 + yarn.lock | 227 ++++++++++- 16 files changed, 1251 insertions(+), 9 deletions(-) create mode 100644 src/dex/generic-rfq/example-api.test.ts create mode 100644 src/dex/generic-rfq/generic-rfq-e2e.test.ts create mode 100644 src/dex/generic-rfq/generic-rfq.ts create mode 100644 src/dex/generic-rfq/rate-fetcher.ts create mode 100644 src/dex/generic-rfq/types.ts create mode 100644 src/lib/fetcher/fetcher.ts diff --git a/package.json b/package.json index fc5af8c7d..76395f54d 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,16 @@ "access": "public" }, "devDependencies": { + "@paraswap/sdk": "^6.0.0", "@types/axios": "0.14.0", + "@types/express": "^4.17.14", "@types/jest": "26.0.24", "@types/lodash": "4.14.178", "@types/node": "^17.0.21", "axios": "0.26.0", "change-case": "^4.1.2", "dotenv": "16.0.0", + "express": "^4.18.2", "husky": "7.0.1", "jest": "^29.0.3", "paraswap-core": "1.0.2", diff --git a/src/config.ts b/src/config.ts index dda1532ca..3307d5a13 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,7 +1,8 @@ import { Config, Address, Token } from './types'; -import { Network } from './constants'; +import { Network, PORT_TEST_SERVER } from './constants'; import { isETHAddress } from './utils'; import { ETHER_ADDRESS } from 'paraswap'; +import { RFQConfig } from './dex/generic-rfq/types'; // Hardcoded and environment values from which actual config is derived type BaseConfig = { @@ -16,11 +17,13 @@ type BaseConfig = { wrappedNativeTokenAddress: Address; hasEIP1559: boolean; augustusAddress: Address; + augustusRFQAddress: Address; tokenTransferProxyAddress: Address; multicallV2Address: Address; privateHttpProvider?: string; adapterAddresses: { [name: string]: Address }; uniswapV2ExchangeRouterAddress: Address; + rfqConfigs: Record; }; const baseConfigs: { [network: number]: BaseConfig } = { @@ -33,6 +36,7 @@ const baseConfigs: { [network: number]: BaseConfig } = { wrappedNativeTokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', hasEIP1559: true, augustusAddress: '0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57', + augustusRFQAddress: '0xe92b586627ccA7a83dC919cc7127196d70f55a06', tokenTransferProxyAddress: '0x216b4b4ba9f3e719726886d34a177484278bfcae', multicallV2Address: '0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696', privateHttpProvider: process.env.HTTP_PROVIDER, @@ -44,6 +48,30 @@ const baseConfigs: { [network: number]: BaseConfig } = { }, uniswapV2ExchangeRouterAddress: '0xF9234CB08edb93c0d4a4d4c70cC3FfD070e78e07', + rfqConfigs: { + DummyParaSwapPool: { + marketConfig: { + reqParams: { + url: `http://localhost:${PORT_TEST_SERVER}/markets`, + method: 'GET', + }, + intervalMs: 1000 * 60 * 60 * 10, // every 10 minutes + dataTTLS: 1000 * 60 * 60 * 11, // tll 11 minutes + }, + rateConfig: { + reqParams: { + url: `http://localhost:${PORT_TEST_SERVER}/prices`, + method: 'GET', + }, + intervalMs: 1000 * 60 * 60 * 1, // every 1 minute + dataTTLS: 1000 * 60 * 60 * 1, // tll 1 minute + }, + firmRateConfig: { + url: `http://localhost:${PORT_TEST_SERVER}/firm`, + method: 'POST', + }, + }, + }, }, [Network.ROPSTEN]: { network: Network.ROPSTEN, @@ -55,6 +83,7 @@ const baseConfigs: { [network: number]: BaseConfig } = { wrappedNativeTokenAddress: '0xc778417E063141139Fce010982780140Aa0cD5Ab', hasEIP1559: true, augustusAddress: '0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57', + augustusRFQAddress: '0x34268C38fcbC798814b058656bC0156C7511c0E4', tokenTransferProxyAddress: '0x216b4b4ba9f3e719726886d34a177484278bfcae', multicallV2Address: '0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696', privateHttpProvider: process.env.HTTP_PROVIDER_3, @@ -64,6 +93,7 @@ const baseConfigs: { [network: number]: BaseConfig } = { }, uniswapV2ExchangeRouterAddress: '0x53e693c6C7FFC4446c53B205Cf513105Bf140D7b', + rfqConfigs: {}, }, [Network.BSC]: { network: Network.BSC, @@ -73,6 +103,7 @@ const baseConfigs: { [network: number]: BaseConfig } = { wrappedNativeTokenAddress: '0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c', hasEIP1559: false, augustusAddress: '0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57', + augustusRFQAddress: '0x8DcDfe88EF0351f27437284D0710cD65b20288bb', tokenTransferProxyAddress: '0x216b4b4ba9f3e719726886d34a177484278bfcae', multicallV2Address: '0xC50F4c1E81c873B2204D7eFf7069Ffec6Fbe136D', privateHttpProvider: process.env.HTTP_PROVIDER_56, @@ -83,6 +114,7 @@ const baseConfigs: { [network: number]: BaseConfig } = { }, uniswapV2ExchangeRouterAddress: '0x53e693c6C7FFC4446c53B205Cf513105Bf140D7b', + rfqConfigs: {}, }, [Network.POLYGON]: { network: Network.POLYGON, @@ -93,6 +125,7 @@ const baseConfigs: { [network: number]: BaseConfig } = { wrappedNativeTokenAddress: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', hasEIP1559: true, augustusAddress: '0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57', + augustusRFQAddress: '0xF3CD476C3C4D3Ac5cA2724767f269070CA09A043', tokenTransferProxyAddress: '0x216b4b4ba9f3e719726886d34a177484278bfcae', multicallV2Address: '0x275617327c958bD06b5D6b871E7f491D76113dd8', privateHttpProvider: process.env.HTTP_PROVIDER_137, @@ -103,6 +136,7 @@ const baseConfigs: { [network: number]: BaseConfig } = { }, uniswapV2ExchangeRouterAddress: '0xf3938337F7294fEf84e9B2c6D548A93F956Cc281', + rfqConfigs: {}, }, [Network.AVALANCHE]: { network: Network.AVALANCHE, @@ -113,6 +147,7 @@ const baseConfigs: { [network: number]: BaseConfig } = { wrappedNativeTokenAddress: '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7', hasEIP1559: true, augustusAddress: '0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57', + augustusRFQAddress: '0x34302c4267d0dA0A8c65510282Cc22E9e39df51f', tokenTransferProxyAddress: '0x216b4b4ba9f3e719726886d34a177484278bfcae', multicallV2Address: '0xd7Fc8aD069f95B6e2835f4DEff03eF84241cF0E1', privateHttpProvider: process.env.HTTP_PROVIDER_43114, @@ -122,6 +157,7 @@ const baseConfigs: { [network: number]: BaseConfig } = { }, uniswapV2ExchangeRouterAddress: '0x53e693c6C7FFC4446c53B205Cf513105Bf140D7b', + rfqConfigs: {}, }, [Network.FANTOM]: { network: Network.FANTOM, @@ -132,6 +168,7 @@ const baseConfigs: { [network: number]: BaseConfig } = { wrappedNativeTokenAddress: '0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83', hasEIP1559: false, augustusAddress: '0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57', + augustusRFQAddress: '0x2DF17455B96Dde3618FD6B1C3a9AA06D6aB89347', tokenTransferProxyAddress: '0x216b4b4ba9f3e719726886d34a177484278bfcae', multicallV2Address: '0xdC6E2b14260F972ad4e5a31c68294Fba7E720701', privateHttpProvider: process.env.HTTP_PROVIDER_250, @@ -141,6 +178,7 @@ const baseConfigs: { [network: number]: BaseConfig } = { }, uniswapV2ExchangeRouterAddress: '0xAB86e2bC9ec5485a9b60E684BA6d49bf4686ACC2', + rfqConfigs: {}, }, [Network.ARBITRUM]: { network: Network.ARBITRUM, @@ -151,6 +189,7 @@ const baseConfigs: { [network: number]: BaseConfig } = { wrappedNativeTokenAddress: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', hasEIP1559: false, augustusAddress: '0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57', + augustusRFQAddress: '0x0927FD43a7a87E3E8b81Df2c44B03C4756849F6D', tokenTransferProxyAddress: '0x216b4b4ba9f3e719726886d34a177484278bfcae', multicallV2Address: '0x7eCfBaa8742fDf5756DAC92fbc8b90a19b8815bF', privateHttpProvider: process.env.HTTP_PROVIDER_42161, @@ -160,6 +199,7 @@ const baseConfigs: { [network: number]: BaseConfig } = { }, uniswapV2ExchangeRouterAddress: '0xB41dD984730dAf82f5C41489E21ac79D5e3B61bC', + rfqConfigs: {}, }, [Network.OPTIMISM]: { network: Network.OPTIMISM, @@ -170,6 +210,7 @@ const baseConfigs: { [network: number]: BaseConfig } = { wrappedNativeTokenAddress: '0x4200000000000000000000000000000000000006', hasEIP1559: false, augustusAddress: '0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57', + augustusRFQAddress: '0x0927FD43a7a87E3E8b81Df2c44B03C4756849F6D', tokenTransferProxyAddress: '0x216b4b4ba9f3e719726886d34a177484278bfcae', multicallV2Address: '0x2DC0E2aa608532Da689e89e237dF582B783E552C', privateHttpProvider: process.env.HTTP_PROVIDER_10, @@ -179,6 +220,7 @@ const baseConfigs: { [network: number]: BaseConfig } = { }, uniswapV2ExchangeRouterAddress: '0xB41dD984730dAf82f5C41489E21ac79D5e3B61bC', + rfqConfigs: {}, }, }; @@ -207,11 +249,13 @@ export function generateConfig(network: number): Config { wrappedNativeTokenAddress: baseConfig.wrappedNativeTokenAddress, hasEIP1559: baseConfig.hasEIP1559, augustusAddress: baseConfig.augustusAddress, + augustusRFQAddress: baseConfig.augustusRFQAddress, tokenTransferProxyAddress: baseConfig.tokenTransferProxyAddress, multicallV2Address: baseConfig.multicallV2Address, privateHttpProvider: baseConfig.privateHttpProvider, adapterAddresses: { ...baseConfig.adapterAddresses }, uniswapV2ExchangeRouterAddress: baseConfig.uniswapV2ExchangeRouterAddress, + rfqConfigs: {}, }; } diff --git a/src/constants.ts b/src/constants.ts index dd4f0950c..941a39ec0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,6 +1,7 @@ -import { Address } from './types'; export { SwapSide, ContractMethod } from 'paraswap-core'; +export const PORT_TEST_SERVER = 4444; + export const ETHER_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; diff --git a/src/dex-helper/dummy-dex-helper.ts b/src/dex-helper/dummy-dex-helper.ts index 7906e20b3..c9cef45cb 100644 --- a/src/dex-helper/dummy-dex-helper.ts +++ b/src/dex-helper/dummy-dex-helper.ts @@ -15,6 +15,7 @@ import Web3 from 'web3'; import { Contract } from 'web3-eth-contract'; import { generateConfig, ConfigHelper } from '../config'; import { MultiWrapper } from '../lib/multi-wrapper'; +import { Response, RequestConfig } from './irequest-wrapper'; // This is a dummy cache for testing purposes class DummyCache implements ICache { @@ -96,6 +97,10 @@ class DummyRequestWrapper implements IRequestWrapper { }); return axiosResult.data; } + + request>(config: RequestConfig): Promise { + return axios.request(config); + } } class DummyBlockManager implements IBlockManager { diff --git a/src/dex-helper/irequest-wrapper.ts b/src/dex-helper/irequest-wrapper.ts index edb07943d..1c846800f 100644 --- a/src/dex-helper/irequest-wrapper.ts +++ b/src/dex-helper/irequest-wrapper.ts @@ -1,3 +1,45 @@ +export type Method = + | 'get' + | 'GET' + | 'delete' + | 'DELETE' + | 'head' + | 'HEAD' + | 'options' + | 'OPTIONS' + | 'post' + | 'POST' + | 'put' + | 'PUT' + | 'patch' + | 'PATCH' + | 'purge' + | 'PURGE' + | 'link' + | 'LINK' + | 'unlink' + | 'UNLINK'; + +export type RequestHeaders = Record; + +export interface RequestConfig { + url?: string; + method?: Method; + headers?: RequestHeaders; + params?: any; + paramsSerializer?: (params: any) => string; + data?: D; + timeout?: number; +} + +export interface Response { + data: T; + status: number; + statusText: string; + headers: Record; + request?: any; +} + export interface IRequestWrapper { get( url: string, @@ -11,4 +53,6 @@ export interface IRequestWrapper { timeout?: number, headers?: { [key: string]: string | number }, ): Promise; + + request>(config: RequestConfig): Promise; } diff --git a/src/dex/generic-rfq/example-api.test.ts b/src/dex/generic-rfq/example-api.test.ts new file mode 100644 index 000000000..9bbf04e09 --- /dev/null +++ b/src/dex/generic-rfq/example-api.test.ts @@ -0,0 +1,63 @@ +import express from 'express'; + +const app = express(); + +const markets = { + id: 'WETH-DAI', + base: { + address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + decimals: 18, + type: 'erc20', + }, + quote: { + address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + decimals: 18, + type: 'erc20', + }, + status: 'available', +}; + +const prices = { + 'WETH-DAI': { + buyAmounts: [ + '1.166200000000000000', + '1.166200000000000000', + '1.166200000000000000', + '1.166200000000000000', + '1.166200000000000000', + '1.169000000000000000', + ], + buyPrices: [ + '1333.425240000000000000', + '1333.024812000000000000', + '1332.624384000000000000', + '1332.223956000000000000', + '1331.823528000000000000', + '1331.423100000000000000', + ], + sellAmounts: [ + '1.166200000000000000', + '1.166200000000000000', + '1.166200000000000000', + '1.166200000000000000', + '1.166200000000000000', + '1.169000000000000000', + ], + sellPrices: [ + '1336.745410000000000000', + '1337.146033000000000000', + '1337.546656000000000000', + '1337.947279000000000000', + '1338.347902000000000000', + '1338.748525000000000000', + ], + }, +}; + +app.get('/markets', (req, res) => { + return res.status(200).json(markets); +}); + +app.get('/prices', (req, res) => { + return res.status(200).json(prices); +}); diff --git a/src/dex/generic-rfq/generic-rfq-e2e.test.ts b/src/dex/generic-rfq/generic-rfq-e2e.test.ts new file mode 100644 index 000000000..2b283e7f4 --- /dev/null +++ b/src/dex/generic-rfq/generic-rfq-e2e.test.ts @@ -0,0 +1,38 @@ +import dotenv from 'dotenv'; +dotenv.config(); + +import { Network, ContractMethod, SwapSide } from '../../constants'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { generateConfig } from '../../config'; +import { testE2E } from '../../../tests/utils-e2e'; +import { Tokens, Holders } from '../../../tests/constants-e2e'; + +describe('GenericRFQ E2E Mainnet', () => { + const network = Network.MAINNET; + const tokens = Tokens[network]; + const holders = Holders[network]; + const provider = new StaticJsonRpcProvider( + generateConfig(network).privateHttpProvider, + network, + ); + + describe('GenericRFQ', () => { + const dexKey = 'GenericRFQ'; + + describe('Simpleswap', () => { + it('ETH -> TOKEN', async () => { + await testE2E( + tokens.ETH, + tokens.USDC, + holders.ETH, + '7000000000000000000', + SwapSide.SELL, + dexKey, + ContractMethod.simpleSwap, + network, + provider, + ); + }); + }); + }); +}); diff --git a/src/dex/generic-rfq/generic-rfq.ts b/src/dex/generic-rfq/generic-rfq.ts new file mode 100644 index 000000000..18769b57f --- /dev/null +++ b/src/dex/generic-rfq/generic-rfq.ts @@ -0,0 +1,213 @@ +import BigNumber from 'bignumber.js'; +import { + Token, + ExchangePrices, + ExchangeTxInfo, + PreprocessTransactionOptions, + Config, +} from '../../types'; +import { Network, SwapSide } from '../../constants'; +import { IDexHelper } from '../../dex-helper'; +import { ParaSwapLimitOrders } from '../paraswap-limit-orders/paraswap-limit-orders'; +import { BN_0, BN_1, getBigNumberPow } from '../../bignumber-constants'; +import { ParaSwapLimitOrdersData } from '../paraswap-limit-orders/types'; +import { ONE_ORDER_GASCOST } from '../paraswap-limit-orders/constant'; +import { RateFetcher } from './rate-fetcher'; +import { RFQConfig } from './types'; +import { OptimalSwapExchange } from 'paraswap-core'; +import { BI_MAX_UINT256 } from '../../bigint-constants'; + +type OutputsResults = { + outputs: bigint[]; + ordersCount: number; +}; + +export class GenericRFQ extends ParaSwapLimitOrders { + private rateFetcher: RateFetcher; + + public static dexKeysWithNetwork: { key: string; networks: Network[] }[] = []; + + static builderDexKeysWithNetwork(config: Config): void { + Object.keys(config.rfqConfigs).forEach(rfqName => + this.dexKeysWithNetwork.push({ + key: rfqName, + networks: [config.network], + }), + ); + } + + constructor( + protected network: Network, + protected dexKey: string, + protected dexHelper: IDexHelper, + config: RFQConfig, + ) { + super(network, dexKey, dexHelper); + this.rateFetcher = new RateFetcher(dexHelper, config, dexKey, this.logger); + } + + initializePricing(blockNumber: number): void { + //TODO: only master version + this.rateFetcher.start(); + return; + } + + async getPoolIdentifiers( + srcToken: Token, + destToken: Token, + side: SwapSide, + blockNumber: number, + ): Promise { + const _srcToken = this.dexHelper.config.wrapETH(srcToken); + const _destToken = this.dexHelper.config.wrapETH(destToken); + return [`${this.dexKey}_${_srcToken.address}_${_destToken}`.toLowerCase()]; + } + + calcOutsFromAmounts( + amounts: BigNumber[], + outMultiplier: BigNumber, + amountsWithRates: [BigNumber, BigNumber][], + ): OutputsResults { + let lastOrderIndex = 0; + let lastTotalSrcAmount = BN_0; + let lastTotalDestAmount = BN_0; + const outputs = new Array(amounts.length).fill(BN_0); + for (const [i, amount] of amounts.entries()) { + if (amount.isZero()) { + outputs[i] = BN_0; + } else { + let srcAmountLeft = amount.minus(lastTotalSrcAmount); + let destAmountFilled = lastTotalDestAmount; + while (lastOrderIndex < amountsWithRates.length) { + const [srcAmount, rate] = amountsWithRates[lastOrderIndex]; + if (srcAmountLeft.gt(srcAmount)) { + const destAmount = srcAmount.multipliedBy(rate); + + srcAmountLeft = srcAmountLeft.minus(srcAmount); + destAmountFilled = destAmountFilled.plus(destAmount); + + lastTotalSrcAmount = lastTotalSrcAmount.plus(srcAmount); + lastTotalDestAmount = lastTotalDestAmount.plus(destAmount); + lastOrderIndex++; + } else { + destAmountFilled = destAmountFilled.plus( + srcAmountLeft.multipliedBy(rate), + ); + srcAmountLeft = BN_0; + break; + } + } + if (srcAmountLeft.isZero()) { + outputs[i] = destAmountFilled; + } else { + // If current amount was unfillable, then bigger amounts are unfillable as well + break; + } + } + } + + return { + outputs: outputs.map(o => + BigInt(o.multipliedBy(outMultiplier).toFixed(0)), + ), + ordersCount: lastOrderIndex, + }; + } + + async getPricesVolume( + srcToken: Token, + destToken: Token, + amounts: bigint[], + side: SwapSide, + blockNumber: number, + limitPools?: string[], + ): Promise | null> { + const _srcToken = this.dexHelper.config.wrapETH(srcToken); + const _destToken = this.dexHelper.config.wrapETH(destToken); + + const _srcAddress = _srcToken.address.toLowerCase(); + const _destAddress = _destToken.address.toLowerCase(); + if (_srcAddress === _destAddress) return null; + + const expectedIdentifier = this.getIdentifier(_srcAddress, _destAddress); + + const orderPricesInfo = await this.rateFetcher.getOrderPrice( + _srcToken, + _destToken, + side, + ); + if (!orderPricesInfo) { + return null; + } + + const inDecimals = + side === SwapSide.SELL ? _srcToken.decimals : _destToken.decimals; + const outDecimals = + side === SwapSide.SELL ? _destToken.decimals : _srcToken.decimals; + + const _amountsInBN = amounts.map(a => + new BigNumber(a.toString()).dividedBy(getBigNumberPow(inDecimals)), + ); + + const unitVolume = BN_1; + + const unitResults = this.calcOutsFromAmounts( + [unitVolume], + getBigNumberPow(outDecimals), + orderPricesInfo.rates, + ); + + const unit = unitResults.outputs[0]; + + const outputs = this.calcOutsFromAmounts( + _amountsInBN, + getBigNumberPow(outDecimals), + orderPricesInfo.rates, + ); + return [ + { + gasCost: Number(BigInt(outputs.ordersCount) * ONE_ORDER_GASCOST), + exchange: this.dexKey, + poolIdentifier: expectedIdentifier, + prices: outputs.outputs, + unit, + data: { + orderInfos: null, + }, + poolAddresses: [this.augustusRFQAddress], + }, + ]; + } + + async preProcessTransaction?( + optimalSwapExchange: OptimalSwapExchange, + srcToken: Token, + destToken: Token, + side: SwapSide, + options: PreprocessTransactionOptions, + ): Promise<[OptimalSwapExchange, ExchangeTxInfo]> { + const isSell = side === SwapSide.SELL; + const order = await this.rateFetcher.getFirmRate( + srcToken, + destToken, + isSell ? optimalSwapExchange.srcAmount : optimalSwapExchange.destAmount, + side, + options.txOrigin, + ); + + const expiryAsBigInt = BigInt(order.order.expiry); + const minDeadline = expiryAsBigInt > 0 ? expiryAsBigInt : BI_MAX_UINT256; + + return [ + { + ...optimalSwapExchange, + data: { + orderInfos: [order], + }, + }, + { + deadline: minDeadline, + }, + ]; + } +} diff --git a/src/dex/generic-rfq/rate-fetcher.ts b/src/dex/generic-rfq/rate-fetcher.ts new file mode 100644 index 000000000..7b3ccb7e9 --- /dev/null +++ b/src/dex/generic-rfq/rate-fetcher.ts @@ -0,0 +1,377 @@ +import BigNumber from 'bignumber.js'; +import { SwapSide } from 'paraswap-core'; +import { BN_0, BN_1, getBigNumberPow } from '../../bignumber-constants'; +import { IDexHelper } from '../../dex-helper'; +import Fetcher from '../../lib/fetcher/fetcher'; +import { Logger, Address, Token } from '../../types'; +import { OrderInfo } from '../paraswap-limit-orders/types'; +import { + BigNumberRate, + BigNumberRates, + MarketResponse, + OrderPriceInfo, + PairMap, + Rates, + RatesResponse, + RFQConfig, + RFQFirmRateResponse, + RFQPayload, +} from './types'; + +export const FIRM_RATE_TIMEOUT_MS = 500; + +function onlyUnique(value: string, index: number, self: string[]) { + return self.indexOf(value) === index; +} +const reversePrice = ([amount, p]: BigNumberRate) => + [amount.times(p), BN_1.dividedBy(p)] as BigNumberRate; + +export class RateFetcher { + private augustusAddress: Address; + + private marketFetcher: Fetcher; + private rateFetcher: Fetcher; + + private pairs: PairMap = {}; + + constructor( + private dexHelper: IDexHelper, + private config: RFQConfig, + private dexKey: string, + private logger: Logger, + ) { + (this.augustusAddress = + dexHelper.config.data.augustusAddress.toLowerCase()), + (this.marketFetcher = new Fetcher( + dexHelper.httpRequest, + { + info: { + requestOptions: config.marketConfig.reqParams, + caster: this.castMarketResponse.bind(this), + }, + handler: this.handleMarketResponse, + }, + config.marketConfig.intervalMs, + this.logger, + )); + + this.rateFetcher = new Fetcher( + dexHelper.httpRequest, + { + info: { + requestOptions: config.rateConfig.reqParams, + caster: this.castRateResponse.bind(this), + }, + handler: this.handleRatesResponse, + }, + config.rateConfig.intervalMs, + logger, + ); + } + + private castMarketResponse(data: unknown): MarketResponse | null { + if (!data || !(data as MarketResponse).markets) { + return null; + } + + return data as MarketResponse; + } + + private castRateResponse(data: unknown): RatesResponse | null { + if (!data) { + return null; + } + + return data as RatesResponse; + } + + start() { + this.marketFetcher.startPolling(); + this.rateFetcher.startPolling(); + } + + stop() { + this.marketFetcher.stopPolling(); + this.rateFetcher.stopPolling(); + } + + private handleMarketResponse(resp: MarketResponse) { + this.pairs = {}; + + if (this.rateFetcher.isPolling()) { + this.rateFetcher.stopPolling(); + } + + const tokensAddress: string[] = []; + const pairsFullName: string[] = []; + const pairs: PairMap = {}; + for (const pair of resp.markets) { + if (pair.status !== 'available') { + continue; + } + pairs[pair.id] = { + ...pair, + fullName: `${pair.base.address}_${pair.quote.address}`, + }; + tokensAddress.push(pair.base.address); + tokensAddress.push(pair.quote.address); + + const reversedId = pair.id.split('-').reverse().join('-'); + const reversedPair = { + id: reversedId, + fullName: `${pair.quote.address}_${pair.base.address}`, + base: pair.quote, + quote: pair.base, + status: pair.status, + }; + + pairs[reversedId] = reversedPair; + + pairsFullName.push(pair.fullName); + pairsFullName.push(reversedPair.fullName); + } + + this.dexHelper.cache.setex( + this.dexKey, + this.dexHelper.config.data.network, + 'tokens', + this.config.marketConfig.dataTTLS, + JSON.stringify(tokensAddress.filter(onlyUnique)), + ); + + this.dexHelper.cache.setex( + this.dexKey, + this.dexHelper.config.data.network, + 'pairs', + this.config.marketConfig.dataTTLS, + JSON.stringify(pairsFullName.filter(onlyUnique)), + ); + + this.pairs = pairs; + this.rateFetcher.startPolling(); + } + + private handleRatesResponse(resp: RatesResponse) { + const pairs = this.pairs; + for (const [pairName, price] of Object.entries(resp)) { + const pair = pairs[pairName]; + if (!pair) { + continue; + } + + const buyPrices = price.buyAmounts.map((amount, i) => [ + amount, + price.buyPrices[i], + ]); + const sellPrices = price.sellAmounts.map((amount, i) => [ + amount, + price.sellPrices[i], + ]); + + if (buyPrices.length) { + this.dexHelper.cache.setex( + this.dexKey, + this.dexHelper.config.data.network, + `${pair.base}_${pair.quote}_${SwapSide.SELL}`, + this.config.marketConfig.dataTTLS, + JSON.stringify(buyPrices), + ); + } + + if (sellPrices.length) { + this.dexHelper.cache.setex( + this.dexKey, + this.dexHelper.config.data.network, + `${pair.fullName}_${SwapSide.BUY}`, + this.config.marketConfig.dataTTLS, + JSON.stringify(sellPrices), + ); + } + } + } + + checkHealth(): boolean { + return [this.marketFetcher, this.rateFetcher].some( + f => f.lastFetchSucceded, + ); + } + + public async getOrderPrice( + srcToken: Token, + destToken: Token, + side: SwapSide, + ): Promise { + let from = side === SwapSide.SELL ? srcToken : destToken; + let to = side === SwapSide.SELL ? destToken : srcToken; + + let pricesAsString: string | null = await this.dexHelper.cache.get( + this.dexKey, + this.dexHelper.config.data.network, + `${from.address}_${to.address}_${side}`, + ); + let reversed = false; + if (!pricesAsString) { + side = side === SwapSide.SELL ? SwapSide.BUY : SwapSide.SELL; + [from, to] = [to, from]; + pricesAsString = await this.dexHelper.cache.get( + this.dexKey, + this.dexHelper.config.data.network, + `${from.address}_${to.address}_${side}`, + ); + reversed = true; + } + + if (!pricesAsString) { + return null; + } + + const orderPricesAsString: Rates = JSON.parse(pricesAsString); + if (!orderPricesAsString) { + return null; + } + + let orderPrices: BigNumberRates = orderPricesAsString.map( + ([amount, price]) => [new BigNumber(amount), new BigNumber(price)], + ); + + if (reversed) { + orderPrices = orderPrices.map(reversePrice); + } + + return { + reversed, + from, + to, + side, + rates: orderPrices, + }; + } + + private async getPayload( + srcToken: Token, + destToken: Token, + srcAmount: string, + side: SwapSide, + txOrigin: Address, + ) { + let orderPrices: OrderPriceInfo | null = null; + try { + orderPrices = await this.getOrderPrice(srcToken, destToken, side); + if (!orderPrices) { + return { + value: BN_0, + }; + } + } catch (e) { + this.logger.error(e); + return { error: e }; + } + if (!orderPrices) { + return null; + } + + let _side = orderPrices.reversed ? 'sell' : 'buy'; + if (side === SwapSide.BUY) { + _side = orderPrices.reversed ? 'buy' : 'sell'; + } + + const amount = new BigNumber(srcAmount).div( + getBigNumberPow( + (_side === SwapSide.SELL ? srcToken : destToken).decimals, + ), + ); + + let obj: { + makerAmount?: string; + takerAmount?: string; + } = {}; + + if (orderPrices.reversed) { + if (_side === SwapSide.SELL) { + obj = { takerAmount: amount.toFixed() }; + } else { + obj = { makerAmount: amount.toFixed() }; + } + } else { + if (_side === SwapSide.SELL) { + obj = { makerAmount: amount.toFixed() }; + } else { + obj = { takerAmount: amount.toFixed() }; + } + } + + const payload: RFQPayload = { + makerAsset: orderPrices.from.address, + takerAsset: orderPrices.to.address, + model: 'firm', + side, + ...obj, + taker: this.augustusAddress, + txOrigin, + }; + + return { payload, value: amount }; + } + + async getFirmRate( + _srcToken: Token, + _destToken: Token, + srcAmount: string, + side: SwapSide, + txOrigin: Address, + ): Promise { + const srcToken = this.dexHelper.config.wrapETH(_srcToken); + const destToken = this.dexHelper.config.wrapETH(_destToken); + + const result = await this.getPayload( + srcToken, + destToken, + srcAmount, + side, + txOrigin, + ); + + if (!result) { + this.logger.error(`getPayload failed with empty payload`); + throw new Error('getFirmRate failed with empty payload'); + } + + if (result.error) { + this.logger.error(`getPayload failed with error: `, result.error); + throw new Error('getFirmRate failed no payload'); + } + + if (result.value?.isZero()) { + throw new Error( + `Empty value. payload: ${JSON.stringify(result.payload)}.`, + ); + } + + if (!result.payload) { + this.logger.error(`No payload ${JSON.stringify(result)}`); + throw new Error('getFirmRate failed no payload'); + } + + try { + const { data } = + await this.dexHelper.httpRequest.request({ + data: result.payload, + ...this.config.firmRateConfig, + }); + + if (data.status === 'rejected') { + this.logger.warn( + `getFirmRate failed ${JSON.stringify( + result, + )}, result:${JSON.stringify(data)}`, + ); + throw new Error('getFirmRate rejected'); + } + + return data.order; + } catch (e) { + this.logger.error(e); + throw e; + } + } +} diff --git a/src/dex/generic-rfq/types.ts b/src/dex/generic-rfq/types.ts new file mode 100644 index 000000000..2e81e2a14 --- /dev/null +++ b/src/dex/generic-rfq/types.ts @@ -0,0 +1,74 @@ +import BigNumber from 'bignumber.js'; +import { SwapSide } from 'paraswap-core'; +import { RequestConfig } from '../../dex-helper/irequest-wrapper'; +import { Address, Token } from '../../types'; +import { OrderInfo } from '../paraswap-limit-orders/types'; + +type Pair = { + id: string; + fullName: string; + base: Token; + quote: Token; + status: string; +}; + +export type PairMap = { + [pairName: string]: Pair; +}; + +export type MarketResponse = { + markets: Pair[]; +}; + +export type PairPriceResponse = { + buyAmounts: string[]; + buyPrices: string[]; + sellAmounts: string[]; + sellPrices: string[]; +}; + +export type RatesResponse = { + [pair: string]: PairPriceResponse; +}; + +export type FetcherParams = { + reqParams: RequestConfig; + intervalMs: number; + dataTTLS: number; +}; + +export type Rates = Array<[string, string]>; +export type BigNumberRate = [BigNumber, BigNumber]; +export type BigNumberRates = Array; + +export type OrderPriceInfo = { + reversed: boolean; + from: Token; + to: Token; + side: SwapSide; + rates: BigNumberRates; +}; + +export type RFQModel = 'firm' | 'indicative'; + +export type RFQConfig = { + marketConfig: FetcherParams; + rateConfig: FetcherParams; + firmRateConfig: RequestConfig; +}; + +export type RFQPayload = { + makerAsset: Address; + takerAsset: Address; + model: RFQModel; + side: SwapSide; + makerAmount?: string; + takerAmount?: string; + taker: Address; + txOrigin: Address; +}; + +export type RFQFirmRateResponse = { + status: 'accepted' | 'rejected'; + order: OrderInfo; +}; diff --git a/src/dex/idex.ts b/src/dex/idex.ts index 32bc8ba46..8deaeb666 100644 --- a/src/dex/idex.ts +++ b/src/dex/idex.ts @@ -13,6 +13,7 @@ import { ExchangeTxInfo, PreprocessTransactionOptions, TransferFeeParams, + Config, } from '../types'; import { SwapSide, Network } from '../constants'; import { IDexHelper } from '../dex-helper/idex-helper'; @@ -185,7 +186,9 @@ export interface DexContructor< // and networks they are supported. This is useful for using // same DEX implementation for multiple forks supported // in different set of networks. - dexKeysWithNetwork: { key: string; networks: Network[] }[]; + dexKeysWithNetwork?: { key: string; networks: Network[] }[]; + + builderDexKeysWithNetwork?(dexHelper: Config): void; } export type IRouteOptimizer = (formaterRate: T) => T; diff --git a/src/dex/index.ts b/src/dex/index.ts index dc80a409e..84dfa8efa 100644 --- a/src/dex/index.ts +++ b/src/dex/index.ts @@ -53,6 +53,7 @@ import { balancerV1Merge } from './balancer-v1/optimizer'; import { CurveV1 } from './curve-v1/curve-v1'; import { CurveFork } from './curve-v1/forks/curve-forks/curve-forks'; import { Swerve } from './curve-v1/forks/swerve/swerve'; +import { GenericRFQ } from './generic-rfq/generic-rfq'; const LegacyDexes = [ CurveV2, @@ -182,6 +183,16 @@ export class DexAdapterService { }); }); + const rfqConfigs = dexHelper.config.data.rfqConfigs; + Object.keys(dexHelper.config.data.rfqConfigs).forEach(rfqName => { + const dex = new GenericRFQ( + network, + rfqName, + dexHelper, + rfqConfigs[rfqName], + ); + }); + this.directFunctionsNames = [...LegacyDexes, ...Dexes] .flatMap(dexAdapter => { const _dexAdapter = dexAdapter as IGetDirectFunctionName; diff --git a/src/dex/paraswap-limit-orders/paraswap-limit-orders.ts b/src/dex/paraswap-limit-orders/paraswap-limit-orders.ts index 4e30b3947..76f3554f8 100644 --- a/src/dex/paraswap-limit-orders/paraswap-limit-orders.ts +++ b/src/dex/paraswap-limit-orders/paraswap-limit-orders.ts @@ -26,7 +26,7 @@ import { OrderInfo, ParaSwapOrderBook, } from './types'; -import { ParaSwapLimitOrdersConfig, Adapters } from './config'; +import { Adapters, ParaSwapLimitOrdersConfig } from './config'; import { LimitOrderExchange } from '../limit-order-exchange'; import { BI_MAX_UINT256 } from '../../bigint-constants'; import augustusRFQABI from '../../abi/paraswap-limit-orders/AugustusRFQ.abi.json'; @@ -52,17 +52,18 @@ export class ParaSwapLimitOrders logger: Logger; + protected augustusRFQAddress: Address; + constructor( protected network: Network, protected dexKey: string, protected dexHelper: IDexHelper, protected adapters = Adapters[network] ? Adapters[network] : {}, - protected augustusRFQAddress = ParaSwapLimitOrdersConfig[dexKey][ - network - ].rfqAddress.toLowerCase(), protected rfqIface = new Interface(augustusRFQABI), ) { super(dexHelper.config.data.augustusAddress, dexHelper.web3Provider); + this.augustusRFQAddress = + dexHelper.config.data.augustusRFQAddress.toLowerCase(); this.logger = dexHelper.getLogger(dexKey); } diff --git a/src/lib/fetcher/fetcher.ts b/src/lib/fetcher/fetcher.ts new file mode 100644 index 000000000..559fb1c58 --- /dev/null +++ b/src/lib/fetcher/fetcher.ts @@ -0,0 +1,138 @@ +import { IRequestWrapper } from '../../dex-helper'; +import { Logger } from 'log4js'; +import { RequestConfig, Response } from '../../dex-helper/irequest-wrapper'; + +import Timeout = NodeJS.Timeout; + +const FETCH_TIMEOUT_MS = 10 * 1000; +const FETCH_FAIL_MAX_ATTEMPT = 5; +const FETCH_FAIL_RETRY_TIMEOUT_MS = 60 * 1000; + +export type RequestInfo = { + requestOptions: RequestConfig; + caster: (data: unknown) => T | null; + authenticate?: (options: RequestConfig) => void; + excludedFieldsCaching?: string[]; +}; + +export type RequestInfoWithHandler = { + info: RequestInfo; + handler: (data: T) => void; +}; + +export default class Fetcher { + private intervalId?: Timeout; + private requests: RequestInfoWithHandler[]; + public lastFetchSucceded: boolean = false; + private failedCount = 0; + + constructor( + private requestWrapper: IRequestWrapper, + requestsInfo: RequestInfoWithHandler | RequestInfoWithHandler[], + private pollInterval: number, + private logger: Logger, + ) { + if (Array.isArray(requestsInfo)) { + this.requests = requestsInfo; + } else { + this.requests = [requestsInfo]; + } + } + + async fetch() { + if (this.failedCount >= FETCH_FAIL_MAX_ATTEMPT) { + this.stopPolling(); + + setTimeout(() => { + this.startPolling(); + this.failedCount = 0; + }, FETCH_FAIL_RETRY_TIMEOUT_MS); + return; + } + + const promises = this.requests.map>>( + async (reqInfo: RequestInfoWithHandler) => { + const info = reqInfo.info; + let options = info.requestOptions; + if (info.authenticate) { + info.authenticate(options); + } + + try { + const result = await this.requestWrapper.request({ + timeout: FETCH_TIMEOUT_MS, + ...options, + }); + return result; + } catch (e) { + return e as Error; + } + }, + ); + const results = await Promise.all(promises); + const failures = results + .map((_, i) => i) + .filter(i => results[i] instanceof Error); + + failures.forEach(i => { + this.logger.warn( + `failled polling ${this.requests[i].info.requestOptions.url} ${results[i]}`, + ); + }); + + if (failures.length === results.length) { + this.lastFetchSucceded = false; + this.failedCount += 1; + } else { + this.lastFetchSucceded = true; + } + + results + .map((_, i) => i) + .filter(i => !(results[i] instanceof Error)) + .forEach(i => { + const response = results[i] as Response; + const reqInfo = this.requests[i]; + const info = reqInfo.info; + const options = reqInfo.info.requestOptions; + + const parsedData = info.caster(response.data); + + if (!parsedData) { + this.logger.debug(`(${options.url}) received incorrect data`); + return; + } + + reqInfo.handler(parsedData); + this.logger.debug(`(${options.url}) received new data`); + }); + } + + private getUrls(): string[] { + return this.requests.map(el => el.info.requestOptions.url!); + } + + startPolling(): void { + if (this.intervalId) return; + + this.intervalId = setInterval(() => { + this.fetch(); + }, this.pollInterval); + // TODO add some mechanism for removing polling if it's failing + + this.logger.info(`Polling started for ${this.getUrls()}`); + this.fetch(); + } + + stopPolling() { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = undefined; + this.logger.info(`Polling stopped for ${this.getUrls()}`); + } + } + + isPolling(): boolean { + return this.intervalId !== undefined ? true : false; + } +} diff --git a/src/types.ts b/src/types.ts index f251d846f..6df2af82d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,6 +16,7 @@ import { Logger } from 'log4js'; export { Logger } from 'log4js'; import { OptimalRate } from 'paraswap-core'; import BigNumber from 'bignumber.js'; +import { RFQConfig } from './dex/generic-rfq/types'; // Check: Should the logger be replaced with Logger Interface export type LoggerConstructor = (name?: string) => Logger; @@ -175,6 +176,7 @@ export type Token = { address: string; decimals: number; symbol?: string; + type?: string; }; export type aToken = { @@ -259,11 +261,13 @@ export type Config = { wrappedNativeTokenAddress: Address; hasEIP1559: boolean; augustusAddress: Address; + augustusRFQAddress: Address; tokenTransferProxyAddress: Address; multicallV2Address: Address; privateHttpProvider: string; adapterAddresses: { [name: string]: Address }; uniswapV2ExchangeRouterAddress: Address; + rfqConfigs: Record; }; export type BigIntAsString = string; diff --git a/yarn.lock b/yarn.lock index 7bf0d1e05..3788992e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1359,6 +1359,15 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@paraswap/sdk@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@paraswap/sdk/-/sdk-6.0.0.tgz#94732148f78575a1f13551959952083273e9c5b6" + integrity sha512-d1SMuUnRhL/wmlkGwMz9Y6+U1lSuhsKwixvxuqABu5EH6a22fo7TECxrHysagttvhmbN/sy+J1Uy5qQuXSsbXw== + dependencies: + bignumber.js "^9.0.2" + paraswap-core "^1.0.2" + ts-essentials "^9.1.2" + "@sinclair/typebox@^0.24.1": version "0.24.42" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.42.tgz#a74b608d494a1f4cc079738e050142a678813f52" @@ -1464,6 +1473,40 @@ dependencies: "@types/node" "*" +"@types/body-parser@*": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@^4.17.18": + version "4.17.31" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz#a1139efeab4e7323834bb0226e62ac019f474b2f" + integrity sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@^4.17.14": + version "4.17.14" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.14.tgz#143ea0557249bc1b3b54f15db4c81c3d4eb3569c" + integrity sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.18" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/graceful-fs@^4.1.3": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" @@ -1503,6 +1546,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8" integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw== +"@types/mime@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" + integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== + "@types/minimatch@^3.0.3": version "3.0.5" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" @@ -1540,6 +1588,16 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== +"@types/qs@*": + version "6.9.7" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + "@types/react@*": version "17.0.14" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.14.tgz#f0629761ca02945c4e8fea99b8177f4c5c61fb0f" @@ -1561,6 +1619,14 @@ dependencies: "@types/node" "*" +"@types/serve-static@*": + version "1.15.0" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.0.tgz#c7930ff61afb334e121a9da780aac0d9b8f34155" + integrity sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg== + dependencies: + "@types/mime" "*" + "@types/node" "*" + "@types/stack-utils@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" @@ -1866,7 +1932,7 @@ bignumber.js@8.1.1: resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-8.1.1.tgz#4b072ae5aea9c20f6730e4e5d529df1271c4d885" integrity sha512-QD46ppGintwPGuL1KqmwhR0O+N2cZUg8JG/VzwI2e28sM9TqHjQB10lI4QAaMHVbLzwVLLAwEglpKPViWX+5NQ== -bignumber.js@9.1.0: +bignumber.js@9.1.0, bignumber.js@^9.0.2: version "9.1.0" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.0.tgz#8d340146107fe3a6cb8d40699643c302e8773b62" integrity sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A== @@ -1922,6 +1988,24 @@ body-parser@1.19.2, body-parser@^1.16.0: raw-body "2.4.3" type-is "~1.6.18" +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -2330,6 +2414,11 @@ cookie@0.4.2: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + cookiejar@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc" @@ -2503,6 +2592,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -2516,6 +2610,11 @@ des.js@^1.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" @@ -2992,6 +3091,43 @@ express@^4.14.0: utils-merge "1.0.1" vary "~1.1.2" +express@^4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + ext@^1.1.2: version "1.6.0" resolved "https://registry.yarnpkg.com/ext/-/ext-1.6.0.tgz#3871d50641e874cc172e2b53f919842d19db4c52" @@ -3038,6 +3174,19 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + finalhandler@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" @@ -3395,6 +3544,17 @@ http-errors@1.8.1: statuses ">= 1.5.0 < 2" toidentifier "1.0.1" +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + http-https@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/http-https/-/http-https-1.0.0.tgz#2f908dd5f1db4068c058cd6e6d4ce392c913389b" @@ -4645,6 +4805,13 @@ oboe@2.1.5: dependencies: http-https "^1.0.0" +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -4722,7 +4889,7 @@ param-case@^3.0.4: dot-case "^3.0.4" tslib "^2.0.3" -paraswap-core@1.0.2: +paraswap-core@1.0.2, paraswap-core@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/paraswap-core/-/paraswap-core-1.0.2.tgz#9e2ae35bc393b6f3bb9407bd097f21d498abb5a7" integrity sha512-/PyonrjBsv9xOsFk4mVr0rh7BENrQtYYvvZMwu6QFN4qcca2VgmqOgpNWZPvfyBpKY+fUqmbBNlhqlaMQ3TMzQ== @@ -4968,6 +5135,13 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + qs@6.9.7: version "6.9.7" resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe" @@ -5024,6 +5198,16 @@ raw-body@2.4.3: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + react-is@^17.0.1: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" @@ -5199,6 +5383,25 @@ send@0.17.2: range-parser "~1.2.1" statuses "~1.5.0" +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + sentence-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/sentence-case/-/sentence-case-3.0.4.tgz#3645a7b8c117c787fde8702056225bb62a45131f" @@ -5218,6 +5421,16 @@ serve-static@1.14.2: parseurl "~1.3.3" send "0.17.2" +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + servify@^0.1.12: version "0.1.12" resolved "https://registry.yarnpkg.com/servify/-/servify-0.1.12.tgz#142ab7bee1f1d033b66d0707086085b17c06db95" @@ -5355,6 +5568,11 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" @@ -5590,6 +5808,11 @@ ts-essentials@^7.0.0: resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.3.tgz#686fd155a02133eedcc5362dc8b5056cde3e5a38" integrity sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ== +ts-essentials@^9.1.2: + version "9.3.0" + resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-9.3.0.tgz#7e639c1a76b1805c3c60d6e1b5178da2e70aea02" + integrity sha512-XeiCboEyBG8UqXZtXl59bWEi4ZgOqRsogFDI6WDGIF1LmzbYiAkIwjkXN6zZWWl4re/lsOqMlYfe8KA0XiiEPw== + ts-jest@^29.0.1: version "29.0.1" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.0.1.tgz#3296b39d069dc55825ce1d059a9510b33c718b86"