diff --git a/package.json b/package.json index ef6c88127c..4d3ae06a3b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hummingbot-gateway", - "version": "dev-1.28.0", + "version": "dev-2.0.1", "description": "Middleware that helps Hummingbot clients access standardized DEX API endpoints on different blockchain networks", "main": "index.js", "license": "Apache-2.0", @@ -24,6 +24,7 @@ }, "dependencies": { "@cosmjs/amino": "^0.32.2", + "@balancer-labs/sdk": "^1.1.5", "@bancor/carbon-sdk": "^0.0.93-DEV", "@cosmjs/proto-signing": "^0.31.1", "@cosmjs/stargate": "^0.31.1", diff --git a/src/app.ts b/src/app.ts index 300754577a..5acf60e4dd 100644 --- a/src/app.ts +++ b/src/app.ts @@ -112,7 +112,7 @@ export const startSwagger = async () => { export const startGateway = async () => { const port = ConfigManagerV2.getInstance().get('server.port'); - const gateway_version="dev-1.28.0" + const gateway_version="dev-2.0.1" if (!ConfigManagerV2.getInstance().get('server.id')) { ConfigManagerV2.getInstance().set( 'server.id', diff --git a/src/chains/avalanche/avalanche.ts b/src/chains/avalanche/avalanche.ts index 168bcb09d4..03ac928aec 100644 --- a/src/chains/avalanche/avalanche.ts +++ b/src/chains/avalanche/avalanche.ts @@ -12,6 +12,7 @@ import { SushiswapConfig } from '../../connectors/sushiswap/sushiswap.config'; import { ConfigManagerV2 } from '../../services/config-manager-v2'; import { EVMController } from '../ethereum/evm.controllers'; import { Curve } from '../../connectors/curve/curve'; +import { BalancerConfig } from '../../connectors/balancer/balancer.config'; export class Avalanche extends EthereumBase implements Ethereumish { private static _instances: { [name: string]: Avalanche }; @@ -102,6 +103,8 @@ export class Avalanche extends EthereumBase implements Ethereumish { throw Error('Curve not ready'); } spender = curve.router; + } else if (reqSpender === 'balancer') { + spender = BalancerConfig.config.routerAddress(this._chain); } else { spender = reqSpender; } diff --git a/src/chains/avalanche/avalanche.validators.ts b/src/chains/avalanche/avalanche.validators.ts index c1c7b3682a..c771a2d43a 100644 --- a/src/chains/avalanche/avalanche.validators.ts +++ b/src/chains/avalanche/avalanche.validators.ts @@ -27,6 +27,7 @@ export const validateSpender: Validator = mkValidator( val === 'traderjoe' || val === 'openocean' || val === 'sushiswap' || + val === 'balancer' || isAddress(val)) ); diff --git a/src/chains/ethereum/ethereum.ts b/src/chains/ethereum/ethereum.ts index 8084a38e7a..c1d90d3b4f 100644 --- a/src/chains/ethereum/ethereum.ts +++ b/src/chains/ethereum/ethereum.ts @@ -16,6 +16,7 @@ import { SushiswapConfig } from '../../connectors/sushiswap/sushiswap.config'; import { OpenoceanConfig } from '../../connectors/openocean/openocean.config'; import { Curve } from '../../connectors/curve/curve'; import { CarbonConfig } from '../../connectors/carbon/carbon.config'; +import { BalancerConfig } from '../../connectors/balancer/balancer.config'; // MKR does not match the ERC20 perfectly so we need to use a separate ABI. const MKR_ADDRESS = '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2'; @@ -222,6 +223,8 @@ export class Ethereum extends EthereumBase implements Ethereumish { throw Error('Curve not ready'); } spender = curve.router; + } else if (reqSpender === 'balancer') { + spender = BalancerConfig.config.routerAddress(this._chain); } else { spender = reqSpender; } diff --git a/src/chains/ethereum/ethereum.validators.ts b/src/chains/ethereum/ethereum.validators.ts index e8280547b2..9a6e6aed7e 100644 --- a/src/chains/ethereum/ethereum.validators.ts +++ b/src/chains/ethereum/ethereum.validators.ts @@ -64,6 +64,7 @@ export const validateSpender: Validator = mkValidator( val === 'xsswap' || val === 'curve' || val === 'carbonamm' || + val === 'balancer' || isAddress(val)) ); diff --git a/src/chains/polygon/polygon.ts b/src/chains/polygon/polygon.ts index 025f5f354c..dc8d8ff8be 100644 --- a/src/chains/polygon/polygon.ts +++ b/src/chains/polygon/polygon.ts @@ -12,6 +12,7 @@ import { ConfigManagerV2 } from '../../services/config-manager-v2'; import { OpenoceanConfig } from '../../connectors/openocean/openocean.config'; import { EVMController } from '../ethereum/evm.controllers'; import { Curve } from '../../connectors/curve/curve'; +import { BalancerConfig } from '../../connectors/balancer/balancer.config'; export class Polygon extends EthereumBase implements Ethereumish { private static _instances: { [name: string]: Polygon }; @@ -95,6 +96,8 @@ export class Polygon extends EthereumBase implements Ethereumish { throw Error('Curve not ready'); } spender = curve.router; + } else if (reqSpender === 'balancer') { + spender = BalancerConfig.config.routerAddress(this._chain); } else { spender = reqSpender; } diff --git a/src/chains/polygon/polygon.validators.ts b/src/chains/polygon/polygon.validators.ts index 4c4089f136..cb03f2bce2 100644 --- a/src/chains/polygon/polygon.validators.ts +++ b/src/chains/polygon/polygon.validators.ts @@ -27,6 +27,7 @@ export const validateSpender: Validator = mkValidator( val === 'sushi' || val === 'quickswap' || val === 'openocean' || + val === 'balancer' || isAddress(val)) ); diff --git a/src/connectors/balancer/balancer.config.ts b/src/connectors/balancer/balancer.config.ts new file mode 100644 index 0000000000..0625af3bbc --- /dev/null +++ b/src/connectors/balancer/balancer.config.ts @@ -0,0 +1,23 @@ +import { buildConfig, NetworkConfig } from '../../network/network.utils'; + +export namespace BalancerConfig { + export const config: NetworkConfig = buildConfig( + 'balancer', + ['AMM'], + [ + { + chain: 'avalanche', + networks: ['avalanche'], + }, + { + chain: 'ethereum', + networks: ['mainnet', 'arbitrum', 'optimism'], + }, + { + chain: 'polygon', + networks: ['mainnet'], + }, + ], + 'EVM' + ); +} diff --git a/src/connectors/balancer/balancer.ts b/src/connectors/balancer/balancer.ts new file mode 100644 index 0000000000..e948b3c1d0 --- /dev/null +++ b/src/connectors/balancer/balancer.ts @@ -0,0 +1,331 @@ +import { + BigNumber, + ContractTransaction, + Transaction, + Wallet, + ContractInterface, + } from 'ethers'; + import { percentRegexp } from '../../services/config-manager-v2'; + import { + InitializationError, + SERVICE_UNITIALIZED_ERROR_CODE, + SERVICE_UNITIALIZED_ERROR_MESSAGE, + UniswapishPriceError, + } from '../../services/error-handler'; + import { logger } from '../../services/logger'; + import { isFractionString } from '../../services/validators'; + import { BalancerConfig } from './balancer.config'; + import { getAddress } from 'ethers/lib/utils'; + import { Polygon } from '../../chains/polygon/polygon'; + import { Ethereum } from '../../chains/ethereum/ethereum'; + import { EVMTxBroadcaster } from '../../chains/ethereum/evm.broadcaster'; + import { Fraction, Token } from '@uniswap/sdk'; + import { BalancerSDK, SwapInfo, SwapType } from '@balancer-labs/sdk' + import { Uniswapish, UniswapishTrade } from '../../services/common-interfaces'; + import { Avalanche } from '../../chains/avalanche/avalanche'; +import { Currency, CurrencyAmount } from '@uniswap/sdk-core'; +import * as math from 'mathjs'; + +export interface BalancerSwap { + swapInfo: SwapInfo; + maxSlippage: number; + deadline: string; + kind: SwapType; +} +export interface BalancerTrade { + swap : BalancerSwap; + executionPrice: Fraction; +} + + export class Balancer implements Uniswapish { + private static _instances: { [name: string]: Balancer }; + private _chain: Ethereum | Polygon | Avalanche; + private _config: typeof BalancerConfig.config; + private tokenList: Record = {}; + private _ready: boolean = false; + public gasLimitEstimate: any; + public router: any; + public balancer: BalancerSDK; + public routerAbi: any[]; + public ttl: any; + + private constructor(chain: string, network: string) { + this._config = BalancerConfig.config; + if (chain === 'ethereum') { + this._chain = Ethereum.getInstance(network); + } else if (chain === 'avalanche') { + this._chain = Avalanche.getInstance(network); + } else if (chain === 'polygon') { + this._chain = Polygon.getInstance(network); + } else throw Error('Chain not supported.'); + this.balancer = new BalancerSDK({ + network: this._chain.chainId, + rpcUrl: this._chain.rpcUrl + }); + this.routerAbi = []; + this.ttl = this._config.ttl; + this.gasLimitEstimate = this._config.gasLimitEstimate; + } + + public static getInstance(chain: string, network: string): Balancer { + if (Balancer._instances === undefined) { + Balancer._instances = {}; + } + if (!(chain + network in Balancer._instances)) { + Balancer._instances[chain + network] = new Balancer(chain, network); + } + + return Balancer._instances[chain + network]; + } + + public async init() { + if (!this._chain.ready()) + throw new InitializationError( + SERVICE_UNITIALIZED_ERROR_MESSAGE(this._chain.chainName), + SERVICE_UNITIALIZED_ERROR_CODE + ); + for (const token of this._chain.storedTokenList) { + this.tokenList[token.address] = new Token( + this._chain.chainId, + token.address, + token.decimals, + token.symbol, + token.name + ); + } + this._ready = true; + } + + /* + * Given a token's address, return the connector's native representation of + * the token. + * + * @param address Token address + */ + public getTokenByAddress(address: string): Token { + return this.tokenList[getAddress(address)]; + } + + /** + * Determines if the connector is ready. + */ + public ready(): boolean { + return this._ready; + } + + /** + * Gets the allowed slippage percent from the optional parameter or the value + * in the configuration. + * + * @param allowedSlippageStr (Optional) should be of the form '1/10'. + */ + public getAllowedSlippage(allowedSlippageStr?: string): number { + if (allowedSlippageStr != null && isFractionString(allowedSlippageStr)) { + const fractionSplit = allowedSlippageStr.split('/'); + return Number((Number(fractionSplit[0]) / Number(fractionSplit[1]) * 100).toFixed(0)); + } + + const allowedSlippage = this._config.allowedSlippage; + const matches = allowedSlippage.match(percentRegexp); + if (matches) return Number((Number(matches[1]) / Number(matches[2]) * 100).toFixed(0)); + throw new Error( + 'Encountered a malformed percent string in the config for ALLOWED_SLIPPAGE.' + ); + } + + /** + * Given the amount of `baseToken` desired to acquire from a transaction, + * calculate the amount of `quoteToken` needed for the transaction. + * + * This is typically used for calculating token buy prices. + * + * @param quoteToken Token input for the transaction + * @param baseToken Token output from the transaction + * @param amount Amount of `baseToken` desired from the transaction + * @param allowedSlippage (Optional) Fraction in string representing the allowed slippage for this transaction + */ + async estimateBuyTrade( + quoteToken: Token, + baseToken: Token, + amount: BigNumber, + allowedSlippage?: string, + poolId?: string, + ) { + logger.info( + `Fetching pair data for ${quoteToken.address}-${baseToken.address}.` + ); + + + const filter = { + where: { + id: { + eq: poolId, + }, + } + }; + + await this.balancer.swaps.fetchPools(poolId? filter: undefined); + + const info = await this.balancer.swaps.findRouteGivenOut( + { + tokenIn: quoteToken.address, + tokenOut: baseToken.address, + amount: amount, + gasPrice: BigNumber.from(this._chain.gasPrice.toFixed(0)), + } + ); + if (info.swaps.length === 0) { + throw new UniswapishPriceError( + `No pool found for ${quoteToken.address} to ${baseToken.address}.` + ); + } + const marketSp = math.fraction(info.marketSp) as math.Fraction; + const executionPrice = new Fraction(marketSp.d.toString(), marketSp.n.toString()) + return { + trade: { + swap: { + swapInfo: info, + maxSlippage: this.getAllowedSlippage(allowedSlippage), + deadline: '0', // updated before trade execution + kind: SwapType.SwapExactOut, + }, + executionPrice + }, + expectedAmount: CurrencyAmount.fromRawAmount(quoteToken, info.returnAmount.toString()), + }; + } + + /** + * Given the amount of `baseToken` to put into a transaction, calculate the + * amount of `quoteToken` that can be expected from the transaction. + * + * This is typically used for calculating token sell prices. + * + * @param baseToken Token input for the transaction + * @param quoteToken Output from the transaction + * @param amount Amount of `baseToken` to put into the transaction + * @param allowedSlippage (Optional) Fraction in string representing the allowed slippage for this transaction + */ + async estimateSellTrade( + baseToken: Token, + quoteToken: Token, + amount: BigNumber, + allowedSlippage?: string, + poolId?: string + ) { + logger.info( + `Fetching pair data for ${quoteToken.address}-${baseToken.address}.` + ); + + const filter = { + where: { + id: { + eq: poolId, + }, + } + }; + + await this.balancer.swaps.fetchPools(poolId? filter: undefined); + + const info = await this.balancer.swaps.findRouteGivenIn( + { + tokenIn: baseToken.address, + tokenOut: quoteToken.address, + amount: amount, + gasPrice: BigNumber.from(this._chain.gasPrice.toFixed(0)), + } + ); + if (info.swaps.length === 0) { + throw new UniswapishPriceError( + `No pool found for ${quoteToken.address} to ${baseToken.address}.` + ); + } + const marketSp = math.fraction(info.marketSp) as math.Fraction; + const executionPrice = new Fraction(marketSp.d.toString(), marketSp.n.toString()); + return { + trade: { + swap: { + swapInfo: info, + maxSlippage: this.getAllowedSlippage(allowedSlippage), + deadline: '0', // updated before trade execution + kind: SwapType.SwapExactIn, + }, + executionPrice + }, + expectedAmount: CurrencyAmount.fromRawAmount(quoteToken, info.returnAmount.toString()), + }; + } + + /** + * Given a wallet and a Uniswap trade, try to execute it on blockchain. + * + * @param wallet Wallet + * @param trade Expected trade + * @param gasPrice Base gas price, for pre-EIP1559 transactions + * @param _router Router smart contract address + * @param _ttl How long the swap is valid before expiry, in seconds + * @param _abi Router contract ABI + * @param gasLimit Gas limit + * @param nonce (Optional) EVM transaction nonce + * @param maxFeePerGas (Optional) Maximum total fee per gas you want to pay + * @param maxPriorityFeePerGas (Optional) Maximum tip per gas you want to pay + * @param allowedSlippage (Optional) Fraction in string representing the allowed slippage for this transaction + */ + async executeTrade( + wallet: Wallet, + trade: UniswapishTrade, + _gasPrice: number, + _uniswapRouter: string, + ttl: number, + _abi: ContractInterface, + gasLimit: number, + nonce?: number, + maxFeePerGas?: BigNumber, + maxPriorityFeePerGas?: BigNumber + ): Promise { + let overrideParams: { + gasLimit: string | number; + value: number; + nonce: number | undefined; + maxFeePerGas?: BigNumber | undefined; + maxPriorityFeePerGas?: BigNumber | undefined; + gasPrice?: string; + }; + if (maxFeePerGas || maxPriorityFeePerGas) { + overrideParams = { + gasLimit: gasLimit, + value: 0, + nonce: nonce, + maxFeePerGas, + maxPriorityFeePerGas, + }; + } else { + overrideParams = { + // gasPrice: (gasPrice * 1e9).toFixed(0), + gasLimit: gasLimit.toFixed(0), + value: 0, + nonce: nonce, + }; + } + const t: BalancerTrade = trade; + const txDataRaw = this.balancer.swaps.buildSwap({ + ...t.swap, + ...{userAddress: wallet.address, + deadline: Math.floor(Date.now() / 1000 + ttl).toString()} + }) + const txData = { + to: txDataRaw.to, + data: txDataRaw.data, + value: txDataRaw.value + } + + const txResponse: ContractTransaction = await EVMTxBroadcaster.getInstance( + this._chain, + wallet.address + ).broadcast({...txData, ...overrideParams}); + + logger.info(`Transaction Details: ${JSON.stringify(txResponse.hash)}`); + return txResponse; + } + } + diff --git a/src/connectors/connectors.routes.ts b/src/connectors/connectors.routes.ts index d9ac0b63f9..e03e551f0f 100644 --- a/src/connectors/connectors.routes.ts +++ b/src/connectors/connectors.routes.ts @@ -24,6 +24,7 @@ import { KujiraConfig } from './kujira/kujira.config'; import { QuipuswapConfig } from './quipuswap/quipuswap.config'; import { OsmosisConfig } from '../chains/osmosis/osmosis.config'; import { CarbonConfig } from './carbon/carbon.config'; +import { BalancerConfig } from './balancer/balancer.config'; export namespace ConnectorsRoutes { export const router = Router(); @@ -183,6 +184,12 @@ export namespace ConnectorsRoutes { chain_type: CarbonConfig.config.chainType, available_networks: CarbonConfig.config.availableNetworks, }, + { + name: 'balancer', + trading_type: BalancerConfig.config.tradingTypes, + chain_type: BalancerConfig.config.chainType, + available_networks: BalancerConfig.config.availableNetworks, + }, ], }); }) diff --git a/src/connectors/uniswap/uniswap.controllers.ts b/src/connectors/uniswap/uniswap.controllers.ts index fc9c50202d..b27e020124 100644 --- a/src/connectors/uniswap/uniswap.controllers.ts +++ b/src/connectors/uniswap/uniswap.controllers.ts @@ -222,6 +222,7 @@ export async function trade( req.quote, new Decimal(req.amount), req.side, + req.allowedSlippage, req.poolId, ); } catch (e) { diff --git a/src/connectors/uniswap/uniswap.ts b/src/connectors/uniswap/uniswap.ts index 4501f489fc..94e7710d17 100644 --- a/src/connectors/uniswap/uniswap.ts +++ b/src/connectors/uniswap/uniswap.ts @@ -207,7 +207,8 @@ export class Uniswap implements Uniswapish { baseToken: Token, quoteToken: Token, amount: BigNumber, - allowedSlippage?: string + allowedSlippage?: string, + poolId?: string ): Promise { const nativeTokenAmount: CurrencyAmount = CurrencyAmount.fromRawAmount(baseToken, amount.toString()); @@ -242,7 +243,7 @@ export class Uniswap implements Uniswapish { ); return { trade: route.trade, expectedAmount }; } else { - const pool = await this.getPool(baseToken, quoteToken, this._feeTier); + const pool = await this.getPool(baseToken, quoteToken, this._feeTier, poolId); if (!pool) { throw new UniswapishPriceError( `priceSwapIn: no trade pair found for ${baseToken.address} to ${quoteToken.address}.` @@ -287,7 +288,8 @@ export class Uniswap implements Uniswapish { quoteToken: Token, baseToken: Token, amount: BigNumber, - allowedSlippage?: string + allowedSlippage?: string, + poolId?: string ): Promise { const nativeTokenAmount: CurrencyAmount = CurrencyAmount.fromRawAmount(baseToken, amount.toString()); @@ -321,7 +323,7 @@ export class Uniswap implements Uniswapish { ); return { trade: route.trade, expectedAmount }; } else { - const pool = await this.getPool(quoteToken, baseToken, this._feeTier); + const pool = await this.getPool(quoteToken, baseToken, this._feeTier, poolId); if (!pool) { throw new UniswapishPriceError( `priceSwapOut: no trade pair found for ${quoteToken.address} to ${baseToken.address}.` @@ -422,7 +424,8 @@ export class Uniswap implements Uniswapish { private async getPool( tokenA: Token, tokenB: Token, - feeTier: FeeAmount + feeTier: FeeAmount, + poolId?: string ): Promise { const uniswapFactory = new Contract( FACTORY_ADDRESS, @@ -430,12 +433,12 @@ export class Uniswap implements Uniswapish { this.chain.provider ); // Use Uniswap V3 factory to get pool address instead of `Pool.getAddress` to check if pool exists. - const poolAddress = await uniswapFactory.getPool( + const poolAddress = poolId || await uniswapFactory.getPool( tokenA.address, tokenB.address, feeTier ); - if (poolAddress === constants.AddressZero) { + if (poolAddress === constants.AddressZero || poolAddress === undefined || poolAddress === '') { return null; } const poolContract = new Contract( @@ -444,16 +447,17 @@ export class Uniswap implements Uniswapish { this.chain.provider ); - const [liquidity, slot0] = await Promise.all([ + const [liquidity, slot0, fee] = await Promise.all([ poolContract.liquidity(), poolContract.slot0(), + poolContract.fee(), ]); const [sqrtPriceX96, tick] = slot0; const pool = new Pool( tokenA, tokenB, - this._feeTier, + fee, sqrtPriceX96, liquidity, tick diff --git a/src/services/common-interfaces.ts b/src/services/common-interfaces.ts index 464b06e29d..4fd53df531 100644 --- a/src/services/common-interfaces.ts +++ b/src/services/common-interfaces.ts @@ -115,6 +115,7 @@ import { TradeV2 } from '@traderjoe-xyz/sdk-v2'; import { CurveTrade } from '../connectors/curve/curve'; import { SerializableExtendedPool as CosmosSerializableExtendedPool } from '../chains/osmosis/osmosis.types'; import { CarbonTrade } from '../connectors/carbon/carbonAMM'; +import { BalancerTrade } from '../connectors/balancer/balancer'; // TODO Check the possibility to have clob/solana/serum equivalents here // Check this link https://hummingbot.org/developers/gateway/building-gateway-connectors/#5-add-sdk-classes-to-uniswapish-interface @@ -164,7 +165,8 @@ export type UniswapishTrade = | TradeXsswap | TradeV2 | CurveTrade - | CarbonTrade; + | CarbonTrade + | BalancerTrade; export type UniswapishTradeOptions = | MMFTradeOptions diff --git a/src/services/connection-manager.ts b/src/services/connection-manager.ts index e2bd6d4866..456d1eeedf 100644 --- a/src/services/connection-manager.ts +++ b/src/services/connection-manager.ts @@ -45,6 +45,7 @@ import { PancakeswapLP } from '../connectors/pancakeswap/pancakeswap.lp'; import { XRPLCLOB } from '../connectors/xrpl/xrpl'; import { QuipuSwap } from '../connectors/quipuswap/quipuswap'; import { Carbonamm } from '../connectors/carbon/carbonAMM'; +import { Balancer } from '../connectors/balancer/balancer'; export type ChainUnion = | Algorand @@ -245,6 +246,11 @@ export async function getConnector( connector === 'curve' ) { connectorInstance = Curve.getInstance(chain, network); + } else if ( + (chain === 'ethereum' || chain === 'polygon' || chain === 'avalanche') && + connector === 'balancer' + ) { + connectorInstance = Balancer.getInstance(chain, network); } else if (chain === 'tezos' && connector === 'quipuswap') { connectorInstance = QuipuSwap.getInstance(network); } else if (chain === 'ethereum' && connector === 'carbonamm') { diff --git a/src/services/schema/balancer-schema.json b/src/services/schema/balancer-schema.json new file mode 100644 index 0000000000..79fcabb1a2 --- /dev/null +++ b/src/services/schema/balancer-schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "allowedSlippage": { "type": "string" }, + "gasLimitEstimate": { "type": "integer" }, + "ttl": { "type": "integer" }, + "maximumHops": { "type": "integer" }, + "contractAddresses": { + "type": "object", + "patternProperties": { + "^[\\w-]+$": { + "type": "object", + "patternProperties": { + "^\\w+$": { + "type": "object", + "properties": { + "balancerV2VaultAddress": { "type": "string" } + }, + "required": ["balancerV2VaultAddress"], + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "required": ["allowedSlippage", "gasLimitEstimate", "ttl", "maximumHops"] + } \ No newline at end of file diff --git a/src/templates/balancer.yml b/src/templates/balancer.yml new file mode 100644 index 0000000000..730950d0f3 --- /dev/null +++ b/src/templates/balancer.yml @@ -0,0 +1,21 @@ +# allowedSlippage: how much the execution price is allowed to move unfavorably +# from the trade execution price. It uses a rational number for precision. +allowedSlippage: '2/100' + +# the maximum gas used to estimate cost of a xsswap trade. +gasLimitEstimate: 300000 + +# ttl: how long a trade is valid in seconds. After this time passes +# xsswap will not perform the trade, but the gas will still be sent. +ttl: 600 + +contractAddresses: + mainnet: + routerAddress: '0xBA12222222228d8Ba445958a75a0704d566BF2C8' + arbitrum: + routerAddress: '0xBA12222222228d8Ba445958a75a0704d566BF2C8' + optimism: + routerAddress: '0xBA12222222228d8Ba445958a75a0704d566BF2C8' + avalanche: + routerAddress: '0xBA12222222228d8Ba445958a75a0704d566BF2C8' + diff --git a/src/templates/root.yml b/src/templates/root.yml index 748ec134b2..d15d5b0028 100644 --- a/src/templates/root.yml +++ b/src/templates/root.yml @@ -131,3 +131,7 @@ configurations: $namespace carbon: configurationPath: carbon.yml schemaPath: carbon-schema.json + + $namespace balancer: + configurationPath: balancer.yml + schemaPath: cronos-connector-schema.json diff --git a/test-bronze/connectors/balancer/balancer.routes.test.ts b/test-bronze/connectors/balancer/balancer.routes.test.ts new file mode 100644 index 0000000000..25ad003c6d --- /dev/null +++ b/test-bronze/connectors/balancer/balancer.routes.test.ts @@ -0,0 +1,573 @@ +import request from 'supertest'; +import { patch, unpatch } from '../../../test/services/patch'; +import { gatewayApp } from '../../../src/app'; +import { Ethereum } from '../../../src/chains/ethereum/ethereum'; +import { Balancer } from '../../../src/connectors/balancer/balancer'; +import { patchEVMNonceManager } from '../../../test/evm.nonce.mock'; +import { BigNumber } from 'ethers'; + + +const SWAP_DATA = { + swapAmount: BigNumber.from(1), + swapAmountForSwaps: BigNumber.from(1), + returnAmount: BigNumber.from(1), + returnAmountFromSwaps: BigNumber.from(1), + returnAmountConsideringFees: BigNumber.from(1), + swaps: [ + { + poolId: "0x0b09dea16768f0799065c475be02919503cb2a3500020000000000000000001a", + assetInIndex: 0, + assetOutIndex: 1, + amount: "1000000000000000000", + userData: "0x", + returnAmount: "1000000000000000000", + }, + ], + tokenAddresses: [ + "0xd0A1E359811322d97991E03f863a0C30C2cF029C", + "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", + ], + tokenIn: "0xd0A1E359811322d97991E03f863a0C30C2cF029C", + tokenOut: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", + marketSp: "1.0", + tokenInForSwaps: "0xd0A1E359811322d97991E03f863a0C30C2cF029C", + tokenOutFromSwaps: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", +}; + +let ethereum: Ethereum; +let balancer: Balancer; + +beforeAll(async () => { + ethereum = Ethereum.getInstance('goerli'); + patchEVMNonceManager(ethereum.nonceManager); + await ethereum.init(); + + balancer = Balancer.getInstance('ethereum', 'goerli'); + await balancer.init(); +}); + +beforeEach(() => { + patchEVMNonceManager(ethereum.nonceManager); +}); + +afterEach(() => { + unpatch(); +}); + +afterAll(async () => { + await ethereum.close(); +}); + +const address: string = '0xFaA12FD102FE8623C9299c72B03E45107F2772B5'; + +const patchGetWallet = () => { + patch(ethereum, 'getWallet', () => { + return { + address: '0xFaA12FD102FE8623C9299c72B03E45107F2772B5', + }; + }); +}; + +const patchStoredTokenList = () => { + patch(ethereum, 'tokenList', () => { + return [ + { + chainId: 5, + name: 'WETH', + symbol: 'WETH', + address: '0xd0A1E359811322d97991E03f863a0C30C2cF029C', + decimals: 18, + }, + { + chainId: 5, + name: 'Wrapped AVAX', + symbol: 'WAVAX', + address: '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7', + decimals: 18, + }, + ]; + }); +}; + +const patchGetTokenBySymbol = () => { + patch(ethereum, 'getTokenBySymbol', (symbol: string) => { + if (symbol === 'WETH') { + return { + chainId: 5, + name: 'WETH', + symbol: 'WETH', + address: '0xd0A1E359811322d97991E03f863a0C30C2cF029C', + decimals: 18, + }; + } else { + return { + chainId: 42, + name: 'WAVAX', + symbol: 'WAVAX', + address: '0x4f96fe3b7a6cf9725f59d353f723c1bdb64ca6aa', + decimals: 18, + }; + } + }); +}; + +const patchGetTokenByAddress = () => { + patch(balancer, 'getTokenByAddress', () => { + return { + chainId: 5, + name: 'WETH', + symbol: 'WETH', + address: '0xd0A1E359811322d97991E03f863a0C30C2cF029C', + decimals: 18, + }; + }); +}; + +const patchGasPrice = () => { + patch(ethereum, 'gasPrice', () => 100); +}; + +const patchEstimateBuyTrade = () => { + patchEstimateSellTrade(); +}; + +const patchEstimateSellTrade = () => { + patch(balancer.balancer.swaps, 'fetchPools', () => { + return true; + }); + patch(balancer.balancer.swaps, 'findRouteGivenIn', async () => { + return SWAP_DATA; + }); + patch(balancer.balancer.swaps, 'findRouteGivenOut', async () => { + return SWAP_DATA; + }); +}; + +const patchGetNonce = () => { + patch(ethereum.nonceManager, 'getNonce', () => 21); +}; + +const patchExecuteTrade = () => { + patch(balancer, 'executeTrade', () => { + return { nonce: 21, hash: '000000000000000' }; + }); +}; + +describe('POST /amm/price', () => { + it('should return 200 for BUY', async () => { + patchGetWallet(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patchGasPrice(); + patchEstimateBuyTrade(); + patchGetNonce(); + patchExecuteTrade(); + + await request(gatewayApp) + .post(`/amm/price`) + .send({ + chain: 'ethereum', + network: 'goerli', + connector: 'balancer', + quote: 'WAVAX', + base: 'WETH', + amount: '10000', + side: 'BUY', + }) + .set('Accept', 'application/json') + .expect(200) + .then((res: any) => { + expect(res.body.amount).toEqual('10000.000000000000000000'); + expect(res.body.rawAmount).toEqual('10000000000000000000000'); + }); + }); + + it('should return 200 for SELL', async () => { + patchGetWallet(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patchGasPrice(); + patchEstimateSellTrade(); + patchGetNonce(); + patchExecuteTrade(); + + await request(gatewayApp) + .post(`/amm/price`) + .send({ + chain: 'ethereum', + network: 'goerli', + connector: 'balancer', + quote: 'WAVAX', + base: 'WETH', + amount: '10000', + side: 'SELL', + }) + .set('Accept', 'application/json') + .expect(200) + .then((res: any) => { + expect(res.body.amount).toEqual('10000.000000000000000000'); + expect(res.body.rawAmount).toEqual('10000000000000000000000'); + }); + }); + + it('should return 500 for unrecognized quote symbol', async () => { + patchGetWallet(); + patchStoredTokenList(); + patch(ethereum, 'getTokenBySymbol', (symbol: string) => { + if (symbol === 'WETH') { + return { + chainId: 5, + name: 'WETH', + symbol: 'WETH', + address: '0xd0A1E359811322d97991E03f863a0C30C2cF029C', + decimals: 18, + }; + } else { + return null; + } + }); + patchGetTokenByAddress(); + + await request(gatewayApp) + .post(`/amm/price`) + .send({ + chain: 'ethereum', + network: 'goerli', + connector: 'balancer', + quote: 'DOGE', + base: 'WETH', + amount: '10000', + side: 'SELL', + }) + .set('Accept', 'application/json') + .expect(500); + }); + + it('should return 500 for unrecognized base symbol', async () => { + patchGetWallet(); + patchStoredTokenList(); + patch(ethereum, 'getTokenBySymbol', (symbol: string) => { + if (symbol === 'WETH') { + return { + chainId: 5, + name: 'WETH', + symbol: 'WETH', + address: '0xd0A1E359811322d97991E03f863a0C30C2cF029C', + decimals: 18, + }; + } else { + return null; + } + }); + patchGetTokenByAddress(); + + await request(gatewayApp) + .post(`/amm/price`) + .send({ + chain: 'ethereum', + network: 'goerli', + connector: 'balancer', + quote: 'WAVAX', + base: 'SHIBA', + amount: '10000', + side: 'SELL', + }) + .set('Accept', 'application/json') + .expect(500); + }); +}); + +describe('POST /amm/trade', () => { + const patchForBuy = () => { + patchGetWallet(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patchGasPrice(); + patchEstimateBuyTrade(); + patchGetNonce(); + patchExecuteTrade(); + }; + it('should return 200 for BUY', async () => { + patchForBuy(); + await request(gatewayApp) + .post(`/amm/trade`) + .send({ + chain: 'ethereum', + network: 'goerli', + connector: 'balancer', + quote: 'WAVAX', + base: 'WETH', + amount: '10000', + address, + side: 'BUY', + nonce: 21, + }) + .set('Accept', 'application/json') + .expect(200) + .then((res: any) => { + expect(res.body.nonce).toEqual(21); + }); + }); + + it('should return 200 for BUY without nonce parameter', async () => { + patchForBuy(); + await request(gatewayApp) + .post(`/amm/trade`) + .send({ + chain: 'ethereum', + network: 'goerli', + connector: 'balancer', + quote: 'WAVAX', + base: 'WETH', + amount: '10000', + address, + side: 'BUY', + }) + .set('Accept', 'application/json') + .expect(200); + }); + + it('should return 200 for BUY with maxFeePerGas and maxPriorityFeePerGas', async () => { + patchForBuy(); + await request(gatewayApp) + .post(`/amm/trade`) + .send({ + chain: 'ethereum', + network: 'goerli', + connector: 'balancer', + quote: 'WAVAX', + base: 'WETH', + amount: '10000', + address, + side: 'BUY', + nonce: 21, + maxFeePerGas: '5000000000', + maxPriorityFeePerGas: '5000000000', + }) + .set('Accept', 'application/json') + .expect(200); + }); + + const patchForSell = () => { + patchGetWallet(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patchGasPrice(); + patchEstimateSellTrade(); + patchGetNonce(); + patchExecuteTrade(); + }; + it('should return 200 for SELL', async () => { + patchForSell(); + await request(gatewayApp) + .post(`/amm/trade`) + .send({ + chain: 'ethereum', + network: 'goerli', + connector: 'balancer', + quote: 'WAVAX', + base: 'WETH', + amount: '10000', + address, + side: 'SELL', + nonce: 21, + }) + .set('Accept', 'application/json') + .expect(200) + .then((res: any) => { + expect(res.body.nonce).toEqual(21); + }); + }); + + it('should return 200 for SELL with maxFeePerGas and maxPriorityFeePerGas', async () => { + patchForSell(); + await request(gatewayApp) + .post(`/amm/trade`) + .send({ + chain: 'ethereum', + network: 'goerli', + connector: 'balancer', + quote: 'WAVAX', + base: 'WETH', + amount: '10000', + address, + side: 'SELL', + nonce: 21, + maxFeePerGas: '5000000000', + maxPriorityFeePerGas: '5000000000', + }) + .set('Accept', 'application/json') + .expect(200); + }); + + it('should return 404 when parameters are incorrect', async () => { + await request(gatewayApp) + .post(`/amm/trade`) + .send({ + chain: 'ethereum', + network: 'goerli', + connector: 'balancer', + quote: 'WAVAX', + base: 'WETH', + amount: 10000, + address: 'da8', + side: 'comprar', + }) + .set('Accept', 'application/json') + .expect(404); + }); + + it('should return 500 when base token is unknown', async () => { + patchForSell(); + patch(ethereum, 'getTokenBySymbol', (symbol: string) => { + if (symbol === 'WETH') { + return { + chainId: 5, + name: 'WETH', + symbol: 'WETH', + address: '0xd0A1E359811322d97991E03f863a0C30C2cF029C', + decimals: 18, + }; + } else { + return null; + } + }); + + await request(gatewayApp) + .post(`/amm/trade`) + .send({ + chain: 'ethereum', + network: 'goerli', + connector: 'balancer', + quote: 'WETH', + base: 'BITCOIN', + amount: '10000', + address, + side: 'BUY', + nonce: 21, + maxFeePerGas: '5000000000', + maxPriorityFeePerGas: '5000000000', + }) + .set('Accept', 'application/json') + .expect(500); + }); + + it('should return 500 when quote token is unknown', async () => { + patchForSell(); + patch(ethereum, 'getTokenBySymbol', (symbol: string) => { + if (symbol === 'WETH') { + return { + chainId: 5, + name: 'WETH', + symbol: 'WETH', + address: '0xd0A1E359811322d97991E03f863a0C30C2cF029C', + decimals: 18, + }; + } else { + return null; + } + }); + + await request(gatewayApp) + .post(`/amm/trade`) + .send({ + chain: 'ethereum', + network: 'goerli', + connector: 'balancer', + quote: 'BITCOIN', + base: 'WETH', + amount: '10000', + address, + side: 'BUY', + nonce: 21, + maxFeePerGas: '5000000000', + maxPriorityFeePerGas: '5000000000', + }) + .set('Accept', 'application/json') + .expect(500); + }); + + it('should return 200 for SELL with limitPrice', async () => { + patchForSell(); + await request(gatewayApp) + .post(`/amm/trade`) + .send({ + chain: 'ethereum', + network: 'goerli', + connector: 'balancer', + quote: 'WAVAX', + base: 'WETH', + amount: '10000', + address, + side: 'SELL', + nonce: 21, + limitPrice: '0.5', + }) + .set('Accept', 'application/json') + .expect(200); + }); + + it('should return 200 for BUY with limitPrice', async () => { + patchForBuy(); + await request(gatewayApp) + .post(`/amm/trade`) + .send({ + chain: 'ethereum', + network: 'goerli', + connector: 'balancer', + quote: 'WAVAX', + base: 'WETH', + amount: '10000', + address, + side: 'BUY', + nonce: 21, + limitPrice: '999999999999999999999', + }) + .set('Accept', 'application/json') + .expect(200); + }); + + it('should return 200 for SELL with price higher than limitPrice', async () => { + patchForSell(); + await request(gatewayApp) + .post(`/amm/trade`) + .send({ + chain: 'ethereum', + network: 'goerli', + connector: 'balancer', + quote: 'WAVAX', + base: 'WETH', + amount: '10000', + address, + side: 'SELL', + nonce: 21, + limitPrice: '99999999999', + }) + .set('Accept', 'application/json') + .expect(500); + }); + + it('should return 200 for BUY with price less than limitPrice', async () => { + patchForBuy(); + await request(gatewayApp) + .post(`/amm/trade`) + .send({ + chain: 'ethereum', + network: 'goerli', + connector: 'balancer', + quote: 'WAVAX', + base: 'WETH', + amount: '10000', + address, + side: 'BUY', + nonce: 21, + limitPrice: '0.5', + }) + .set('Accept', 'application/json') + .expect(500); + }); +}); diff --git a/test-bronze/connectors/balancer/balancer.test.ts b/test-bronze/connectors/balancer/balancer.test.ts new file mode 100644 index 0000000000..789c44c0d4 --- /dev/null +++ b/test-bronze/connectors/balancer/balancer.test.ts @@ -0,0 +1,148 @@ +jest.useFakeTimers(); +import { Balancer } from '../../../src/connectors/balancer/balancer'; +import { patch, unpatch } from '../../../test/services/patch'; +import { UniswapishPriceError } from '../../../src/services/error-handler'; +import { BigNumber } from 'ethers'; +import { Ethereum } from '../../../src/chains/ethereum/ethereum'; +import { patchEVMNonceManager } from '../../../test/evm.nonce.mock'; +// import { Percent } from '@uniswap/sdk-core'; +import { Token } from '@uniswap/sdk'; +let ethereum: Ethereum; +let balancer: Balancer; + + +const SWAP_DATA = { + swapAmount: BigNumber.from(1), + swapAmountForSwaps: BigNumber.from(1), + returnAmount: BigNumber.from(1), + returnAmountFromSwaps: BigNumber.from(1), + returnAmountConsideringFees: BigNumber.from(1), + swaps: [ + { + poolId: "0x0b09dea16768f0799065c475be02919503cb2a3500020000000000000000001a", + assetInIndex: 0, + assetOutIndex: 1, + amount: "1000000000000000000", + userData: "0x", + returnAmount: "1000000000000000000", + }, + ], + tokenAddresses: [ + "0xd0A1E359811322d97991E03f863a0C30C2cF029C", + "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", + ], + tokenIn: "0xd0A1E359811322d97991E03f863a0C30C2cF029C", + tokenOut: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", + marketSp: "1.0", + tokenInForSwaps: "0xd0A1E359811322d97991E03f863a0C30C2cF029C", + tokenOutFromSwaps: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", +}; + +const WETH = new Token( + 5, + '0xd0A1E359811322d97991E03f863a0C30C2cF029C', + 18, + 'WETH' +); +const WAVAX = new Token( + 5, + '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7', + 18, + 'WAVAX' +); + +beforeAll(async () => { + ethereum = Ethereum.getInstance('goerli'); + patchEVMNonceManager(ethereum.nonceManager); + await ethereum.init(); + + balancer = Balancer.getInstance('ethereum', 'goerli'); + await balancer.init(); +}); + +beforeEach(() => { + patchEVMNonceManager(ethereum.nonceManager); +}); + +afterEach(() => { + unpatch(); +}); + +afterAll(async () => { + await ethereum.close(); +}); + +const patchTrade = (_key: string, error?: Error) => { + patch(balancer.balancer.swaps, 'fetchPools', () => { + return true; + }); + patch(balancer.balancer.swaps, 'findRouteGivenIn', async () => { + if (error) return {swaps: []}; + return SWAP_DATA; + }); + patch(balancer.balancer.swaps, 'findRouteGivenOut', async () => { + if (error) return {swaps: []}; + return SWAP_DATA; + }); +}; + +describe('verify Balancer estimateSellTrade', () => { + it('Should return an ExpectedTrade when available', async () => { + patchTrade('bestTradeExactIn'); + + const expectedTrade = await balancer.estimateSellTrade( + WETH, + WAVAX, + BigNumber.from(1) + ); + expect(expectedTrade).toHaveProperty('trade'); + expect(expectedTrade).toHaveProperty('expectedAmount'); + }); + + it('Should throw an error if no pair is available', async () => { + patchTrade('bestTradeExactIn', new Error('error getting trade')); + + await expect(async () => { + await balancer.estimateSellTrade(WETH, WAVAX, BigNumber.from(1)); + }).rejects.toThrow(UniswapishPriceError); + }); +}); + +describe('verify Balancer estimateBuyTrade', () => { + it('Should return an ExpectedTrade when available', async () => { + patchTrade('bestTradeExactOut'); + + const expectedTrade = await balancer.estimateBuyTrade( + WETH, + WAVAX, + BigNumber.from(1) + ); + expect(expectedTrade).toHaveProperty('trade'); + expect(expectedTrade).toHaveProperty('expectedAmount'); + }); + + it('Should return an error if no pair is available', async () => { + patchTrade('bestTradeExactOut', new Error('error getting trade')); + + await expect(async () => { + await balancer.estimateBuyTrade(WETH, WAVAX, BigNumber.from(1)); + }).rejects.toThrow(UniswapishPriceError); + }); +}); + +describe('getAllowedSlippage', () => { + it('return value of string when not null', () => { + const allowedSlippage = balancer.getAllowedSlippage('3/100'); + expect(allowedSlippage).toEqual(3); + }); + + it('return value from config when string is null', () => { + const allowedSlippage = balancer.getAllowedSlippage(); + expect(allowedSlippage).toEqual(2); + }); + + it('return value from config when string is malformed', () => { + const allowedSlippage = balancer.getAllowedSlippage('yo'); + expect(allowedSlippage).toEqual(2); + }); +}); diff --git a/yarn.lock b/yarn.lock index be94faef31..b1c88a054d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -665,6 +665,34 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@balancer-labs/sdk@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@balancer-labs/sdk/-/sdk-1.1.5.tgz#29e70d15d7e98899831b18d8b909b6569e062758" + integrity sha512-Hxmo1u8qJureQxdbVAMAkN9qceeZxcKx4QjoevTDMrSGY5H9jc1hX18p9TTZXwlGAkw8n4rdErydUgsdWjWuLA== + dependencies: + "@balancer-labs/sor" "^4.1.1-beta.16" + "@ethersproject/abi" "^5.4.0" + "@ethersproject/abstract-signer" "^5.4.0" + "@ethersproject/address" "^5.4.0" + "@ethersproject/base64" "5.5.0" + "@ethersproject/bignumber" "^5.4.0" + "@ethersproject/bytes" "^5.4.0" + "@ethersproject/constants" "^5.4.0" + "@ethersproject/contracts" "^5.4.0" + "@ethersproject/providers" "^5.4.5" + axios "^0.24.0" + graphql "^15.6.1" + graphql-request "^3.5.0" + json-to-graphql-query "^2.2.4" + lodash "^4.17.21" + +"@balancer-labs/sor@^4.1.1-beta.16": + version "4.1.1-beta.17" + resolved "https://registry.yarnpkg.com/@balancer-labs/sor/-/sor-4.1.1-beta.17.tgz#8c404a86174003cccf2bb87d49221cfdcf083246" + integrity sha512-JcX/HeppyoIs+Sa3Z/pdZhqMOBAGajOwVkBkFA8rehd1K2qaU/k/a3OkbIidXjs4lQI9sJE1WO8RauCLtuLQfg== + dependencies: + isomorphic-fetch "^2.2.1" + "@bancor/carbon-sdk@^0.0.93-DEV": version "0.0.93-DEV" resolved "https://registry.yarnpkg.com/@bancor/carbon-sdk/-/carbon-sdk-0.0.93-DEV.tgz#2071f1dc03c25c3897fd4cd9133f9345abdeb2db" @@ -1762,6 +1790,13 @@ "@ethersproject/logger" "^5.7.0" "@ethersproject/rlp" "^5.7.0" +"@ethersproject/base64@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.5.0.tgz#881e8544e47ed976930836986e5eb8fab259c090" + integrity sha512-tdayUKhU1ljrlHzEWbStXazDpsx4eg1dBXUSI6+mHlYklOXoXF6lZvw8tnD6oVaWfnMxAgRSKROg3cVKtCcppA== + dependencies: + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/base64@5.7.0", "@ethersproject/base64@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.7.0.tgz#ac4ee92aa36c1628173e221d0d01f53692059e1c" @@ -1786,14 +1821,14 @@ "@ethersproject/logger" "^5.7.0" bn.js "^5.2.1" -"@ethersproject/bytes@5.7.0", "@ethersproject/bytes@^5.4.0", "@ethersproject/bytes@^5.7.0": +"@ethersproject/bytes@5.7.0", "@ethersproject/bytes@^5.4.0", "@ethersproject/bytes@^5.5.0", "@ethersproject/bytes@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.7.0.tgz#a00f6ea8d7e7534d6d87f47188af1148d71f155d" integrity sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A== dependencies: "@ethersproject/logger" "^5.7.0" -"@ethersproject/constants@5.7.0", "@ethersproject/constants@^5.7.0": +"@ethersproject/constants@5.7.0", "@ethersproject/constants@^5.4.0", "@ethersproject/constants@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.7.0.tgz#df80a9705a7e08984161f09014ea012d1c75295e" integrity sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA== @@ -1945,7 +1980,7 @@ bech32 "1.1.4" ws "7.4.6" -"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.0.4", "@ethersproject/providers@^5.4.0", "@ethersproject/providers@^5.7.2": +"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.0.4", "@ethersproject/providers@^5.4.0", "@ethersproject/providers@^5.4.5", "@ethersproject/providers@^5.7.2": version "5.7.2" resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb" integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg== @@ -5817,6 +5852,13 @@ axios@^0.21.1, axios@^0.21.2: dependencies: follow-redirects "^1.14.0" +axios@^0.24.0: + version "0.24.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6" + integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA== + dependencies: + follow-redirects "^1.14.4" + axios@^0.26.0: version "0.26.1" resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" @@ -7634,6 +7676,13 @@ encoding-down@^6.3.0: level-codec "^9.0.0" level-errors "^2.0.0" +encoding@^0.1.11: + version "0.1.13" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + end-of-stream@^1.0.0, end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -8728,6 +8777,11 @@ follow-redirects@^1.12.1, follow-redirects@^1.14.0, follow-redirects@^1.14.8, fo resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf" integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw== +follow-redirects@^1.14.4: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -9250,7 +9304,7 @@ graphql-request@5.0.0: extract-files "^9.0.0" form-data "^3.0.0" -graphql-request@^3.4.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== @@ -9274,7 +9328,7 @@ graphql-tag@^2.11.0, graphql-tag@^2.12.6: dependencies: tslib "^2.1.0" -graphql@^15.3.0, graphql@^15.5.0: +graphql@^15.3.0, graphql@^15.5.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== @@ -9653,6 +9707,13 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@^0.6.2: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + idna-uts46-hx@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/idna-uts46-hx/-/idna-uts46-hx-2.3.1.tgz#a1dc5c4df37eee522bf66d969cc980e00e8711f9" @@ -10050,7 +10111,7 @@ is-shared-array-buffer@^1.0.2: dependencies: call-bind "^1.0.2" -is-stream@^1.0.0, is-stream@^1.1.0: +is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== @@ -10146,6 +10207,14 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== +isomorphic-fetch@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" + integrity sha512-9c4TNAKYXM5PRyVcwUZrF3W09nQ+sO7+jydgs4ZGW9dhsLG2VOlISJABombdQqQRXCwuYG3sYV/puGf5rp0qmA== + dependencies: + node-fetch "^1.0.1" + whatwg-fetch ">=0.10.0" + isomorphic-unfetch@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz#87341d5f4f7b63843d468438128cb087b7c3e98f" @@ -10823,6 +10892,11 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== +json-to-graphql-query@^2.2.4: + version "2.2.5" + resolved "https://registry.yarnpkg.com/json-to-graphql-query/-/json-to-graphql-query-2.2.5.tgz#56b072a693b50fd4dc981367b60d52e3dc78f426" + integrity sha512-5Nom9inkIMrtY992LMBBG1Zaekrc10JaRhyZgprwHBVMDtRgllTvzl0oBbg13wJsVZoSoFNNMaeIVQs0P04vsA== + json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" @@ -12057,6 +12131,14 @@ node-fetch@2.6.7: dependencies: whatwg-url "^5.0.0" +node-fetch@^1.0.1: + version "1.7.3" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" + integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + node-fetch@^2.6.1, node-fetch@^2.6.7: version "2.6.9" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" @@ -13674,7 +13756,7 @@ safe-stable-stringify@^2.3.1: resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -15897,6 +15979,11 @@ websocket@^1.0.28, websocket@^1.0.32: typedarray-to-buffer "^3.1.5" yaeti "^0.0.6" +whatwg-fetch@>=0.10.0: + version "3.6.20" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" + integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg== + whatwg-mimetype@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7"