From 157d49c2ece86b55f4a3f14a1b88017dea659ae3 Mon Sep 17 00:00:00 2001 From: vic-en Date: Wed, 24 Jul 2024 00:21:52 -0500 Subject: [PATCH 01/13] add lp connector --- src/chains/etc/etc.ts | 115 ++++++ src/chains/ethereum/ethereum.validators.ts | 1 + src/connectors/connectors.routes.ts | 8 + src/connectors/etcswap/etcswap.config.ts | 45 +++ src/connectors/etcswap/etcswap.lp.helper.ts | 421 ++++++++++++++++++++ src/connectors/etcswap/etcswap.lp.ts | 261 ++++++++++++ src/network/network.controllers.ts | 6 + src/services/connection-manager.ts | 7 + src/services/schema/etcswap-schema.json | 43 ++ src/templates/etc.yml | 11 + src/templates/etcswap.yml | 31 ++ src/templates/lists/etc.json | 228 +++++++++++ src/templates/root.yml | 8 + 13 files changed, 1185 insertions(+) create mode 100644 src/chains/etc/etc.ts create mode 100644 src/connectors/etcswap/etcswap.config.ts create mode 100644 src/connectors/etcswap/etcswap.lp.helper.ts create mode 100644 src/connectors/etcswap/etcswap.lp.ts create mode 100644 src/services/schema/etcswap-schema.json create mode 100644 src/templates/etc.yml create mode 100644 src/templates/etcswap.yml create mode 100644 src/templates/lists/etc.json diff --git a/src/chains/etc/etc.ts b/src/chains/etc/etc.ts new file mode 100644 index 0000000000..5e67ef9f26 --- /dev/null +++ b/src/chains/etc/etc.ts @@ -0,0 +1,115 @@ +import abi from '../ethereum/ethereum.abi.json'; +import { logger } from '../../services/logger'; +import { Contract, Transaction, Wallet } from 'ethers'; +import { EthereumBase } from '../ethereum/ethereum-base'; +import { getEthereumConfig as getETCChainConfig } from '../ethereum/ethereum.config'; +import { Provider } from '@ethersproject/abstract-provider'; +import { Chain as Ethereumish } from '../../services/common-interfaces'; +import { ConfigManagerV2 } from '../../services/config-manager-v2'; +import { EVMController } from '../ethereum/evm.controllers'; +import { ETCSwapConfig } from '../../connectors/etcswap/etcswap.config'; + +export class ETCChain extends EthereumBase implements Ethereumish { + private static _instances: { [name: string]: ETCChain }; + private _chain: string; + private _gasPrice: number; + private _gasPriceRefreshInterval: number | null; + private _nativeTokenSymbol: string; + public controller; + + private constructor(network: string) { + const config = getETCChainConfig('etc', network); + super( + 'etc', + config.network.chainID, + config.network.nodeURL, + config.network.tokenListSource, + config.network.tokenListType, + config.manualGasPrice, + config.gasLimitTransaction, + ConfigManagerV2.getInstance().get('server.nonceDbPath'), + ConfigManagerV2.getInstance().get('server.transactionDbPath') + ); + this._chain = config.network.name; + this._nativeTokenSymbol = config.nativeCurrencySymbol; + this._gasPrice = config.manualGasPrice; + this._gasPriceRefreshInterval = + config.network.gasPriceRefreshInterval !== undefined + ? config.network.gasPriceRefreshInterval + : null; + + this.updateGasPrice(); + this.controller = EVMController; + } + + public static getInstance(network: string): ETCChain { + if (ETCChain._instances === undefined) { + ETCChain._instances = {}; + } + if (!(network in ETCChain._instances)) { + ETCChain._instances[network] = new ETCChain(network); + } + + return ETCChain._instances[network]; + } + + public static getConnectedInstances(): { [name: string]: ETCChain } { + return ETCChain._instances; + } + + /** + * Automatically update the prevailing gas price on the network from the connected RPC node. + */ + async updateGasPrice(): Promise { + if (this._gasPriceRefreshInterval === null) { + return; + } + + const gasPrice: number = (await this.provider.getGasPrice()).toNumber(); + + this._gasPrice = gasPrice * 1e-9; + + setTimeout( + this.updateGasPrice.bind(this), + this._gasPriceRefreshInterval * 1000 + ); + } + + // getters + + public get gasPrice(): number { + return this._gasPrice; + } + + public get nativeTokenSymbol(): string { + return this._nativeTokenSymbol; + } + + public get chain(): string { + return this._chain; + } + + getContract(tokenAddress: string, signerOrProvider?: Wallet | Provider) { + return new Contract(tokenAddress, abi.ERC20Abi, signerOrProvider); + } + + getSpender(reqSpender: string): string { + let spender: string; + if (reqSpender === 'etcswapLP') { + spender = ETCSwapConfig.config.etcswapV3NftManagerAddress( + this._chain + ); + } else { + spender = reqSpender; + } + return spender; + } + + // cancel transaction + async cancelTx(wallet: Wallet, nonce: number): Promise { + logger.info( + 'Canceling any existing transaction(s) with nonce number ' + nonce + '.' + ); + return super.cancelTxWithGasPrice(wallet, nonce, this._gasPrice * 2); + } +} diff --git a/src/chains/ethereum/ethereum.validators.ts b/src/chains/ethereum/ethereum.validators.ts index 9a6e6aed7e..d7bd684bde 100644 --- a/src/chains/ethereum/ethereum.validators.ts +++ b/src/chains/ethereum/ethereum.validators.ts @@ -65,6 +65,7 @@ export const validateSpender: Validator = mkValidator( val === 'curve' || val === 'carbonamm' || val === 'balancer' || + val === 'etcswapLP' || isAddress(val)) ); diff --git a/src/connectors/connectors.routes.ts b/src/connectors/connectors.routes.ts index e03e551f0f..708fd52306 100644 --- a/src/connectors/connectors.routes.ts +++ b/src/connectors/connectors.routes.ts @@ -25,6 +25,7 @@ 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'; +import { ETCSwapConfig } from './etcswap/etcswap.config'; export namespace ConnectorsRoutes { export const router = Router(); @@ -190,6 +191,13 @@ export namespace ConnectorsRoutes { chain_type: BalancerConfig.config.chainType, available_networks: BalancerConfig.config.availableNetworks, }, + { + name: 'etcswapLP', + trading_type: ETCSwapConfig.config.tradingTypes('LP'), + chain_type: ETCSwapConfig.config.chainType, + available_networks: ETCSwapConfig.config.availableNetworks, + additional_spenders: ['etcswap'], + }, ], }); }) diff --git a/src/connectors/etcswap/etcswap.config.ts b/src/connectors/etcswap/etcswap.config.ts new file mode 100644 index 0000000000..87645d43cb --- /dev/null +++ b/src/connectors/etcswap/etcswap.config.ts @@ -0,0 +1,45 @@ +import { + buildConfig, + NetworkConfig as V2NetworkConfig, +} from '../../network/network.utils'; +import { ConfigManagerV2 } from '../../services/config-manager-v2'; + +export namespace ETCSwapConfig { + export interface NetworkConfig extends Omit { + maximumHops: number; + etcswapV3SmartOrderRouterAddress: (network: string) => string; + etcswapV3NftManagerAddress: (network: string) => string; + tradingTypes: (type: string) => Array; + useRouter?: boolean; + feeTier?: string; + } + + export const v2Config: V2NetworkConfig = buildConfig( + 'etcswap', + ['AMM'], + [ + { chain: 'etc', networks: ['mainnet'] }, + ], + 'EVM', + ); + + export const config: NetworkConfig = { + ...v2Config, + ...{ + maximumHops: ConfigManagerV2.getInstance().get(`etcswap.maximumHops`), + etcswapV3SmartOrderRouterAddress: (network: string) => + ConfigManagerV2.getInstance().get( + `etcswap.contractAddresses.${network}.etcswapV3SmartOrderRouterAddress`, + ), + etcswapV3NftManagerAddress: (network: string) => + ConfigManagerV2.getInstance().get( + `etcswap.contractAddresses.${network}.etcswapV3NftManagerAddress`, + ), + tradingTypes: (type: string) => { + return type === 'swap' ? ['AMM'] : ['AMM_LP']; + }, + useRouter: ConfigManagerV2.getInstance().get(`etcswap.useRouter`), + feeTier: ConfigManagerV2.getInstance().get(`etcswap.feeTier`), + }, + }; +} diff --git a/src/connectors/etcswap/etcswap.lp.helper.ts b/src/connectors/etcswap/etcswap.lp.helper.ts new file mode 100644 index 0000000000..8c491da44b --- /dev/null +++ b/src/connectors/etcswap/etcswap.lp.helper.ts @@ -0,0 +1,421 @@ +import { + InitializationError, + SERVICE_UNITIALIZED_ERROR_CODE, + SERVICE_UNITIALIZED_ERROR_MESSAGE, +} from '../../services/error-handler'; +import { Contract, ContractInterface } from '@ethersproject/contracts'; +import { Token, CurrencyAmount, Percent, Price } from '@uniswap/sdk-core'; +import * as v3 from '@uniswap/v3-sdk'; +import { providers, Wallet, Signer, utils } from 'ethers'; +import { percentRegexp } from '../../services/config-manager-v2'; +import { + PoolState, + RawPosition, + AddPosReturn, +} from '../uniswap/uniswap.lp.interfaces'; +import * as math from 'mathjs'; +import { getAddress } from 'ethers/lib/utils'; +import { ETCSwapConfig } from './etcswap.config'; +import { ETCChain } from '../../chains/etc/etc'; + +export const FACTORY = "0x2624E907BcC04f93C8f29d7C7149a8700Ceb8cDC"; +export const POOL_INIT = "0x7ea2da342810af3c5a9b47258f990aaac829fe1385a1398feb77d0126a85dbef"; + +export class ETCSwapLPHelper { + protected chain: ETCChain; + protected chainId; + private _router: string; + private _nftManager: string; + private _ttl: number; + private _routerAbi: ContractInterface; + private _nftAbi: ContractInterface; + private _poolAbi: ContractInterface; + private tokenList: Record = {}; + private _ready: boolean = false; + public abiDecoder: any; + + constructor(chain: string, network: string) { + this.chain = ETCChain.getInstance(network); + this.chainId = this.getChainId(chain, network); + this._router = + ETCSwapConfig.config.etcswapV3SmartOrderRouterAddress(network); + this._nftManager = + ETCSwapConfig.config.etcswapV3NftManagerAddress(network); + this._ttl = ETCSwapConfig.config.ttl; + this._routerAbi = + require('@uniswap/v3-periphery/artifacts/contracts/SwapRouter.sol/SwapRouter.json').abi; + this._nftAbi = + require('@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json').abi; + this._poolAbi = + require('@uniswap/v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json').abi; + this.abiDecoder = require('abi-decoder'); + this.abiDecoder.addABI(this._nftAbi); + this.abiDecoder.addABI(this._routerAbi); + } + + public ready(): boolean { + return this._ready; + } + + public get router(): string { + return this._router; + } + + public get nftManager(): string { + return this._nftManager; + } + + public get ttl(): number { + return parseInt(String(Date.now() / 1000)) + this._ttl; + } + + public get routerAbi(): ContractInterface { + return this._routerAbi; + } + + public get nftAbi(): ContractInterface { + return this._nftAbi; + } + + public get poolAbi(): ContractInterface { + return this._poolAbi; + } + + /** + * 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)]; + } + + public async init() { + const chainName = this.chain.toString(); + if (!this.chain.ready()) + throw new InitializationError( + SERVICE_UNITIALIZED_ERROR_MESSAGE(chainName), + SERVICE_UNITIALIZED_ERROR_CODE, + ); + for (const token of this.chain.storedTokenList) { + this.tokenList[token.address] = new Token( + this.chainId, + token.address, + token.decimals, + token.symbol, + token.name, + ); + } + this._ready = true; + } + + public getChainId(_chain: string, network: string): number { + return ETCChain.getInstance(network).chainId; + } + + getPercentage(rawPercent: number | string): Percent { + const slippage = math.fraction(rawPercent) as math.Fraction; + return new Percent(slippage.n, slippage.d * 100); + } + + getSlippagePercentage(): Percent { + const allowedSlippage = ETCSwapConfig.config.allowedSlippage; + const nd = allowedSlippage.match(percentRegexp); + if (nd) return new Percent(nd[1], nd[2]); + throw new Error( + 'Encountered a malformed percent string in the config for ALLOWED_SLIPPAGE.', + ); + } + + getContract( + contract: string, + signer: providers.StaticJsonRpcProvider | Signer, + ): Contract { + if (contract === 'router') { + return new Contract(this.router, this.routerAbi, signer); + } else { + return new Contract(this.nftManager, this.nftAbi, signer); + } + } + + getPoolContract( + pool: string, + wallet: providers.StaticJsonRpcProvider | Signer, + ): Contract { + return new Contract(pool, this.poolAbi, wallet); + } + + async getPoolState( + poolAddress: string, + fee: v3.FeeAmount, + ): Promise { + const poolContract = this.getPoolContract(poolAddress, this.chain.provider); + const minTick = v3.nearestUsableTick( + v3.TickMath.MIN_TICK, + v3.TICK_SPACINGS[fee], + ); + const maxTick = v3.nearestUsableTick( + v3.TickMath.MAX_TICK, + v3.TICK_SPACINGS[fee], + ); + const poolDataReq = await Promise.allSettled([ + poolContract.liquidity(), + poolContract.slot0(), + poolContract.ticks(minTick), + poolContract.ticks(maxTick), + ]); + + const rejected = poolDataReq.filter( + (r) => r.status === 'rejected', + ) as PromiseRejectedResult[]; + + if (rejected.length > 0) throw new Error('Unable to fetch pool state'); + + const poolData = ( + poolDataReq.filter( + (r) => r.status === 'fulfilled', + ) as PromiseFulfilledResult[] + ).map((r) => r.value); + + return { + liquidity: poolData[0], + sqrtPriceX96: poolData[1][0], + tick: poolData[1][1], + observationIndex: poolData[1][2], + observationCardinality: poolData[1][3], + observationCardinalityNext: poolData[1][4], + feeProtocol: poolData[1][5], + unlocked: poolData[1][6], + fee: fee, + tickProvider: [ + { + index: minTick, + liquidityNet: poolData[2][1], + liquidityGross: poolData[2][0], + }, + { + index: maxTick, + liquidityNet: poolData[3][1], + liquidityGross: poolData[3][0], + }, + ], + }; + } + + async poolPrice( + token0: Token, + token1: Token, + tier: string, + period: number = 1, + interval: number = 1, + ): Promise { + const fetchPriceTime = []; + const prices = []; + const fee = v3.FeeAmount[tier as keyof typeof v3.FeeAmount]; + const poolContract = new Contract( + v3.Pool.getAddress(token0, token1, fee, POOL_INIT, FACTORY), + this.poolAbi, + this.chain.provider, + ); + for ( + let x = Math.ceil(period / interval) * interval; + x >= 0; + x -= interval + ) { + fetchPriceTime.push(x); + } + try { + const response = await poolContract.observe(fetchPriceTime); + for (let twap = 0; twap < response.tickCumulatives.length - 1; twap++) { + prices.push( + v3 + .tickToPrice( + token0, + token1, + Math.ceil( + response.tickCumulatives[twap + 1].sub( + response.tickCumulatives[twap].toNumber(), + ) / interval, + ), + ) + .toFixed(8), + ); + } + } catch (e) { + return ['0']; + } + return prices; + } + + async getRawPosition(wallet: Wallet, tokenId: number): Promise { + const contract = this.getContract('nft', wallet); + const requests = [contract.positions(tokenId)]; + const positionInfoReq = await Promise.allSettled(requests); + const rejected = positionInfoReq.filter( + (r) => r.status === 'rejected', + ) as PromiseRejectedResult[]; + if (rejected.length > 0) throw new Error('Unable to fetch position'); + const positionInfo = ( + positionInfoReq.filter( + (r) => r.status === 'fulfilled', + ) as PromiseFulfilledResult[] + ).map((r) => r.value); + return positionInfo[0]; + } + + getReduceLiquidityData( + percent: number, + tokenId: number, + token0: Token, + token1: Token, + wallet: Wallet, + ): v3.RemoveLiquidityOptions { + // }; // recipient: string; // expectedCurrencyOwed1: CurrencyAmount; // expectedCurrencyOwed0: CurrencyAmount; // collectOptions: { // burnToken: boolean; // deadline: number; // slippageTolerance: Percent; // liquidityPercentage: Percent; // tokenId: number; // { + return { + tokenId: tokenId, + liquidityPercentage: this.getPercentage(percent), + slippageTolerance: this.getSlippagePercentage(), + deadline: this.ttl, + burnToken: false, + collectOptions: { + expectedCurrencyOwed0: CurrencyAmount.fromRawAmount(token0, '0'), + expectedCurrencyOwed1: CurrencyAmount.fromRawAmount(token1, '0'), + recipient: <`0x${string}`>wallet.address, + }, + }; + } + + async addPositionHelper( + wallet: Wallet, + token0: Token, + token1: Token, + amount0: string, + amount1: string, + fee: v3.FeeAmount, + lowerPrice: number, + upperPrice: number, + tokenId: number = 0, + ): Promise { + if (token1.sortsBefore(token0)) { + [token0, token1] = [token1, token0]; + [amount0, amount1] = [amount1, amount0]; + [lowerPrice, upperPrice] = [1 / upperPrice, 1 / lowerPrice]; + } + const lowerPriceInFraction = math.fraction(lowerPrice) as math.Fraction; + const upperPriceInFraction = math.fraction(upperPrice) as math.Fraction; + const poolData = await this.getPoolState( + v3.Pool.getAddress(token0, token1, fee, POOL_INIT, FACTORY), + fee, + ); + const pool = new v3.Pool( + token0, + token1, + poolData.fee, + poolData.sqrtPriceX96.toString(), + poolData.liquidity.toString(), + poolData.tick, + ); + + const addLiquidityOptions = { + recipient: wallet.address, + tokenId: tokenId ? tokenId : 0, + }; + + const swapOptions = { + recipient: wallet.address, + slippageTolerance: this.getSlippagePercentage(), + deadline: this.ttl, + }; + + const tickLower = v3.nearestUsableTick( + v3.priceToClosestTick( + new Price( + token0, + token1, + utils + .parseUnits(lowerPriceInFraction.d.toString(), token0.decimals) + .toString(), + utils + .parseUnits(lowerPriceInFraction.n.toString(), token1.decimals) + .toString(), + ), + ), + v3.TICK_SPACINGS[fee], + ); + + const tickUpper = v3.nearestUsableTick( + v3.priceToClosestTick( + new Price( + token0, + token1, + utils + .parseUnits(upperPriceInFraction.d.toString(), token0.decimals) + .toString(), + utils + .parseUnits(upperPriceInFraction.n.toString(), token1.decimals) + .toString(), + ), + ), + v3.TICK_SPACINGS[fee], + ); + + const position = v3.Position.fromAmounts({ + pool: pool, + tickLower: + tickLower === tickUpper ? tickLower - v3.TICK_SPACINGS[fee] : tickLower, + tickUpper: tickUpper, + amount0: utils.parseUnits(amount0, token0.decimals).toString(), + amount1: utils.parseUnits(amount1, token1.decimals).toString(), + useFullPrecision: true, + }); + + const methodParameters = v3.NonfungiblePositionManager.addCallParameters( + position, + { ...swapOptions, ...addLiquidityOptions }, + ); + return { ...methodParameters, swapRequired: false }; + } + + async reducePositionHelper( + wallet: Wallet, + tokenId: number, + decreasePercent: number, + ): Promise { + // Reduce position and burn + const positionData = await this.getRawPosition(wallet, tokenId); + const token0 = this.getTokenByAddress(positionData.token0); + const token1 = this.getTokenByAddress(positionData.token1); + const fee = positionData.fee; + if (!token0 || !token1) { + throw new Error( + `One of the tokens in this position isn't recognized. $token0: ${token0}, $token1: ${token1}`, + ); + } + const poolAddress = v3.Pool.getAddress(token0, token1, fee, POOL_INIT, FACTORY); + const poolData = await this.getPoolState(poolAddress, fee); + const position = new v3.Position({ + pool: new v3.Pool( + token0, + token1, + poolData.fee, + poolData.sqrtPriceX96.toString(), + poolData.liquidity.toString(), + poolData.tick, + ), + tickLower: positionData.tickLower, + tickUpper: positionData.tickUpper, + liquidity: positionData.liquidity, + }); + return v3.NonfungiblePositionManager.removeCallParameters( + position, + this.getReduceLiquidityData( + decreasePercent, + tokenId, + token0, + token1, + wallet, + ), + ); + } +} diff --git a/src/connectors/etcswap/etcswap.lp.ts b/src/connectors/etcswap/etcswap.lp.ts new file mode 100644 index 0000000000..6572ceb53d --- /dev/null +++ b/src/connectors/etcswap/etcswap.lp.ts @@ -0,0 +1,261 @@ +import { logger } from '../../services/logger'; +import { PositionInfo, UniswapLPish } from '../../services/common-interfaces'; +import * as v3 from '@uniswap/v3-sdk'; +import { Token } from '@uniswap/sdk-core'; +import { + BigNumber, + Transaction, + Wallet, + utils, + constants, + providers, +} from 'ethers'; +import { ETCSwapLPHelper, FACTORY, POOL_INIT } from './etcswap.lp.helper'; +import { AddPosReturn } from '../uniswap/uniswap.lp.interfaces'; +import { ETCSwapConfig } from './etcswap.config'; + +const MaxUint128 = BigNumber.from(2).pow(128).sub(1); + +export type Overrides = { + gasLimit: BigNumber; + gasPrice?: BigNumber; + value?: BigNumber; + nonce?: BigNumber; + maxFeePerGas?: BigNumber; + maxPriorityFeePerGas?: BigNumber; +}; + +export class ETCSwapLP extends ETCSwapLPHelper implements UniswapLPish { + private static _instances: { [name: string]: ETCSwapLP }; + private _gasLimitEstimate: number; + + private constructor(chain: string, network: string) { + super(chain, network); + this._gasLimitEstimate = ETCSwapConfig.config.gasLimitEstimate; + } + + public static getInstance(chain: string, network: string): ETCSwapLP { + if (ETCSwapLP._instances === undefined) { + ETCSwapLP._instances = {}; + } + if (!(chain + network in ETCSwapLP._instances)) { + ETCSwapLP._instances[chain + network] = new ETCSwapLP( + chain, + network, + ); + } + + return ETCSwapLP._instances[chain + network]; + } + + /** + * Default gas limit for swap transactions. + */ + public get gasLimitEstimate(): number { + return this._gasLimitEstimate; + } + + async getPosition(tokenId: number): Promise { + const contract = this.getContract('nft', this.chain.provider); + const requests = [ + contract.positions(tokenId), + this.collectFees(this.chain.provider, tokenId), // static call to calculate earned fees + ]; + const positionInfoReq = await Promise.allSettled(requests); + const rejected = positionInfoReq.filter( + (r) => r.status === 'rejected', + ) as PromiseRejectedResult[]; + if (rejected.length > 0) + throw new Error(`Unable to fetch position with id ${tokenId}`); + const positionInfo = ( + positionInfoReq.filter( + (r) => r.status === 'fulfilled', + ) as PromiseFulfilledResult[] + ).map((r) => r.value); + const position = positionInfo[0]; + const feeInfo = positionInfo[1]; + const token0 = this.getTokenByAddress(position.token0); + const token1 = this.getTokenByAddress(position.token1); + if (!token0 || !token1) { + throw new Error(`One of the tokens in this position isn't recognized.`); + } + const fee = position.fee; + const poolAddress = v3.Pool.getAddress(token0, token1, fee, POOL_INIT, FACTORY); + const poolData = await this.getPoolState(poolAddress, fee); + const positionInst = new v3.Position({ + pool: new v3.Pool( + token0, + token1, + poolData.fee, + poolData.sqrtPriceX96.toString(), + poolData.liquidity.toString(), + poolData.tick, + ), + tickLower: position.tickLower, + tickUpper: position.tickUpper, + liquidity: position.liquidity, + }); + return { + token0: token0.symbol, + token1: token1.symbol, + fee: v3.FeeAmount[position.fee], + lowerPrice: positionInst.token0PriceLower.toFixed(8), + upperPrice: positionInst.token0PriceUpper.toFixed(8), + amount0: positionInst.amount0.toFixed(), + amount1: positionInst.amount1.toFixed(), + unclaimedToken0: utils.formatUnits( + feeInfo.amount0.toString(), + token0.decimals, + ), + unclaimedToken1: utils.formatUnits( + feeInfo.amount1.toString(), + token1.decimals, + ), + }; + } + + async addPosition( + wallet: Wallet, + token0: Token, + token1: Token, + amount0: string, + amount1: string, + fee: string, + lowerPrice: number, + upperPrice: number, + tokenId: number = 0, + gasLimit: number, + gasPrice: number, + nonce?: number, + maxFeePerGas?: BigNumber, + maxPriorityFeePerGas?: BigNumber, + ): Promise { + const convertedFee = v3.FeeAmount[fee as keyof typeof v3.FeeAmount]; + const addLiquidityResponse: AddPosReturn = await this.addPositionHelper( + wallet, + token0, + token1, + amount0, + amount1, + convertedFee, + lowerPrice, + upperPrice, + tokenId, + ); + + if (nonce === undefined) { + nonce = await this.chain.nonceManager.getNextNonce(wallet.address); + } + + const tx = await wallet.sendTransaction({ + data: addLiquidityResponse.calldata, + to: addLiquidityResponse.swapRequired ? this.router : this.nftManager, + ...this.generateOverrides( + gasLimit, + gasPrice, + nonce, + maxFeePerGas, + maxPriorityFeePerGas, + addLiquidityResponse.value, + ), + }); + logger.info(`Pancakeswap V3 Add position Tx Hash: ${tx.hash}`); + return tx; + } + + async reducePosition( + wallet: Wallet, + tokenId: number, + decreasePercent: number = 100, + gasLimit: number, + gasPrice: number, + nonce?: number, + maxFeePerGas?: BigNumber, + maxPriorityFeePerGas?: BigNumber, + ): Promise { + // Reduce position and burn + const contract = this.getContract('nft', wallet); + const { calldata, value } = await this.reducePositionHelper( + wallet, + tokenId, + decreasePercent, + ); + + if (nonce === undefined) { + nonce = await this.chain.nonceManager.getNextNonce(wallet.address); + } + + const tx = await contract.multicall( + [calldata], + this.generateOverrides( + gasLimit, + gasPrice, + nonce, + maxFeePerGas, + maxPriorityFeePerGas, + value, + ), + ); + logger.info(`Pancakeswap V3 Remove position Tx Hash: ${tx.hash}`); + return tx; + } + + async collectFees( + wallet: Wallet | providers.StaticJsonRpcProvider, + tokenId: number, + gasLimit: number = this.gasLimitEstimate, + gasPrice: number = 0, + nonce?: number, + maxFeePerGas?: BigNumber, + maxPriorityFeePerGas?: BigNumber, + ): Promise { + const contract = this.getContract('nft', wallet); + const collectData = { + tokenId: tokenId, + recipient: constants.AddressZero, + amount0Max: MaxUint128, + amount1Max: MaxUint128, + }; + + if (wallet instanceof providers.StaticJsonRpcProvider) { + return await contract.callStatic.collect(collectData); + } else { + collectData.recipient = wallet.address; + if (nonce === undefined) { + nonce = await this.chain.nonceManager.getNextNonce(wallet.address); + } + return await contract.collect( + collectData, + this.generateOverrides( + gasLimit, + gasPrice, + nonce, + maxFeePerGas, + maxPriorityFeePerGas, + ), + ); + } + } + + generateOverrides( + gasLimit: number, + gasPrice: number, + nonce?: number, + maxFeePerGas?: BigNumber, + maxPriorityFeePerGas?: BigNumber, + value?: string, + ): Overrides { + const overrides: Overrides = { + gasLimit: BigNumber.from(String(gasLimit.toFixed(0))), + }; + if (maxFeePerGas && maxPriorityFeePerGas) { + overrides.maxFeePerGas = maxFeePerGas; + overrides.maxPriorityFeePerGas = maxPriorityFeePerGas; + } else { + overrides.gasPrice = BigNumber.from(String((gasPrice * 1e9).toFixed(0))); + } + if (nonce) overrides.nonce = BigNumber.from(String(nonce)); + if (value) overrides.value = BigNumber.from(value); + return overrides; + } +} diff --git a/src/network/network.controllers.ts b/src/network/network.controllers.ts index bb4b165158..ee1f50e8a1 100644 --- a/src/network/network.controllers.ts +++ b/src/network/network.controllers.ts @@ -21,6 +21,7 @@ import { } from '../services/connection-manager'; import { Osmosis } from '../chains/osmosis/osmosis'; import { XRPL } from '../chains/xrpl/xrpl'; +import { ETCChain } from '../chains/etc/etc'; export async function getStatus( req: StatusRequest @@ -114,6 +115,11 @@ export async function getStatus( connections = connections.concat( osmosisConnections ? Object.values(osmosisConnections) : [] ); + + const etcConnections = ETCChain.getConnectedInstances(); + connections = connections.concat( + etcConnections ? Object.values(etcConnections) : [] + ); } for (const connection of connections) { diff --git a/src/services/connection-manager.ts b/src/services/connection-manager.ts index 456d1eeedf..f9f5304ef8 100644 --- a/src/services/connection-manager.ts +++ b/src/services/connection-manager.ts @@ -46,6 +46,8 @@ import { XRPLCLOB } from '../connectors/xrpl/xrpl'; import { QuipuSwap } from '../connectors/quipuswap/quipuswap'; import { Carbonamm } from '../connectors/carbon/carbonAMM'; import { Balancer } from '../connectors/balancer/balancer'; +import { ETCSwapLP } from '../connectors/etcswap/etcswap.lp'; +import { ETCChain } from '../chains/etc/etc'; export type ChainUnion = | Algorand @@ -141,6 +143,8 @@ export async function getChainInstance( connection = XRPL.getInstance(network); } else if (chain === 'kujira') { connection = Kujira.getInstance(network); + } else if (chain === 'etc') { + connection = ETCChain.getInstance(network); } else { connection = undefined; } @@ -255,6 +259,9 @@ export async function getConnector( connectorInstance = QuipuSwap.getInstance(network); } else if (chain === 'ethereum' && connector === 'carbonamm') { connectorInstance = Carbonamm.getInstance(chain, network); + } else if (chain === 'etc' && connector === 'etcswapLP' + ) { + connectorInstance = ETCSwapLP.getInstance(chain, network) } else { throw new Error('unsupported chain or connector'); } diff --git a/src/services/schema/etcswap-schema.json b/src/services/schema/etcswap-schema.json new file mode 100644 index 0000000000..88e6c06a5b --- /dev/null +++ b/src/services/schema/etcswap-schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "allowedSlippage": { "type": "string" }, + "gasLimitEstimate": { "type": "integer" }, + "ttl": { "type": "integer" }, + "maximumHops": { "type": "integer" }, + "useRouter": { "type": "boolean" }, + "feeTier": { + "enum": ["LOWEST", "LOW", "MEDIUM", "HIGH"] + }, + "contractAddresses": { + "type": "object", + "patternProperties": { + "^\\w+$": { + "type": "object", + "properties": { + "routerAddress": { "type": "string" }, + "etcswapV3SmartOrderRouterAddress": { "type": "string" }, + "etcswapV3NftManagerAddress": { "type": "string" }, + "etcswapV3QuoterV2ContractAddress": { "type": "string" } + }, + "required": [ + "routerAddress", + "etcswapV3SmartOrderRouterAddress", + "etcswapV3NftManagerAddress" + ], + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "required": [ + "allowedSlippage", + "gasLimitEstimate", + "ttl", + "maximumHops", + "contractAddresses" + ] +} diff --git a/src/templates/etc.yml b/src/templates/etc.yml new file mode 100644 index 0000000000..2b456225ae --- /dev/null +++ b/src/templates/etc.yml @@ -0,0 +1,11 @@ +networks: + mainnet: + chainID: 61 + nodeURL: 'https://geth-at.etc-network.info' + tokenListType: FILE + tokenListSource: /home/gateway/conf/lists/etc.json + nativeCurrencySymbol: 'ETC' + gasPriceRefreshInterval: 60 + +manualGasPrice: 100 +gasLimitTransaction: 3000000 diff --git a/src/templates/etcswap.yml b/src/templates/etcswap.yml new file mode 100644 index 0000000000..da00dcb546 --- /dev/null +++ b/src/templates/etcswap.yml @@ -0,0 +1,31 @@ +# how much the execution price is allowed to move unfavorably from the trade +# execution price. It uses a rational number for precision. +allowedSlippage: '1/100' + +# the maximum gas used to estimate gasCost for a etcswap trade. +gasLimitEstimate: 150688 + +# how long a trade is valid in seconds. After time passes etcswap will not +# perform the trade, but the gas will still be spent. +ttl: 86400 + +# For each swap, the maximum number of hops to consider. +# Note: More hops will increase latency of the algorithm. +maximumHops: 4 + +# Use etcswap Router or Quoter to quote prices. +# true - use Smart Order Router +# false - use QuoterV2 Contract +useRouter: false + +# Fee tier to use for the etcswap Quoter. +# Required if `useRouter` is false. +# Available options: 'LOWEST', 'LOW', 'MEDIUM', 'HIGH'. +feeTier: 'MEDIUM' + +contractAddresses: + mainnet: + routerAddress: '0xEd88EDD995b00956097bF90d39C9341BBde324d1' + etcswapV3SmartOrderRouterAddress: '0xEd88EDD995b00956097bF90d39C9341BBde324d1' + etcswapV3NftManagerAddress: '0x3CEDe6562D6626A04d7502CC35720901999AB699' + etcswapV3QuoterV2ContractAddress: '0x4d8c163400CB87Cbe1bae76dBf36A09FED85d39B' diff --git a/src/templates/lists/etc.json b/src/templates/lists/etc.json new file mode 100644 index 0000000000..492afd48ce --- /dev/null +++ b/src/templates/lists/etc.json @@ -0,0 +1,228 @@ +{ + "tokens": [ + { + "address": "0x1953cab0e5bfa6d4a9bad6e05fd46c1cc6527a5a", + "symbol": "WETC", + "name": "Wrapped Ether", + "chainId": 61, + "decimals": 18, + "derivedETH": "1", + "volumeUSD": "87692.06726577001016234150498432839", + "volume": "3680.916158082445540853", + "txCount": "1465", + "totalValueLocked": "1309.108799434495380405", + "feesUSD": "269.8437698119726429445662484384581", + "totalValueLockedUSD": "31118.61587586461004265383226494394" + }, + { + "address": "0xde093684c796204224bc081f937aa059d903c52a", + "symbol": "USC", + "name": "Classic USD", + "chainId": 61, + "decimals": 6, + "derivedETH": "0.04206834920475468169275381513153288", + "volumeUSD": "85893.93730280406454320216469354218", + "volume": "85914.505167", + "txCount": "1325", + "totalValueLocked": "13578.454537", + "feesUSD": "253.3360602125878880343256394938591", + "totalValueLockedUSD": "13578.454537" + }, + { + "address": "0xfdcc3dd6671eab0709a4c0f3f53de9a333d80798", + "symbol": "SBC", + "name": "Stable Coin", + "chainId": 61, + "decimals": 6, + "derivedETH": "0.04205836854480378092682076709608981", + "volumeUSD": "388.827117", + "volume": "388.747266", + "txCount": "18", + "totalValueLocked": "4664.024238", + "feesUSD": "0.0388827117", + "totalValueLockedUSD": "4664.995135576698389659061818666678" + }, + { + "address": "0x9aa2901007fce996e35305fd9ba196e17fcd2605", + "symbol": "NYKE", + "name": "Nyke Finance", + "chainId": 61, + "decimals": 18, + "derivedETH": "0.00003279524031029087165578677336642955", + "volumeUSD": "2375.414163547750211938499475359414", + "volume": "5072737.356391124975921346", + "txCount": "179", + "totalValueLocked": "1375798.800601297172908066", + "feesUSD": "7.126242490643250635815498426078237", + "totalValueLockedUSD": "1066.338234843645753090714983554244" + }, + { + "address": "0xbb2d194abbac8834c833dccd0ccb266670b0d3de", + "symbol": "WAACC", + "name": "Wrapped AyeAyeCoin Classic", + "chainId": 61, + "decimals": 0, + "derivedETH": "0.00008575776559099022383522345349872617", + "volumeUSD": "426.7544267409323465386289850712885", + "volume": "173869", + "txCount": "16", + "totalValueLocked": "494129", + "feesUSD": "4.267544267409323465386289850712885", + "totalValueLockedUSD": "973.9058068540719003854332496218711" + }, + { + "address": "0x271dc2df1390a7b319cae1711a454fa416d6a309", + "symbol": "BOB", + "name": "BOB", + "chainId": 61, + "decimals": 0, + "derivedETH": "0.002939378453392258215476343177710635", + "volumeUSD": "537.0179550715121805573489814304719", + "volume": "7482", + "txCount": "81", + "totalValueLocked": "6173", + "feesUSD": "5.370179550715121805573489814304719", + "totalValueLockedUSD": "430.4652884325885182114832416370025" + }, + { + "address": "0xfc84c3dc9898e186ad4b85734100e951e3bcb68c", + "symbol": "CLD", + "name": "ClassicDAO", + "chainId": 61, + "decimals": 18, + "derivedETH": "0.001367109254195061583140481301990352", + "volumeUSD": "142.7510265367518173107350815599169", + "volume": "4842.963939069066742574", + "txCount": "21", + "totalValueLocked": "11967.143714371464749287", + "feesUSD": "0.4282530796102554519322052446797507", + "totalValueLockedUSD": "388.1325657945309416547439380819776" + }, + { + "address": "0xbf72bfefa79957fa944431f25e73a6aaebc81798", + "symbol": "TMWSTW", + "name": "TMWSTW_PROFITS", + "chainId": 61, + "decimals": 0, + "derivedETH": "0.0001777360707679544194225120206935986", + "volumeUSD": "136.271907", + "volume": "29377", + "txCount": "30", + "totalValueLocked": "57651", + "feesUSD": "1.36271907", + "totalValueLockedUSD": "218.3108317642871550357461755979839" + }, + { + "address": "0xa1ccb330165cda264f35de7630de084e83d39134", + "symbol": "SLAG", + "name": "SLAG", + "chainId": 61, + "decimals": 0, + "derivedETH": "0.002691681591866244789787729281248919", + "volumeUSD": "40.904303", + "volume": "703", + "txCount": "11", + "totalValueLocked": "1497", + "feesUSD": "0.40904303", + "totalValueLockedUSD": "95.59426499115455909690009603809247" + }, + { + "address": "0x19b4343d272da48779ab7a9a7436f95f63249871", + "symbol": "INK", + "name": "INK", + "chainId": 61, + "decimals": 0, + "derivedETH": "0.0003948512232998594665326076509148147", + "volumeUSD": "8.412785", + "volume": "887", + "txCount": "6", + "totalValueLocked": "7790", + "feesUSD": "0.08412785", + "totalValueLockedUSD": "72.97217636249137531254168335705121" + }, + { + "address": "0x6c3b413c461c42a88160ed1b1b31d6f7b02a1c83", + "symbol": "ETCPOW", + "name": "ETCPOW", + "chainId": 61, + "decimals": 18, + "derivedETH": "0.0005768612994998048292819372254023664", + "volumeUSD": "834.4691029166814454138859559927646", + "volume": "47870.019711029763702745", + "txCount": "124", + "totalValueLocked": "2719.685796872949608943", + "feesUSD": "8.340768344364492851532994108233981", + "totalValueLockedUSD": "37.22001045298055078986525723522444" + }, + { + "address": "0xca47e962a9806acd7a97a954926d0924fe15a2e6", + "symbol": "CBTC", + "name": "BITCOIN Classic", + "chainId": 61, + "decimals": 18, + "derivedETH": "0", + "volumeUSD": "1078.496151947402210568198949576743", + "volume": "37536.708824693625785018", + "txCount": "84", + "totalValueLocked": "1504.605046689227460019", + "feesUSD": "10.78496151947402210568198949576743", + "totalValueLockedUSD": "0" + }, + { + "address": "0xba991144ffdbe47936703606a6e74194db0da8aa", + "symbol": "PEPE", + "name": "PepeCoin Classic", + "decimals": 18, + "chainId": 61, + "derivedETH": "0", + "volumeUSD": "55.66152603779395759396033095967166", + "volume": "3327059.643930018475528965", + "txCount": "14", + "totalValueLocked": "1699274.24661097809385813", + "feesUSD": "0.5566152603779395759396033095967166", + "totalValueLockedUSD": "0" + }, + { + "address": "0x88d8c3dc6b5324f34e8cf229a93e197048671abd", + "symbol": "HEBE", + "name": "Hebe Wallet", + "chainId": 61, + "decimals": 18, + "derivedETH": "0", + "volumeUSD": "0", + "volume": "0", + "txCount": "5", + "totalValueLocked": "0.000000000000000002", + "feesUSD": "0", + "totalValueLockedUSD": "0" + }, + { + "address": "0x75829ce77de730e1e16811f309f358907972074f", + "symbol": "POSS", + "name": "Possum Token", + "chainId": 61, + "decimals": 18, + "derivedETH": "0", + "volumeUSD": "0", + "volume": "0", + "txCount": "3", + "totalValueLocked": "0.000000000000000002", + "feesUSD": "0", + "totalValueLockedUSD": "0" + }, + { + "address": "0x0bd01d2c68f89abed94bc85988fa8a6e18efb2db", + "symbol": "PUPU", + "name": "Pupu", + "chainId": 61, + "decimals": 18, + "derivedETH": "0", + "volumeUSD": "0.4110134269978101034701575310529836", + "volume": "4777395.508336922571095032", + "txCount": "4", + "totalValueLocked": "39573537.324409264656493464", + "feesUSD": "0.004110134269978101034701575310529836", + "totalValueLockedUSD": "0" + } + ] +} \ No newline at end of file diff --git a/src/templates/root.yml b/src/templates/root.yml index d15d5b0028..0b6b27de30 100644 --- a/src/templates/root.yml +++ b/src/templates/root.yml @@ -135,3 +135,11 @@ configurations: $namespace balancer: configurationPath: balancer.yml schemaPath: cronos-connector-schema.json + + $namespace etc: + configurationPath: etc.yml + schemaPath: ethereum-schema.json + + $namespace etcswap: + configurationPath: etcswap.yml + schemaPath: etcswap-schema.json From 91a61c06df77645b60f5836a8e4f780396b608f2 Mon Sep 17 00:00:00 2001 From: vic-en Date: Wed, 24 Jul 2024 23:45:19 -0500 Subject: [PATCH 02/13] update rpc endpoint and token list --- src/templates/etc.yml | 2 +- src/templates/lists/etc.json | 214 +---------------------------------- 2 files changed, 3 insertions(+), 213 deletions(-) diff --git a/src/templates/etc.yml b/src/templates/etc.yml index 2b456225ae..5a56839674 100644 --- a/src/templates/etc.yml +++ b/src/templates/etc.yml @@ -1,7 +1,7 @@ networks: mainnet: chainID: 61 - nodeURL: 'https://geth-at.etc-network.info' + nodeURL: 'https://etc.rivet.link' tokenListType: FILE tokenListSource: /home/gateway/conf/lists/etc.json nativeCurrencySymbol: 'ETC' diff --git a/src/templates/lists/etc.json b/src/templates/lists/etc.json index 492afd48ce..461257d300 100644 --- a/src/templates/lists/etc.json +++ b/src/templates/lists/etc.json @@ -5,224 +5,14 @@ "symbol": "WETC", "name": "Wrapped Ether", "chainId": 61, - "decimals": 18, - "derivedETH": "1", - "volumeUSD": "87692.06726577001016234150498432839", - "volume": "3680.916158082445540853", - "txCount": "1465", - "totalValueLocked": "1309.108799434495380405", - "feesUSD": "269.8437698119726429445662484384581", - "totalValueLockedUSD": "31118.61587586461004265383226494394" + "decimals": 18 }, { "address": "0xde093684c796204224bc081f937aa059d903c52a", "symbol": "USC", "name": "Classic USD", "chainId": 61, - "decimals": 6, - "derivedETH": "0.04206834920475468169275381513153288", - "volumeUSD": "85893.93730280406454320216469354218", - "volume": "85914.505167", - "txCount": "1325", - "totalValueLocked": "13578.454537", - "feesUSD": "253.3360602125878880343256394938591", - "totalValueLockedUSD": "13578.454537" - }, - { - "address": "0xfdcc3dd6671eab0709a4c0f3f53de9a333d80798", - "symbol": "SBC", - "name": "Stable Coin", - "chainId": 61, - "decimals": 6, - "derivedETH": "0.04205836854480378092682076709608981", - "volumeUSD": "388.827117", - "volume": "388.747266", - "txCount": "18", - "totalValueLocked": "4664.024238", - "feesUSD": "0.0388827117", - "totalValueLockedUSD": "4664.995135576698389659061818666678" - }, - { - "address": "0x9aa2901007fce996e35305fd9ba196e17fcd2605", - "symbol": "NYKE", - "name": "Nyke Finance", - "chainId": 61, - "decimals": 18, - "derivedETH": "0.00003279524031029087165578677336642955", - "volumeUSD": "2375.414163547750211938499475359414", - "volume": "5072737.356391124975921346", - "txCount": "179", - "totalValueLocked": "1375798.800601297172908066", - "feesUSD": "7.126242490643250635815498426078237", - "totalValueLockedUSD": "1066.338234843645753090714983554244" - }, - { - "address": "0xbb2d194abbac8834c833dccd0ccb266670b0d3de", - "symbol": "WAACC", - "name": "Wrapped AyeAyeCoin Classic", - "chainId": 61, - "decimals": 0, - "derivedETH": "0.00008575776559099022383522345349872617", - "volumeUSD": "426.7544267409323465386289850712885", - "volume": "173869", - "txCount": "16", - "totalValueLocked": "494129", - "feesUSD": "4.267544267409323465386289850712885", - "totalValueLockedUSD": "973.9058068540719003854332496218711" - }, - { - "address": "0x271dc2df1390a7b319cae1711a454fa416d6a309", - "symbol": "BOB", - "name": "BOB", - "chainId": 61, - "decimals": 0, - "derivedETH": "0.002939378453392258215476343177710635", - "volumeUSD": "537.0179550715121805573489814304719", - "volume": "7482", - "txCount": "81", - "totalValueLocked": "6173", - "feesUSD": "5.370179550715121805573489814304719", - "totalValueLockedUSD": "430.4652884325885182114832416370025" - }, - { - "address": "0xfc84c3dc9898e186ad4b85734100e951e3bcb68c", - "symbol": "CLD", - "name": "ClassicDAO", - "chainId": 61, - "decimals": 18, - "derivedETH": "0.001367109254195061583140481301990352", - "volumeUSD": "142.7510265367518173107350815599169", - "volume": "4842.963939069066742574", - "txCount": "21", - "totalValueLocked": "11967.143714371464749287", - "feesUSD": "0.4282530796102554519322052446797507", - "totalValueLockedUSD": "388.1325657945309416547439380819776" - }, - { - "address": "0xbf72bfefa79957fa944431f25e73a6aaebc81798", - "symbol": "TMWSTW", - "name": "TMWSTW_PROFITS", - "chainId": 61, - "decimals": 0, - "derivedETH": "0.0001777360707679544194225120206935986", - "volumeUSD": "136.271907", - "volume": "29377", - "txCount": "30", - "totalValueLocked": "57651", - "feesUSD": "1.36271907", - "totalValueLockedUSD": "218.3108317642871550357461755979839" - }, - { - "address": "0xa1ccb330165cda264f35de7630de084e83d39134", - "symbol": "SLAG", - "name": "SLAG", - "chainId": 61, - "decimals": 0, - "derivedETH": "0.002691681591866244789787729281248919", - "volumeUSD": "40.904303", - "volume": "703", - "txCount": "11", - "totalValueLocked": "1497", - "feesUSD": "0.40904303", - "totalValueLockedUSD": "95.59426499115455909690009603809247" - }, - { - "address": "0x19b4343d272da48779ab7a9a7436f95f63249871", - "symbol": "INK", - "name": "INK", - "chainId": 61, - "decimals": 0, - "derivedETH": "0.0003948512232998594665326076509148147", - "volumeUSD": "8.412785", - "volume": "887", - "txCount": "6", - "totalValueLocked": "7790", - "feesUSD": "0.08412785", - "totalValueLockedUSD": "72.97217636249137531254168335705121" - }, - { - "address": "0x6c3b413c461c42a88160ed1b1b31d6f7b02a1c83", - "symbol": "ETCPOW", - "name": "ETCPOW", - "chainId": 61, - "decimals": 18, - "derivedETH": "0.0005768612994998048292819372254023664", - "volumeUSD": "834.4691029166814454138859559927646", - "volume": "47870.019711029763702745", - "txCount": "124", - "totalValueLocked": "2719.685796872949608943", - "feesUSD": "8.340768344364492851532994108233981", - "totalValueLockedUSD": "37.22001045298055078986525723522444" - }, - { - "address": "0xca47e962a9806acd7a97a954926d0924fe15a2e6", - "symbol": "CBTC", - "name": "BITCOIN Classic", - "chainId": 61, - "decimals": 18, - "derivedETH": "0", - "volumeUSD": "1078.496151947402210568198949576743", - "volume": "37536.708824693625785018", - "txCount": "84", - "totalValueLocked": "1504.605046689227460019", - "feesUSD": "10.78496151947402210568198949576743", - "totalValueLockedUSD": "0" - }, - { - "address": "0xba991144ffdbe47936703606a6e74194db0da8aa", - "symbol": "PEPE", - "name": "PepeCoin Classic", - "decimals": 18, - "chainId": 61, - "derivedETH": "0", - "volumeUSD": "55.66152603779395759396033095967166", - "volume": "3327059.643930018475528965", - "txCount": "14", - "totalValueLocked": "1699274.24661097809385813", - "feesUSD": "0.5566152603779395759396033095967166", - "totalValueLockedUSD": "0" - }, - { - "address": "0x88d8c3dc6b5324f34e8cf229a93e197048671abd", - "symbol": "HEBE", - "name": "Hebe Wallet", - "chainId": 61, - "decimals": 18, - "derivedETH": "0", - "volumeUSD": "0", - "volume": "0", - "txCount": "5", - "totalValueLocked": "0.000000000000000002", - "feesUSD": "0", - "totalValueLockedUSD": "0" - }, - { - "address": "0x75829ce77de730e1e16811f309f358907972074f", - "symbol": "POSS", - "name": "Possum Token", - "chainId": 61, - "decimals": 18, - "derivedETH": "0", - "volumeUSD": "0", - "volume": "0", - "txCount": "3", - "totalValueLocked": "0.000000000000000002", - "feesUSD": "0", - "totalValueLockedUSD": "0" - }, - { - "address": "0x0bd01d2c68f89abed94bc85988fa8a6e18efb2db", - "symbol": "PUPU", - "name": "Pupu", - "chainId": 61, - "decimals": 18, - "derivedETH": "0", - "volumeUSD": "0.4110134269978101034701575310529836", - "volume": "4777395.508336922571095032", - "txCount": "4", - "totalValueLocked": "39573537.324409264656493464", - "feesUSD": "0.004110134269978101034701575310529836", - "totalValueLockedUSD": "0" + "decimals": 6 } ] } \ No newline at end of file From 0f461b5b4ac6262cfea794eb40b7a562226c174d Mon Sep 17 00:00:00 2001 From: vic-en Date: Thu, 25 Jul 2024 00:13:15 -0500 Subject: [PATCH 03/13] rename etc to ethereum-classic --- .../ethereum-classic.ts} | 26 +++---- src/connectors/etcswap/etcswap.config.ts | 2 +- src/connectors/etcswap/etcswap.lp.helper.ts | 8 +- src/network/network.controllers.ts | 4 +- src/services/connection-manager.ts | 76 +++++++++---------- .../{etc.yml => ethereum-classic.yml} | 2 +- .../lists/{etc.json => ethereum-classic.json} | 0 src/templates/root.yml | 4 +- 8 files changed, 61 insertions(+), 61 deletions(-) rename src/chains/{etc/etc.ts => ethereum-classic/ethereum-classic.ts} (77%) rename src/templates/{etc.yml => ethereum-classic.yml} (75%) rename src/templates/lists/{etc.json => ethereum-classic.json} (100%) diff --git a/src/chains/etc/etc.ts b/src/chains/ethereum-classic/ethereum-classic.ts similarity index 77% rename from src/chains/etc/etc.ts rename to src/chains/ethereum-classic/ethereum-classic.ts index 5e67ef9f26..01d5e34cd8 100644 --- a/src/chains/etc/etc.ts +++ b/src/chains/ethereum-classic/ethereum-classic.ts @@ -2,15 +2,15 @@ import abi from '../ethereum/ethereum.abi.json'; import { logger } from '../../services/logger'; import { Contract, Transaction, Wallet } from 'ethers'; import { EthereumBase } from '../ethereum/ethereum-base'; -import { getEthereumConfig as getETCChainConfig } from '../ethereum/ethereum.config'; +import { getEthereumConfig as getEthereumClassicChainConfig } from '../ethereum/ethereum.config'; import { Provider } from '@ethersproject/abstract-provider'; import { Chain as Ethereumish } from '../../services/common-interfaces'; import { ConfigManagerV2 } from '../../services/config-manager-v2'; import { EVMController } from '../ethereum/evm.controllers'; import { ETCSwapConfig } from '../../connectors/etcswap/etcswap.config'; -export class ETCChain extends EthereumBase implements Ethereumish { - private static _instances: { [name: string]: ETCChain }; +export class EthereumClassicChain extends EthereumBase implements Ethereumish { + private static _instances: { [name: string]: EthereumClassicChain }; private _chain: string; private _gasPrice: number; private _gasPriceRefreshInterval: number | null; @@ -18,9 +18,9 @@ export class ETCChain extends EthereumBase implements Ethereumish { public controller; private constructor(network: string) { - const config = getETCChainConfig('etc', network); + const config = getEthereumClassicChainConfig('ethereum-classic', network); super( - 'etc', + 'ethereum-classic', config.network.chainID, config.network.nodeURL, config.network.tokenListSource, @@ -42,19 +42,19 @@ export class ETCChain extends EthereumBase implements Ethereumish { this.controller = EVMController; } - public static getInstance(network: string): ETCChain { - if (ETCChain._instances === undefined) { - ETCChain._instances = {}; + public static getInstance(network: string): EthereumClassicChain { + if (EthereumClassicChain._instances === undefined) { + EthereumClassicChain._instances = {}; } - if (!(network in ETCChain._instances)) { - ETCChain._instances[network] = new ETCChain(network); + if (!(network in EthereumClassicChain._instances)) { + EthereumClassicChain._instances[network] = new EthereumClassicChain(network); } - return ETCChain._instances[network]; + return EthereumClassicChain._instances[network]; } - public static getConnectedInstances(): { [name: string]: ETCChain } { - return ETCChain._instances; + public static getConnectedInstances(): { [name: string]: EthereumClassicChain } { + return EthereumClassicChain._instances; } /** diff --git a/src/connectors/etcswap/etcswap.config.ts b/src/connectors/etcswap/etcswap.config.ts index 87645d43cb..ccf2770e12 100644 --- a/src/connectors/etcswap/etcswap.config.ts +++ b/src/connectors/etcswap/etcswap.config.ts @@ -18,7 +18,7 @@ export namespace ETCSwapConfig { 'etcswap', ['AMM'], [ - { chain: 'etc', networks: ['mainnet'] }, + { chain: 'ethereum-classic', networks: ['mainnet'] }, ], 'EVM', ); diff --git a/src/connectors/etcswap/etcswap.lp.helper.ts b/src/connectors/etcswap/etcswap.lp.helper.ts index 8c491da44b..ef6f36bc75 100644 --- a/src/connectors/etcswap/etcswap.lp.helper.ts +++ b/src/connectors/etcswap/etcswap.lp.helper.ts @@ -16,13 +16,13 @@ import { import * as math from 'mathjs'; import { getAddress } from 'ethers/lib/utils'; import { ETCSwapConfig } from './etcswap.config'; -import { ETCChain } from '../../chains/etc/etc'; +import { EthereumClassicChain } from '../../chains/ethereum-classic/ethereum-classic'; export const FACTORY = "0x2624E907BcC04f93C8f29d7C7149a8700Ceb8cDC"; export const POOL_INIT = "0x7ea2da342810af3c5a9b47258f990aaac829fe1385a1398feb77d0126a85dbef"; export class ETCSwapLPHelper { - protected chain: ETCChain; + protected chain: EthereumClassicChain; protected chainId; private _router: string; private _nftManager: string; @@ -35,7 +35,7 @@ export class ETCSwapLPHelper { public abiDecoder: any; constructor(chain: string, network: string) { - this.chain = ETCChain.getInstance(network); + this.chain = EthereumClassicChain.getInstance(network); this.chainId = this.getChainId(chain, network); this._router = ETCSwapConfig.config.etcswapV3SmartOrderRouterAddress(network); @@ -111,7 +111,7 @@ export class ETCSwapLPHelper { } public getChainId(_chain: string, network: string): number { - return ETCChain.getInstance(network).chainId; + return EthereumClassicChain.getInstance(network).chainId; } getPercentage(rawPercent: number | string): Percent { diff --git a/src/network/network.controllers.ts b/src/network/network.controllers.ts index ee1f50e8a1..5388656295 100644 --- a/src/network/network.controllers.ts +++ b/src/network/network.controllers.ts @@ -21,7 +21,7 @@ import { } from '../services/connection-manager'; import { Osmosis } from '../chains/osmosis/osmosis'; import { XRPL } from '../chains/xrpl/xrpl'; -import { ETCChain } from '../chains/etc/etc'; +import { EthereumClassicChain } from '../chains/ethereum-classic/ethereum-classic'; export async function getStatus( req: StatusRequest @@ -116,7 +116,7 @@ export async function getStatus( osmosisConnections ? Object.values(osmosisConnections) : [] ); - const etcConnections = ETCChain.getConnectedInstances(); + const etcConnections = EthereumClassicChain.getConnectedInstances(); connections = connections.concat( etcConnections ? Object.values(etcConnections) : [] ); diff --git a/src/services/connection-manager.ts b/src/services/connection-manager.ts index f9f5304ef8..d2e0fa35cb 100644 --- a/src/services/connection-manager.ts +++ b/src/services/connection-manager.ts @@ -47,7 +47,7 @@ import { QuipuSwap } from '../connectors/quipuswap/quipuswap'; import { Carbonamm } from '../connectors/carbon/carbonAMM'; import { Balancer } from '../connectors/balancer/balancer'; import { ETCSwapLP } from '../connectors/etcswap/etcswap.lp'; -import { ETCChain } from '../chains/etc/etc'; +import { EthereumClassicChain } from '../chains/ethereum-classic/ethereum-classic'; export type ChainUnion = | Algorand @@ -63,22 +63,22 @@ export type ChainUnion = export type Chain = T extends Algorand ? Algorand : T extends Cosmos - ? Cosmos - : T extends Ethereumish - ? Ethereumish - : T extends Nearish - ? Nearish - : T extends Xdcish - ? Xdcish - : T extends Tezosish - ? Tezosish - : T extends XRPLish - ? XRPLish - : T extends KujiraCLOB - ? KujiraCLOB - : T extends Osmosis - ? Osmosis - : never; + ? Cosmos + : T extends Ethereumish + ? Ethereumish + : T extends Nearish + ? Nearish + : T extends Xdcish + ? Xdcish + : T extends Tezosish + ? Tezosish + : T extends XRPLish + ? XRPLish + : T extends KujiraCLOB + ? KujiraCLOB + : T extends Osmosis + ? Osmosis + : never; export class UnsupportedChainException extends Error { constructor(message?: string) { @@ -143,8 +143,8 @@ export async function getChainInstance( connection = XRPL.getInstance(network); } else if (chain === 'kujira') { connection = Kujira.getInstance(network); - } else if (chain === 'etc') { - connection = ETCChain.getInstance(network); + } else if (chain === 'ethereum-classic') { + connection = EthereumClassicChain.getInstance(network); } else { connection = undefined; } @@ -168,24 +168,24 @@ export type ConnectorUnion = export type Connector = T extends Uniswapish ? Uniswapish : T extends UniswapLPish - ? UniswapLPish - : T extends Perpish - ? Perpish - : T extends RefAMMish - ? RefAMMish - : T extends CLOBish - ? CLOBish - : T extends Tinyman - ? Tinyman - : T extends Plenty - ? Plenty - : T extends XRPLish - ? XRPLCLOB - : T extends KujiraCLOB - ? KujiraCLOB - : T extends QuipuSwap - ? QuipuSwap - : never; + ? UniswapLPish + : T extends Perpish + ? Perpish + : T extends RefAMMish + ? RefAMMish + : T extends CLOBish + ? CLOBish + : T extends Tinyman + ? Tinyman + : T extends Plenty + ? Plenty + : T extends XRPLish + ? XRPLCLOB + : T extends KujiraCLOB + ? KujiraCLOB + : T extends QuipuSwap + ? QuipuSwap + : never; export async function getConnector( chain: string, @@ -259,7 +259,7 @@ export async function getConnector( connectorInstance = QuipuSwap.getInstance(network); } else if (chain === 'ethereum' && connector === 'carbonamm') { connectorInstance = Carbonamm.getInstance(chain, network); - } else if (chain === 'etc' && connector === 'etcswapLP' + } else if (chain === 'ethereum-classic' && connector === 'etcswapLP' ) { connectorInstance = ETCSwapLP.getInstance(chain, network) } else { diff --git a/src/templates/etc.yml b/src/templates/ethereum-classic.yml similarity index 75% rename from src/templates/etc.yml rename to src/templates/ethereum-classic.yml index 5a56839674..98ca4083a0 100644 --- a/src/templates/etc.yml +++ b/src/templates/ethereum-classic.yml @@ -3,7 +3,7 @@ networks: chainID: 61 nodeURL: 'https://etc.rivet.link' tokenListType: FILE - tokenListSource: /home/gateway/conf/lists/etc.json + tokenListSource: /home/gateway/conf/lists/ethereum-classic.json nativeCurrencySymbol: 'ETC' gasPriceRefreshInterval: 60 diff --git a/src/templates/lists/etc.json b/src/templates/lists/ethereum-classic.json similarity index 100% rename from src/templates/lists/etc.json rename to src/templates/lists/ethereum-classic.json diff --git a/src/templates/root.yml b/src/templates/root.yml index 0b6b27de30..25390278f1 100644 --- a/src/templates/root.yml +++ b/src/templates/root.yml @@ -136,8 +136,8 @@ configurations: configurationPath: balancer.yml schemaPath: cronos-connector-schema.json - $namespace etc: - configurationPath: etc.yml + $namespace ethereum-classic: + configurationPath: ethereum-classic.yml schemaPath: ethereum-schema.json $namespace etcswap: From d2c18419b35725fd4de5347be20615c8ca0824ba Mon Sep 17 00:00:00 2001 From: vic-en Date: Mon, 5 Aug 2024 10:30:51 -0500 Subject: [PATCH 04/13] update wallet validator --- src/services/wallet/wallet.validators.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/services/wallet/wallet.validators.ts b/src/services/wallet/wallet.validators.ts index ac53a621c4..575b81910c 100644 --- a/src/services/wallet/wallet.validators.ts +++ b/src/services/wallet/wallet.validators.ts @@ -153,6 +153,11 @@ export const validatePrivateKey: Validator = mkSelectingValidator( invalidEthPrivateKeyError, (val) => typeof val === 'string' && isEthPrivateKey(val), ), + 'ethereum-classic': mkValidator( + 'privateKey', + invalidEthPrivateKeyError, + (val) => typeof val === 'string' && isEthPrivateKey(val), + ), }, ); @@ -189,7 +194,8 @@ export const validateChain: Validator = mkValidator( val === 'tezos' || val === 'xrpl' || val === 'kujira' || - val === 'telos'), + val === 'telos' || + val === 'ethereum-classic'), ); export const validateNetwork: Validator = mkValidator( From 7b417b09d12e8f2eb0c0663b81aae0e928d6ebb2 Mon Sep 17 00:00:00 2001 From: vic-en Date: Mon, 5 Aug 2024 22:23:43 -0500 Subject: [PATCH 05/13] add unit tests --- .../ethereum-classic/ethereum-classic.test.ts | 357 ++++++++++++++ .../etcSwap/etcSwap.lp.routes.test.ts | 442 ++++++++++++++++++ .../connectors/etcSwap/etcSwap.lp.test.ts | 277 +++++++++++ 3 files changed, 1076 insertions(+) create mode 100644 test-bronze/chains/ethereum-classic/ethereum-classic.test.ts create mode 100644 test-bronze/connectors/etcSwap/etcSwap.lp.routes.test.ts create mode 100644 test-bronze/connectors/etcSwap/etcSwap.lp.test.ts diff --git a/test-bronze/chains/ethereum-classic/ethereum-classic.test.ts b/test-bronze/chains/ethereum-classic/ethereum-classic.test.ts new file mode 100644 index 0000000000..b7dd4bea3e --- /dev/null +++ b/test-bronze/chains/ethereum-classic/ethereum-classic.test.ts @@ -0,0 +1,357 @@ +import request from 'supertest'; +import { patch, unpatch } from '../../../test/services/patch'; +import { gatewayApp } from '../../../src/app'; +import { + NETWORK_ERROR_CODE, + NETWORK_ERROR_MESSAGE, + UNKNOWN_ERROR_ERROR_CODE, + UNKNOWN_ERROR_MESSAGE, +} from '../../../src/services/error-handler'; +import * as transactionSuccesful from '../../../test/chains/ethereum/fixtures/transaction-succesful.json'; +import * as transactionSuccesfulReceipt from '../../../test/chains/ethereum//fixtures/transaction-succesful-receipt.json'; +import * as transactionOutOfGas from '../../../test/chains/ethereum//fixtures/transaction-out-of-gas.json'; +import { patchEVMNonceManager } from '../../../test/evm.nonce.mock'; +import { EthereumClassicChain } from '../../../src/chains/ethereum-classic/ethereum-classic'; + +let etc: EthereumClassicChain; + +beforeAll(async () => { + etc = EthereumClassicChain.getInstance('mainnet'); + + patchEVMNonceManager(etc.nonceManager); + + await etc.init(); +}); + +beforeEach(() => { + patchEVMNonceManager(etc.nonceManager); +}); + +afterEach(() => { + unpatch(); +}); + +afterAll(async () => { + await etc.close(); +}); + +const address: string = '0x242532ebDfcc760f2Ddfe8378eB51f5F847CE5bD'; + +const patchGetWallet = () => { + patch(etc, 'getWallet', () => { + return { + address, + }; + }); +}; + +const patchGetNonce = () => { + patch(etc.nonceManager, 'getNonce', () => 0); +}; + +const patchGetTokenBySymbol = () => { + patch(etc, 'getTokenBySymbol', () => { + return { + chainId: 97, + address: '0xae13d989dac2f0debff460ac112a837c89baa7cd', + decimals: 18, + name: 'WBNB Token', + symbol: 'WBNB', + logoURI: + 'https://exchange.pancakeswap.finance/images/coins/0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c.png', + }; + }); +}; + +const patchApproveERC20 = () => { + patch(etc, 'approveERC20', () => { + return { + type: 2, + chainId: 97, + nonce: 0, + maxPriorityFeePerGas: { toString: () => '106000000000' }, + maxFeePerGas: { toString: () => '106000000000' }, + gasPrice: { toString: () => null }, + gasLimit: { toString: () => '66763' }, + to: '0x8babbb98678facc7342735486c851abd7a0d17ca', + value: { toString: () => '0' }, + data: '0x095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', // noqa: mock + accessList: [], + hash: '0xffdb7b393b46d3795b82c94b8d836ad6b3087a914244634fa89c3abbbf00ed72', // noqa: mock + v: 229, + r: '0x8800b16cbc6d468acad057dd5f724944d6aa48543cd90472e28dd5c6e90268b1', // noqa: mock + s: '0x662ed86bb86fb40911738ab67785f6e6c76f1c989d977ca23c504ef7a4796d08', // noqa: mock + from: '0x242532ebdfcc760f2ddfe8378eb51f5f847ce5bd', + confirmations: 98, + }; + }); +}; + +const patchGetERC20Allowance = () => { + patch(etc, 'getERC20Allowance', () => ({ value: 1, decimals: 3 })); +}; + +const patchGetNativeBalance = () => { + patch(etc, 'getNativeBalance', () => ({ value: 1, decimals: 3 })); +}; + +const patchGetERC20Balance = () => { + patch(etc, 'getERC20Balance', () => ({ value: 1, decimals: 3 })); +}; + +describe('POST /chain/approve', () => { + it('should return 200', async () => { + patchGetWallet(); + etc.getContract = jest.fn().mockReturnValue({ + address, + }); + patchGetNonce(); + patchGetTokenBySymbol(); + patchApproveERC20(); + + await request(gatewayApp) + .post(`/chain/approve`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + address, + spender: address, + token: 'BNB', + }) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200) + .then((res: any) => { + expect(res.body.nonce).toEqual(0); + }); + }); + + it('should return 404 when parameters are invalid', async () => { + await request(gatewayApp) + .post(`/chain/approve`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + address, + spender: address, + token: 123, + nonce: '23', + }) + .expect(404); + }); +}); + +describe('POST /chain/nonce', () => { + it('should return 200', async () => { + patchGetWallet(); + patchGetNonce(); + + await request(gatewayApp) + .post(`/chain/nonce`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + address, + }) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200) + .expect((res) => expect(res.body.nonce).toBe(0)); + }); +}); + +describe('POST /chain/allowances', () => { + it('should return 200 asking for allowances', async () => { + patchGetWallet(); + patchGetTokenBySymbol(); + const spender = '0x242532ebDfcc760f2Ddfe8378eB51f5F847CE5bD'; + etc.getSpender = jest.fn().mockReturnValue(spender); + etc.getContract = jest.fn().mockReturnValue({ + address: '0x242532ebDfcc760f2Ddfe8378eB51f5F847CE5bD', + }); + patchGetERC20Allowance(); + + await request(gatewayApp) + .post(`/chain/allowances`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + address: '0x242532ebDfcc760f2Ddfe8378eB51f5F847CE5bD', + spender: spender, + tokenSymbols: ['BNB', 'DAI'], + }) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200) + .expect((res) => expect(res.body.spender).toEqual(spender)) + .expect((res) => expect(res.body.approvals.BNB).toEqual('0.001')) + .expect((res) => expect(res.body.approvals.DAI).toEqual('0.001')); + }); +}); + +describe('POST /chain/balances', () => { + it('should return 200 asking for supported tokens', async () => { + patchGetWallet(); + patchGetTokenBySymbol(); + patchGetNativeBalance(); + patchGetERC20Balance(); + etc.getContract = jest.fn().mockReturnValue({ + address: '0x242532ebDfcc760f2Ddfe8378eB51f5F847CE5bD', + }); + + await request(gatewayApp) + .post(`/chain/balances`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + address: '0x242532ebDfcc760f2Ddfe8378eB51f5F847CE5bD', + tokenSymbols: ['WETH', 'DAI'], + }) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200) + .expect((res) => expect(res.body.balances.WETH).toBeDefined()) + .expect((res) => expect(res.body.balances.DAI).toBeDefined()); + }); +}); + +describe('POST /chain/cancel', () => { + it('should return 200', async () => { + // override getWallet (network call) + etc.getWallet = jest.fn().mockReturnValue({ + address, + }); + + etc.cancelTx = jest.fn().mockReturnValue({ + hash: '0xf6b9e7cec507cb3763a1179ff7e2a88c6008372e3a6f297d9027a0b39b0fff77', // noqa: mock + }); + + await request(gatewayApp) + .post(`/chain/cancel`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + address, + nonce: 23, + }) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200) + .then((res: any) => { + expect(res.body.txHash).toEqual( + '0xf6b9e7cec507cb3763a1179ff7e2a88c6008372e3a6f297d9027a0b39b0fff77' // noqa: mock + ); + }); + }); + + it('should return 404 when parameters are invalid', async () => { + await request(gatewayApp) + .post(`/chain/cancel`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + address: '', + nonce: '23', + }) + .expect(404); + }); +}); + +describe('POST /chain/poll', () => { + it('should get a NETWORK_ERROR_CODE when the network is unavailable', async () => { + patch(etc, 'getCurrentBlockNumber', () => { + const error: any = new Error('something went wrong'); + error.code = 'NETWORK_ERROR'; + throw error; + }); + + const res = await request(gatewayApp).post('/chain/poll').send({ + chain: 'ethereum-classic', + network: 'mainnet', + txHash: + '0xffdb7b393b46d3795b82c94b8d836ad6b3087a914244634fa89c3abbbf00ed72', // noqa: mock + }); + + expect(res.statusCode).toEqual(503); + expect(res.body.errorCode).toEqual(NETWORK_ERROR_CODE); + expect(res.body.message).toEqual(NETWORK_ERROR_MESSAGE); + }); + + it('should get a UNKNOWN_ERROR_ERROR_CODE when an unknown error is thrown', async () => { + patch(etc, 'getCurrentBlockNumber', () => { + throw new Error(); + }); + + const res = await request(gatewayApp).post('/chain/poll').send({ + chain: 'ethereum-classic', + network: 'mainnet', + txHash: + '0xffdb7b393b46d3795b82c94b8d836ad6b3087a914244634fa89c3abbbf00ed72', // noqa: mock + }); + + expect(res.statusCode).toEqual(503); + expect(res.body.errorCode).toEqual(UNKNOWN_ERROR_ERROR_CODE); + }); + + it('should get a null in txReceipt for Tx in the mempool', async () => { + patch(etc, 'getCurrentBlockNumber', () => 1); + patch(etc, 'getTransaction', () => transactionOutOfGas); + patch(etc, 'getTransactionReceipt', () => null); + const res = await request(gatewayApp).post('/chain/poll').send({ + chain: 'ethereum-classic', + network: 'mainnet', + txHash: + '0xffdb7b393b46d3795b82c94b8d836ad6b3087a914244634fa89c3abbbf00ed72', // noqa: mock + }); + expect(res.statusCode).toEqual(200); + expect(res.body.txReceipt).toEqual(null); + expect(res.body.txData).toBeDefined(); + }); + + it('should get a null in txReceipt and txData for Tx that didnt reach the mempool and TxReceipt is null', async () => { + patch(etc, 'getCurrentBlockNumber', () => 1); + patch(etc, 'getTransaction', () => null); + patch(etc, 'getTransactionReceipt', () => null); + const res = await request(gatewayApp).post('/chain/poll').send({ + chain: 'ethereum-classic', + network: 'mainnet', + txHash: + '0xffdb7b393b46d3795b82c94b8d836ad6b3087a914244634fa89c3abbbf00ed72', // noqa: mock + }); + expect(res.statusCode).toEqual(200); + expect(res.body.txReceipt).toEqual(null); + expect(res.body.txData).toEqual(null); + }); + + it('should get txStatus = 1 for a succesful query', async () => { + patch(etc, 'getCurrentBlockNumber', () => 1); + patch(etc, 'getTransaction', () => transactionSuccesful); + patch(etc, 'getTransactionReceipt', () => transactionSuccesfulReceipt); + const res = await request(gatewayApp).post('/chain/poll').send({ + chain: 'ethereum-classic', + network: 'mainnet', + txHash: + '0xffdb7b393b46d3795b82c94b8d836ad6b3087a914244634fa89c3abbbf00ed72', // noqa: mock + }); + expect(res.statusCode).toEqual(200); + expect(res.body.txReceipt).toBeDefined(); + expect(res.body.txData).toBeDefined(); + }); + + it('should get unknown error', async () => { + patch(etc, 'getCurrentBlockNumber', () => { + const error: any = new Error('something went wrong'); + error.code = -32006; + throw error; + }); + const res = await request(gatewayApp).post('/chain/poll').send({ + chain: 'ethereum-classic', + network: 'mainnet', + txHash: + '0xffdb7b393b46d3795b82c94b8d836ad6b3087a914244634fa89c3abbbf00ed72', // noqa: mock + }); + expect(res.statusCode).toEqual(503); + expect(res.body.errorCode).toEqual(UNKNOWN_ERROR_ERROR_CODE); + expect(res.body.message).toEqual(UNKNOWN_ERROR_MESSAGE); + }); +}); diff --git a/test-bronze/connectors/etcSwap/etcSwap.lp.routes.test.ts b/test-bronze/connectors/etcSwap/etcSwap.lp.routes.test.ts new file mode 100644 index 0000000000..b20531c1c8 --- /dev/null +++ b/test-bronze/connectors/etcSwap/etcSwap.lp.routes.test.ts @@ -0,0 +1,442 @@ +import express from 'express'; +import { Express } from 'express-serve-static-core'; +import request from 'supertest'; +import { EthereumClassicChain } from '../../../src/chains/ethereum-classic/ethereum-classic'; +import { AmmLiquidityRoutes } from '../../../src/amm/amm.routes'; +import { patch, unpatch } from '../../../test/services/patch'; +import { ETCSwapLP } from '../../../src/connectors/etcswap/etcswap.lp'; +import { patchEVMNonceManager } from '../../../test/evm.nonce.mock'; + +let app: Express; +let ethereumclassic: EthereumClassicChain; +let etcSwap: ETCSwapLP; + +beforeAll(async () => { + app = express(); + app.use(express.json()); + ethereumclassic = EthereumClassicChain.getInstance('mainnet'); + patchEVMNonceManager(ethereumclassic.nonceManager); + await ethereumclassic.init(); + + etcSwap = ETCSwapLP.getInstance('ethereum-classic', 'mainnet'); + await etcSwap.init(); + app.use('/amm/liquidity', AmmLiquidityRoutes.router); +}); + +beforeEach(() => { + patchEVMNonceManager(ethereumclassic.nonceManager); +}); + +afterEach(() => { + unpatch(); +}); + +afterAll(async () => { + await ethereumclassic.close(); +}); + +const address: string = '0xFaA12FD102FE8623C9299c72B03E45107F2772B5'; + +const patchGetWallet = () => { + patch(ethereumclassic, 'getWallet', () => { + return { + address: '0xFaA12FD102FE8623C9299c72B03E45107F2772B5', + }; + }); +}; + +const patchInit = () => { + patch(etcSwap, 'init', async () => { + return; + }); +}; + +const patchStoredTokenList = () => { + patch(ethereumclassic, 'tokenList', () => { + return [ + { + chainId: 61, + name: 'WETH', + symbol: 'WETH', + address: '0x1953cab0e5bfa6d4a9bad6e05fd46c1cc6527a5a', + decimals: 18, + }, + { + chainId: 61, + name: 'DAI', + symbol: 'DAI', + address: '0xde093684c796204224bc081f937aa059d903c52a', + decimals: 18, + }, + ]; + }); +}; + +const patchGetTokenBySymbol = () => { + patch(ethereumclassic, 'getTokenBySymbol', (symbol: string) => { + if (symbol === 'WETH') { + return { + chainId: 61, + name: 'WETH', + symbol: 'WETH', + address: '0x1953cab0e5bfa6d4a9bad6e05fd46c1cc6527a5a', + decimals: 18, + }; + } else { + return { + chainId: 61, + name: 'DAI', + symbol: 'DAI', + address: '0xde093684c796204224bc081f937aa059d903c52a', + decimals: 18, + }; + } + }); +}; + +const patchGetTokenByAddress = () => { + patch(etcSwap, 'getTokenByAddress', () => { + return { + chainId: 61, + name: 'WETH', + symbol: 'WETH', + address: '0x1953cab0e5bfa6d4a9bad6e05fd46c1cc6527a5a', + decimals: 18, + }; + }); +}; + +const patchGasPrice = () => { + patch(ethereumclassic, 'gasPrice', () => 100); +}; + +const patchGetNonce = () => { + patch(ethereumclassic.nonceManager, 'getNonce', () => 21); +}; + +const patchAddPosition = () => { + patch(etcSwap, 'addPosition', () => { + return { nonce: 21, hash: '000000000000000' }; + }); +}; + +const patchRemovePosition = () => { + patch(etcSwap, 'reducePosition', () => { + return { nonce: 21, hash: '000000000000000' }; + }); +}; + +const patchCollectFees = () => { + patch(etcSwap, 'collectFees', () => { + return { nonce: 21, hash: '000000000000000' }; + }); +}; + +const patchPosition = () => { + patch(etcSwap, 'getPosition', () => { + return { + token0: 'DAI', + token1: 'WETH', + fee: 300, + lowerPrice: '1', + upperPrice: '5', + amount0: '1', + amount1: '1', + unclaimedToken0: '1', + unclaimedToken1: '1', + }; + }); +}; + +describe('POST /liquidity/add', () => { + it('should return 200 when all parameter are OK', async () => { + patchGetWallet(); + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patchGasPrice(); + patchAddPosition(); + patchGetNonce(); + + await request(app) + .post(`/amm/liquidity/add`) + .send({ + address: address, + token0: 'DAI', + token1: 'WETH', + amount0: '1', + amount1: '1', + fee: 'LOW', + lowerPrice: '1', + upperPrice: '5', + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswapLP', + }) + .set('Accept', 'application/json') + .expect(200); + }); + + it('should return 500 for unrecognized token0 symbol', async () => { + patchGetWallet(); + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + + await request(app) + .post(`/amm/liquidity/add`) + .send({ + address: address, + token0: 'DOGE', + token1: 'WETH', + amount0: '1', + amount1: '1', + fee: 'LOW', + lowerPrice: '1', + upperPrice: '5', + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswapLP', + }) + .set('Accept', 'application/json') + .expect(500); + }); + + it('should return 404 for invalid fee tier', async () => { + patchGetWallet(); + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + + await request(app) + .post(`/amm/liquidity/add`) + .send({ + address: address, + token0: 'DAI', + token1: 'WETH', + amount0: '1', + amount1: '1', + fee: 300, + lowerPrice: '1', + upperPrice: '5', + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswapLP', + }) + .set('Accept', 'application/json') + .expect(404); + }); + + it('should return 500 when the helper operation fails', async () => { + patchGetWallet(); + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patch(etcSwap, 'addPositionHelper', () => { + return 'error'; + }); + + await request(app) + .post(`/amm/liquidity/add`) + .send({ + address: address, + token0: 'DAI', + token1: 'WETH', + amount0: '1', + amount1: '1', + fee: 'LOW', + lowerPrice: '1', + upperPrice: '5', + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswapLP', + }) + .set('Accept', 'application/json') + .expect(500); + }); +}); + +describe('POST /liquidity/remove', () => { + const patchForBuy = () => { + patchGetWallet(); + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patchGasPrice(); + patchRemovePosition(); + patchGetNonce(); + }; + it('should return 200 when all parameter are OK', async () => { + patchForBuy(); + await request(app) + .post(`/amm/liquidity/remove`) + .send({ + address: address, + tokenId: 2732, + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswapLP', + }) + .set('Accept', 'application/json') + .expect(200); + }); + + it('should return 404 when the tokenId is invalid', async () => { + patchGetWallet(); + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + + await request(app) + .post(`/amm/liquidity/remove`) + .send({ + address: address, + tokenId: 'Invalid', + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswapLP', + }) + .set('Accept', 'application/json') + .expect(404); + }); +}); + +describe('POST /liquidity/collect_fees', () => { + const patchForBuy = () => { + patchGetWallet(); + patchInit(); + patchGasPrice(); + patchCollectFees(); + patchGetNonce(); + }; + it('should return 200 when all parameter are OK', async () => { + patchForBuy(); + await request(app) + .post(`/amm/liquidity/collect_fees`) + .send({ + address: address, + tokenId: 2732, + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswapLP', + }) + .set('Accept', 'application/json') + .expect(200); + }); + + it('should return 404 when the tokenId is invalid', async () => { + patchGetWallet(); + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + + await request(app) + .post(`/amm/liquidity/collect_fees`) + .send({ + address: address, + tokenId: 'Invalid', + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswapLP', + }) + .set('Accept', 'application/json') + .expect(404); + }); +}); + +describe('POST /liquidity/position', () => { + it('should return 200 when all parameter are OK', async () => { + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patchPosition(); + + await request(app) + .post(`/amm/liquidity/position`) + .send({ + tokenId: 2732, + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswapLP', + }) + .set('Accept', 'application/json') + .expect(200); + }); + + it('should return 404 when the tokenId is invalid', async () => { + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + + await request(app) + .post(`/amm/liquidity/position`) + .send({ + tokenId: 'Invalid', + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswapLP', + }) + .set('Accept', 'application/json') + .expect(404); + }); +}); + +describe('POST /liquidity/price', () => { + const patchForBuy = () => { + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patch(etcSwap, 'poolPrice', () => { + return ['100', '105']; + }); + }; + it('should return 200 when all parameter are OK', async () => { + patchForBuy(); + await request(app) + .post(`/amm/liquidity/price`) + .send({ + token0: 'DAI', + token1: 'WETH', + fee: 'LOW', + period: 120, + interval: 60, + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswapLP', + }) + .set('Accept', 'application/json') + .expect(200); + }); + + it('should return 404 when the fee is invalid', async () => { + patchGetWallet(); + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + + await request(app) + .post(`/amm/liquidity/price`) + .send({ + token0: 'DAI', + token1: 'WETH', + fee: 11, + period: 120, + interval: 60, + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswapLP', + }) + .set('Accept', 'application/json') + .expect(404); + }); +}); diff --git a/test-bronze/connectors/etcSwap/etcSwap.lp.test.ts b/test-bronze/connectors/etcSwap/etcSwap.lp.test.ts new file mode 100644 index 0000000000..855a42537f --- /dev/null +++ b/test-bronze/connectors/etcSwap/etcSwap.lp.test.ts @@ -0,0 +1,277 @@ +jest.useFakeTimers(); +import { Token } from '@uniswap/sdk-core'; +import * as uniV3 from '@uniswap/v3-sdk'; +import { BigNumber, Transaction, Wallet } from 'ethers'; +import { patch, unpatch } from '../../../test/services/patch'; +import { EthereumClassicChain } from '../../../src/chains/ethereum-classic/ethereum-classic'; +import { ETCSwapLP } from '../../../src/connectors/etcswap/etcswap.lp'; +import { patchEVMNonceManager } from '../../../test/evm.nonce.mock'; +let ethereumC: EthereumClassicChain; +let etcSwapLP: ETCSwapLP; +let wallet: Wallet; + +const WETH = new Token( + 61, + '0x1953cab0e5bfa6d4a9bad6e05fd46c1cc6527a5a', + 18, + 'WETH' +); + +const DAI = new Token( + 61, + '0xdc31Ee1784292379Fbb2964b3B9C4124D8F89C60', + 18, + 'DAI' +); + +const USDC = new Token( + 61, + '0xde093684c796204224bc081f937aa059d903c52a', + 6, + 'USDC' +); + +const TX = { + type: 2, + chainId: 61, + nonce: 115, + maxPriorityFeePerGas: { toString: () => '106000000000' }, + maxFeePerGas: { toString: () => '106000000000' }, + gasPrice: { toString: () => null }, + gasLimit: { toString: () => '100000' }, + to: '0xdc31Ee1784292379Fbb2964b3B9C4124D8F89C60', + value: { toString: () => '0' }, + data: '0x095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', // noqa: mock + accessList: [], + hash: '0x75f98675a8f64dcf14927ccde9a1d59b67fa09b72cc2642ad055dae4074853d9', // noqa: mock + v: 0, + r: '0xbeb9aa40028d79b9fdab108fcef5de635457a05f3a254410414c095b02c64643', // noqa: mock + s: '0x5a1506fa4b7f8b4f3826d8648f27ebaa9c0ee4bd67f569414b8cd8884c073100', // noqa: mock + from: '0xFaA12FD102FE8623C9299c72B03E45107F2772B5', + confirmations: 0, +}; + +const POOL_SQRT_RATIO_START = uniV3.encodeSqrtRatioX96(100e6, 100e18); + +const POOL_TICK_CURRENT = uniV3.TickMath.getTickAtSqrtRatio( + POOL_SQRT_RATIO_START +); + +const DAI_USDC_POOL = new uniV3.Pool( + DAI, + USDC, + 500, + POOL_SQRT_RATIO_START, + 0, + POOL_TICK_CURRENT, + [] +); + +beforeAll(async () => { + ethereumC = EthereumClassicChain.getInstance('mainnet'); + patchEVMNonceManager(ethereumC.nonceManager); + await ethereumC.init(); + + wallet = new Wallet( + '0000000000000000000000000000000000000000000000000000000000000002', // noqa: mock + ethereumC.provider + ); + etcSwapLP = ETCSwapLP.getInstance('ethereum-classis', 'mainnet'); + await etcSwapLP.init(); +}); + +beforeEach(() => { + patchEVMNonceManager(ethereumC.nonceManager); +}); + +afterEach(() => { + unpatch(); +}); + +afterAll(async () => { + await ethereumC.close(); +}); + +const patchPoolState = () => { + patch(etcSwapLP, 'getPoolContract', () => { + return { + liquidity() { + return DAI_USDC_POOL.liquidity; + }, + slot0() { + return [ + DAI_USDC_POOL.sqrtRatioX96, + DAI_USDC_POOL.tickCurrent, + 0, + 1, + 1, + 0, + true, + ]; + }, + ticks() { + return ['-118445039955967015140', '118445039955967015140']; + }, + }; + }); +}; + +const patchContract = () => { + patch(etcSwapLP, 'getContract', () => { + return { + estimateGas: { + multicall() { + return BigNumber.from(5); + }, + }, + positions() { + return { + token0: WETH.address, + token1: USDC.address, + fee: 500, + tickLower: 0, + tickUpper: 23030, + liquidity: '6025055903594410671025', + }; + }, + multicall() { + return TX; + }, + collect() { + return TX; + }, + }; + }); +}; + +const patchWallet = () => { + patch(wallet, 'sendTransaction', () => { + return TX; + }); +}; + +describe('verify ETCSwapLP Nft functions', () => { + it('Should return correct contract addresses', async () => { + expect(etcSwapLP.router).toEqual( + '0xEd88EDD995b00956097bF90d39C9341BBde324d1' + ); + expect(etcSwapLP.nftManager).toEqual( + '0x3CEDe6562D6626A04d7502CC35720901999AB699' + ); + }); + + it('Should return correct contract abi', async () => { + expect(Array.isArray(etcSwapLP.routerAbi)).toEqual(true); + expect(Array.isArray(etcSwapLP.nftAbi)).toEqual(true); + expect(Array.isArray(etcSwapLP.poolAbi)).toEqual(true); + }); + + it('addPositionHelper returns calldata and value', async () => { + patchPoolState(); + + const callData = await etcSwapLP.addPositionHelper( + wallet, + DAI, + WETH, + '10', + '10', + 500, + 1, + 10 + ); + expect(callData).toHaveProperty('calldata'); + expect(callData).toHaveProperty('value'); + }); + + it('reducePositionHelper returns calldata and value', async () => { + patchPoolState(); + patchContract(); + + const callData = await etcSwapLP.reducePositionHelper(wallet, 1, 100); + expect(callData).toHaveProperty('calldata'); + expect(callData).toHaveProperty('value'); + }); + + it('basic functions should work', async () => { + patchContract(); + patchPoolState(); + + expect(etcSwapLP.ready()).toEqual(true); + expect(etcSwapLP.gasLimitEstimate).toBeGreaterThan(0); + expect(typeof etcSwapLP.getContract('nft', ethereumC.provider)).toEqual( + 'object' + ); + expect( + typeof etcSwapLP.getPoolContract( + '0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa', + wallet + ) + ).toEqual('object'); + }); + + it('generateOverrides returns overrides correctly', async () => { + const overrides = etcSwapLP.generateOverrides( + 1, + 2, + 3, + BigNumber.from(4), + BigNumber.from(5), + '6' + ); + expect(overrides.gasLimit).toEqual(BigNumber.from('1')); + expect(overrides.gasPrice).toBeUndefined(); + expect(overrides.nonce).toEqual(BigNumber.from(3)); + expect(overrides.maxFeePerGas as BigNumber).toEqual(BigNumber.from(4)); + expect(overrides.maxPriorityFeePerGas as BigNumber).toEqual( + BigNumber.from(5) + ); + expect(overrides.value).toEqual(BigNumber.from('6')); + }); + + it('reducePosition should work', async () => { + patchPoolState(); + patchContract(); + + const reduceTx = (await etcSwapLP.reducePosition( + wallet, + 1, + 100, + 50000, + 10 + )) as Transaction; + expect(reduceTx.hash).toEqual( + '0x75f98675a8f64dcf14927ccde9a1d59b67fa09b72cc2642ad055dae4074853d9' // noqa: mock + ); + }); + + it('addPosition should work', async () => { + patchPoolState(); + patchWallet(); + + const addTx = await etcSwapLP.addPosition( + wallet, + DAI, + WETH, + '10', + '10', + 'LOWEST', + 1, + 10, + 0, + 1, + 1 + ); + expect(addTx.hash).toEqual( + '0x75f98675a8f64dcf14927ccde9a1d59b67fa09b72cc2642ad055dae4074853d9' // noqa: mock + ); + }); + + it('collectFees should work', async () => { + patchContract(); + + const collectTx = (await etcSwapLP.collectFees(wallet, 1)) as Transaction; + expect(collectTx.hash).toEqual( + '0x75f98675a8f64dcf14927ccde9a1d59b67fa09b72cc2642ad055dae4074853d9' // noqa: mock + ); + }); +}); From bd9a268d2e370f5aa13f16eb8204622cbcfbb5ba Mon Sep 17 00:00:00 2001 From: vic-en Date: Mon, 5 Aug 2024 22:26:17 -0500 Subject: [PATCH 06/13] update workflow file --- .github/workflows/workflow.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 724f7e16fb..2b04084376 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -79,6 +79,7 @@ jobs: cp -rf src/templates/* conf sed -i 's|/home/gateway/conf/lists/|conf/lists/|g' ./conf/*.yml sed -i 's/https:\/\/rpc.ankr.com\/eth_goerli/http:\/\/127.0.0.1:8545\//g' ./conf/ethereum.yml + sed -i 's/https:\/\/etc.rivet.link\/eth_goerli/http:\/\/127.0.0.1:8545\//g' ./conf/ethereum-classic.yml - name: Run unit test coverage if: github.event_name == 'pull_request' From 60ff85e870bd0d4c6d525b7b3c64bf70d0934ffb Mon Sep 17 00:00:00 2001 From: vic-en Date: Tue, 6 Aug 2024 16:05:31 -0500 Subject: [PATCH 07/13] comment out etcSwap in additional spender --- src/connectors/connectors.routes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/connectors/connectors.routes.ts b/src/connectors/connectors.routes.ts index 0649494f22..d88b5c1b19 100644 --- a/src/connectors/connectors.routes.ts +++ b/src/connectors/connectors.routes.ts @@ -189,7 +189,8 @@ export namespace ConnectorsRoutes { trading_type: ETCSwapConfig.config.tradingTypes('LP'), chain_type: ETCSwapConfig.config.chainType, available_networks: ETCSwapConfig.config.availableNetworks, - additional_spenders: ['etcswap'], + // additional_spenders: ['etcswap'], + additional_spenders: [], }, ], }); From b4bb002dbc9e73938a37cfa1cb10d7bd73e8b35e Mon Sep 17 00:00:00 2001 From: vic-en Date: Tue, 6 Aug 2024 16:05:45 -0500 Subject: [PATCH 08/13] update workflow file --- .github/workflows/workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 2b04084376..2ac7ed200a 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -79,7 +79,7 @@ jobs: cp -rf src/templates/* conf sed -i 's|/home/gateway/conf/lists/|conf/lists/|g' ./conf/*.yml sed -i 's/https:\/\/rpc.ankr.com\/eth_goerli/http:\/\/127.0.0.1:8545\//g' ./conf/ethereum.yml - sed -i 's/https:\/\/etc.rivet.link\/eth_goerli/http:\/\/127.0.0.1:8545\//g' ./conf/ethereum-classic.yml + sed -i 's/https:\/\/etc.rivet.link/http:\/\/127.0.0.1:8545\//g' ./conf/ethereum-classic.yml - name: Run unit test coverage if: github.event_name == 'pull_request' From 64889f896e9450eef5e5a43366b23471bdbe68a7 Mon Sep 17 00:00:00 2001 From: vic-en Date: Wed, 14 Aug 2024 09:58:32 -0500 Subject: [PATCH 09/13] fix uniswap test --- test/connectors/uniswap/uniswap.lp.test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/connectors/uniswap/uniswap.lp.test.ts b/test/connectors/uniswap/uniswap.lp.test.ts index 64238d4679..a09e39b97a 100644 --- a/test/connectors/uniswap/uniswap.lp.test.ts +++ b/test/connectors/uniswap/uniswap.lp.test.ts @@ -33,7 +33,7 @@ const USDC = new Token( const TX = { type: 2, - chainId: 42, + chainId: 5, nonce: 115, maxPriorityFeePerGas: { toString: () => '106000000000' }, maxFeePerGas: { toString: () => '106000000000' }, @@ -117,8 +117,10 @@ const patchPoolState = () => { }; const patchAlphaRouter = () => { - patch(uniswapLP.alphaRouter, 'routeToRatio', () => { - return { status: 3 }; + patch(uniswapLP, '_alphaRouter', { + routeToRatio() { + return { status: 3 }; + } }); }; From f53b5847d74363013e18384c5a971d824afcb19f Mon Sep 17 00:00:00 2001 From: vic-en Date: Wed, 14 Aug 2024 10:11:27 -0500 Subject: [PATCH 10/13] fix uniswap swawp test --- test/connectors/uniswap/uniswap.test.ts | 88 +++++++++++++------------ 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/test/connectors/uniswap/uniswap.test.ts b/test/connectors/uniswap/uniswap.test.ts index 37da4e28d0..a20ae76fee 100644 --- a/test/connectors/uniswap/uniswap.test.ts +++ b/test/connectors/uniswap/uniswap.test.ts @@ -68,49 +68,51 @@ afterAll(async () => { }); const patchTrade = (_key: string, error?: Error) => { - patch(uniswap.alphaRouter, 'route', () => { - if (error) return false; - const WETH_DAI = new Pair( - CurrencyAmount.fromRawAmount(WETH, '2000000000000000000'), - CurrencyAmount.fromRawAmount(DAI, '1000000000000000000') - ); - const DAI_TO_WETH = new Route([WETH_DAI], DAI, WETH); - return { - quote: CurrencyAmount.fromRawAmount(DAI, '1000000000000000000'), - quoteGasAdjusted: CurrencyAmount.fromRawAmount( - DAI, - '1000000000000000000' - ), - estimatedGasUsed: utils.parseEther('100'), - estimatedGasUsedQuoteToken: CurrencyAmount.fromRawAmount( - DAI, - '1000000000000000000' - ), - estimatedGasUsedUSD: CurrencyAmount.fromRawAmount( - DAI, - '1000000000000000000' - ), - gasPriceWei: utils.parseEther('100'), - trade: new Trade({ - v2Routes: [ - { - routev2: DAI_TO_WETH, - inputAmount: CurrencyAmount.fromRawAmount( - DAI, - '1000000000000000000' - ), - outputAmount: CurrencyAmount.fromRawAmount( - WETH, - '2000000000000000000' - ), - }, - ], - v3Routes: [], - tradeType: TradeType.EXACT_INPUT, - }), - route: [], - blockNumber: BigNumber.from(5000), - }; + patch(uniswap, '_alphaRouter', { + route() { + if (error) return false; + const WETH_DAI = new Pair( + CurrencyAmount.fromRawAmount(WETH, '2000000000000000000'), + CurrencyAmount.fromRawAmount(DAI, '1000000000000000000') + ); + const DAI_TO_WETH = new Route([WETH_DAI], DAI, WETH); + return { + quote: CurrencyAmount.fromRawAmount(DAI, '1000000000000000000'), + quoteGasAdjusted: CurrencyAmount.fromRawAmount( + DAI, + '1000000000000000000' + ), + estimatedGasUsed: utils.parseEther('100'), + estimatedGasUsedQuoteToken: CurrencyAmount.fromRawAmount( + DAI, + '1000000000000000000' + ), + estimatedGasUsedUSD: CurrencyAmount.fromRawAmount( + DAI, + '1000000000000000000' + ), + gasPriceWei: utils.parseEther('100'), + trade: new Trade({ + v2Routes: [ + { + routev2: DAI_TO_WETH, + inputAmount: CurrencyAmount.fromRawAmount( + DAI, + '1000000000000000000' + ), + outputAmount: CurrencyAmount.fromRawAmount( + WETH, + '2000000000000000000' + ), + }, + ], + v3Routes: [], + tradeType: TradeType.EXACT_INPUT, + }), + route: [], + blockNumber: BigNumber.from(5000), + }; + } }); }; From 809dff7c30b973441574604edf99ceba1c70a331 Mon Sep 17 00:00:00 2001 From: vic-en Date: Thu, 29 Aug 2024 16:48:34 -0500 Subject: [PATCH 11/13] update --- package.json | 2 + src/connectors/connectors.routes.ts | 9 +- src/connectors/etcswap/etcswap.config.ts | 49 ++- src/connectors/etcswap/etcswap.ts | 503 +++++++++++++++++++++++ src/connectors/uniswap/uniswap.ts | 6 +- src/services/connection-manager.ts | 4 + src/services/schema/etcswap-schema.json | 8 +- src/templates/etcswap.yml | 3 +- yarn.lock | 165 +++++++- 9 files changed, 709 insertions(+), 40 deletions(-) create mode 100644 src/connectors/etcswap/etcswap.ts diff --git a/package.json b/package.json index 0e8e739e60..624c667ef0 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "test:scripts": "jest -i --verbose ./test-scripts/*.test.ts" }, "dependencies": { + "@_etcswap/v3-sdk": "^3.10.1", + "@_etcswap/smart-order-router": "^3.15.0", "@cosmjs/amino": "^0.32.2", "@balancer-labs/sdk": "^1.1.5", "@bancor/carbon-sdk": "^0.0.93-DEV", diff --git a/src/connectors/connectors.routes.ts b/src/connectors/connectors.routes.ts index d88b5c1b19..0011294bb1 100644 --- a/src/connectors/connectors.routes.ts +++ b/src/connectors/connectors.routes.ts @@ -184,13 +184,18 @@ export namespace ConnectorsRoutes { chain_type: BalancerConfig.config.chainType, available_networks: BalancerConfig.config.availableNetworks, }, + { + name: 'etcswap', + trading_type: ETCSwapConfig.config.tradingTypes('swap'), + chain_type: ETCSwapConfig.config.chainType, + available_networks: ETCSwapConfig.config.availableNetworks, + }, { name: 'etcswapLP', trading_type: ETCSwapConfig.config.tradingTypes('LP'), chain_type: ETCSwapConfig.config.chainType, available_networks: ETCSwapConfig.config.availableNetworks, - // additional_spenders: ['etcswap'], - additional_spenders: [], + additional_spenders: ['etcswap'], }, ], }); diff --git a/src/connectors/etcswap/etcswap.config.ts b/src/connectors/etcswap/etcswap.config.ts index ccf2770e12..2ce3785161 100644 --- a/src/connectors/etcswap/etcswap.config.ts +++ b/src/connectors/etcswap/etcswap.config.ts @@ -1,31 +1,30 @@ -import { - buildConfig, - NetworkConfig as V2NetworkConfig, -} from '../../network/network.utils'; +import { AvailableNetworks } from '../../services/config-manager-types'; import { ConfigManagerV2 } from '../../services/config-manager-v2'; - export namespace ETCSwapConfig { - export interface NetworkConfig extends Omit { + export interface NetworkConfig { + allowedSlippage: string; + gasLimitEstimate: number; + ttl: number; maximumHops: number; etcswapV3SmartOrderRouterAddress: (network: string) => string; etcswapV3NftManagerAddress: (network: string) => string; + etcswapV3FactoryAddress: (network: string) => string; + quoterContractAddress: (network: string) => string; tradingTypes: (type: string) => Array; + chainType: string; + availableNetworks: Array; useRouter?: boolean; feeTier?: string; } - export const v2Config: V2NetworkConfig = buildConfig( - 'etcswap', - ['AMM'], - [ - { chain: 'ethereum-classic', networks: ['mainnet'] }, - ], - 'EVM', - ); - export const config: NetworkConfig = { - ...v2Config, - ...{ + allowedSlippage: ConfigManagerV2.getInstance().get( + `etcswap.allowedSlippage` + ), + gasLimitEstimate: ConfigManagerV2.getInstance().get( + `etcswap.gasLimitEstimate` + ), + ttl: ConfigManagerV2.getInstance().get(`etcswap.ttl`), maximumHops: ConfigManagerV2.getInstance().get(`etcswap.maximumHops`), etcswapV3SmartOrderRouterAddress: (network: string) => ConfigManagerV2.getInstance().get( @@ -35,11 +34,25 @@ export namespace ETCSwapConfig { ConfigManagerV2.getInstance().get( `etcswap.contractAddresses.${network}.etcswapV3NftManagerAddress`, ), + etcswapV3FactoryAddress: (network: string) => + ConfigManagerV2.getInstance().get( + `etcswap.contractAddresses.${network}.etcswapV3FactoryAddress` + ), + quoterContractAddress: (network: string) => + ConfigManagerV2.getInstance().get( + `etcswap.contractAddresses.${network}.etcswapV3QuoterV2ContractAddress` + ), tradingTypes: (type: string) => { return type === 'swap' ? ['AMM'] : ['AMM_LP']; }, + chainType: 'EVM', + availableNetworks: [ + { + chain: 'ethereum-classic', + networks: ['mainnet'] + }, + ], useRouter: ConfigManagerV2.getInstance().get(`etcswap.useRouter`), feeTier: ConfigManagerV2.getInstance().get(`etcswap.feeTier`), - }, }; } diff --git a/src/connectors/etcswap/etcswap.ts b/src/connectors/etcswap/etcswap.ts new file mode 100644 index 0000000000..f3bcf653f2 --- /dev/null +++ b/src/connectors/etcswap/etcswap.ts @@ -0,0 +1,503 @@ +import { UniswapishPriceError } from '../../services/error-handler'; +import { isFractionString } from '../../services/validators'; +import { + ContractInterface, + ContractTransaction, +} from '@ethersproject/contracts'; +// import { AlphaRouter } from '@uniswap/smart-order-router'; +import { AlphaRouter } from '@_etcswap/smart-order-router'; +import routerAbi from '../uniswap/uniswap_v2_router_abi.json'; +import { Trade, SwapRouter } from '@uniswap/router-sdk'; +import { + FeeAmount, + MethodParameters, + Pool, + SwapQuoter, + Trade as UniswapV3Trade, + Route +} from '@uniswap/v3-sdk'; +import { abi as IUniswapV3PoolABI } from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json'; +import { abi as IUniswapV3FactoryABI } from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Factory.sol/IUniswapV3Factory.json'; +import { + Token, + CurrencyAmount, + Percent, + TradeType, + Currency, +} from '@uniswap/sdk-core'; +import { + BigNumber, + Transaction, + Wallet, + Contract, + utils, + constants, +} from 'ethers'; +import { logger } from '../../services/logger'; +import { percentRegexp } from '../../services/config-manager-v2'; +import { ExpectedTrade, Uniswapish } from '../../services/common-interfaces'; +import { getAddress } from 'ethers/lib/utils'; +import { EthereumClassicChain } from '../../chains/ethereum-classic/ethereum-classic'; +import { ETCSwapConfig } from './etcswap.config'; + +export class ETCSwap implements Uniswapish { + private static _instances: { [name: string]: ETCSwap }; + private chain: EthereumClassicChain; + private _alphaRouter: AlphaRouter | null; + private _router: string; + private _routerAbi: ContractInterface; + private _gasLimitEstimate: number; + private _ttl: number; + private _maximumHops: number; + private chainId; + private tokenList: Record = {}; + private _ready: boolean = false; + private readonly _useRouter: boolean; + private readonly _feeTier: FeeAmount; + private readonly _quoterContractAddress: string; + private readonly _factoryAddress: string; + + private constructor(_chain: string, network: string) { + const config = ETCSwapConfig.config; + this.chain = EthereumClassicChain.getInstance(network); + + this.chainId = this.chain.chainId; + this._ttl = ETCSwapConfig.config.ttl; + this._maximumHops = ETCSwapConfig.config.maximumHops; + + this._alphaRouter = new AlphaRouter({ + chainId: this.chainId, + provider: this.chain.provider, + }); + this._routerAbi = routerAbi.abi; + this._gasLimitEstimate = ETCSwapConfig.config.gasLimitEstimate; + this._router = config.etcswapV3SmartOrderRouterAddress(network); + + if (config.useRouter === false && config.feeTier == null) { + throw new Error('Must specify fee tier if not using router'); + } + if (config.useRouter === false && config.quoterContractAddress == null) { + throw new Error( + 'Must specify quoter contract address if not using router' + ); + } + this._useRouter = config.useRouter ?? true; + this._feeTier = config.feeTier + ? FeeAmount[config.feeTier as keyof typeof FeeAmount] + : FeeAmount.MEDIUM; + this._quoterContractAddress = config.quoterContractAddress(network); + this._factoryAddress = config.etcswapV3FactoryAddress(network); + } + + public static getInstance(chain: string, network: string): ETCSwap { + if (ETCSwap._instances === undefined) { + ETCSwap._instances = {}; + } + if (!(chain + network in ETCSwap._instances)) { + ETCSwap._instances[chain + network] = new ETCSwap(chain, network); + } + + return ETCSwap._instances[chain + network]; + } + + /** + * 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)]; + } + + public async init() { + if (!this.chain.ready()) { + await this.chain.init(); + } + for (const token of this.chain.storedTokenList) { + this.tokenList[token.address] = new Token( + this.chainId, + token.address, + token.decimals, + token.symbol, + token.name + ); + } + this._ready = true; + } + + public ready(): boolean { + return this._ready; + } + + /** + * Router address. + */ + public get router(): string { + return this._router; + } + + /** + * AlphaRouter instance. + */ + public get alphaRouter(): AlphaRouter { + if (this._alphaRouter === null) { + throw new Error('AlphaRouter is not initialized'); + } + return this._alphaRouter; + } + + /** + * Router smart contract ABI. + */ + public get routerAbi(): ContractInterface { + return this._routerAbi; + } + + /** + * Default gas limit used to estimate gasCost for swap transactions. + */ + public get gasLimitEstimate(): number { + return this._gasLimitEstimate; + } + + /** + * Default time-to-live for swap transactions, in seconds. + */ + public get ttl(): number { + return this._ttl; + } + + /** + * Default maximum number of hops for to go through for a swap transactions. + */ + public get maximumHops(): number { + return this._maximumHops; + } + + /** + * 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): Percent { + if (allowedSlippageStr != null && isFractionString(allowedSlippageStr)) { + const fractionSplit = allowedSlippageStr.split('/'); + return new Percent(fractionSplit[0], fractionSplit[1]); + } + + const allowedSlippage = ETCSwapConfig.config.allowedSlippage; + const nd = allowedSlippage.match(percentRegexp); + if (nd) return new Percent(nd[1], nd[2]); + throw new Error( + 'Encountered a malformed percent string in the config for ALLOWED_SLIPPAGE.' + ); + } + + /** + * 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 + */ + async estimateSellTrade( + baseToken: Token, + quoteToken: Token, + amount: BigNumber, + allowedSlippage?: string, + poolId?: string + ): Promise { + const nativeTokenAmount: CurrencyAmount = + CurrencyAmount.fromRawAmount(baseToken, amount.toString()); + + logger.info( + `Fetching trade data for ${baseToken.address}-${quoteToken.address}.` + ); + + if (this._useRouter) { + if (this._alphaRouter === null) { + throw new Error('AlphaRouter is not initialized'); + } + const route = await this._alphaRouter.route( + nativeTokenAmount, + quoteToken, + TradeType.EXACT_INPUT, + undefined, + { + maxSwapsPerPath: this.maximumHops, + } + ); + + if (!route) { + throw new UniswapishPriceError( + `priceSwapIn: no trade pair found for ${baseToken.address} to ${quoteToken.address}.` + ); + } + logger.info( + `Best trade for ${baseToken.address}-${quoteToken.address}: ` + + `${route.trade.executionPrice.toFixed(6)}` + + `${baseToken.symbol}.` + ); + const expectedAmount = route.trade.minimumAmountOut( + this.getAllowedSlippage(allowedSlippage) + ); + return { trade: route.trade, expectedAmount }; + } else { + 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}.` + ); + } + const swapRoute = new Route([pool], baseToken, quoteToken); + const quotedAmount = await this.getQuote( + swapRoute, + quoteToken, + nativeTokenAmount, + TradeType.EXACT_INPUT + ); + const trade = UniswapV3Trade.createUncheckedTrade({ + route: swapRoute, + inputAmount: nativeTokenAmount, + outputAmount: quotedAmount, + tradeType: TradeType.EXACT_INPUT, + }); + logger.info( + `Best trade for ${baseToken.address}-${quoteToken.address}: ` + + `${trade.executionPrice.toFixed(6)}` + + `${baseToken.symbol}.` + ); + const expectedAmount = trade.minimumAmountOut( + this.getAllowedSlippage(allowedSlippage) + ); + return { trade, expectedAmount }; + } + } + + /** + * 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 + */ + async estimateBuyTrade( + quoteToken: Token, + baseToken: Token, + amount: BigNumber, + allowedSlippage?: string, + poolId?: string + ): Promise { + const nativeTokenAmount: CurrencyAmount = + CurrencyAmount.fromRawAmount(baseToken, amount.toString()); + logger.info( + `Fetching pair data for ${quoteToken.address}-${baseToken.address}.` + ); + + if (this._useRouter) { + if (this._alphaRouter === null) { + throw new Error('AlphaRouter is not initialized'); + } + const route = await this._alphaRouter.route( + nativeTokenAmount, + quoteToken, + TradeType.EXACT_OUTPUT, + undefined, + { + maxSwapsPerPath: this.maximumHops, + } + ); + if (!route) { + throw new UniswapishPriceError( + `priceSwapOut: no trade pair found for ${quoteToken.address} to ${baseToken.address}.` + ); + } + logger.info( + `Best trade for ${quoteToken.address}-${baseToken.address}: ` + + `${route.trade.executionPrice.invert().toFixed(6)} ` + + `${baseToken.symbol}.` + ); + + const expectedAmount = route.trade.maximumAmountIn( + this.getAllowedSlippage(allowedSlippage) + ); + return { trade: route.trade, expectedAmount }; + } else { + 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}.` + ); + } + const swapRoute = new Route([pool], quoteToken, baseToken); + const quotedAmount = await this.getQuote( + swapRoute, + quoteToken, + nativeTokenAmount, + TradeType.EXACT_OUTPUT + ); + const trade = UniswapV3Trade.createUncheckedTrade({ + route: swapRoute, + inputAmount: quotedAmount, + outputAmount: nativeTokenAmount, + tradeType: TradeType.EXACT_OUTPUT, + }); + logger.info( + `Best trade for ${baseToken.address}-${quoteToken.address}: ` + + `${trade.executionPrice.invert().toFixed(6)}` + + `${baseToken.symbol}.` + ); + const expectedAmount = trade.maximumAmountIn( + this.getAllowedSlippage(allowedSlippage) + ); + return { trade, expectedAmount }; + } + } + + /** + * Given a wallet and a ETCSwap trade, try to execute it on blockchain. + * + * @param wallet Wallet + * @param trade Expected trade + * @param gasPrice Base gas price, for pre-EIP1559 transactions + * @param uniswapRouter 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 + */ + async executeTrade( + wallet: Wallet, + trade: Trade, + gasPrice: number, + uniswapRouter: string, + ttl: number, + _abi: ContractInterface, + gasLimit: number, + nonce?: number, + maxFeePerGas?: BigNumber, + maxPriorityFeePerGas?: BigNumber, + allowedSlippage?: string + ): Promise { + const methodParameters: MethodParameters = SwapRouter.swapCallParameters( + trade, + { + deadlineOrPreviousBlockhash: Math.floor(Date.now() / 1000 + ttl), + recipient: wallet.address, + slippageTolerance: this.getAllowedSlippage(allowedSlippage), + } + ); + + return this.chain.nonceManager.provideNonce( + nonce, + wallet.address, + async (nextNonce) => { + let tx: ContractTransaction; + if (maxFeePerGas !== undefined || maxPriorityFeePerGas !== undefined) { + tx = await wallet.sendTransaction({ + data: methodParameters.calldata, + to: uniswapRouter, + gasLimit: gasLimit.toFixed(0), + value: methodParameters.value, + nonce: nextNonce, + maxFeePerGas, + maxPriorityFeePerGas, + }); + } else { + tx = await wallet.sendTransaction({ + data: methodParameters.calldata, + to: uniswapRouter, + gasPrice: (gasPrice * 1e9).toFixed(0), + gasLimit: gasLimit.toFixed(0), + value: methodParameters.value, + nonce: nextNonce, + }); + } + logger.info(JSON.stringify(tx)); + return tx; + } + ); + } + + private async getPool( + tokenA: Token, + tokenB: Token, + feeTier: FeeAmount, + poolId?: string + ): Promise { + const uniswapFactory = new Contract( + this._factoryAddress, + IUniswapV3FactoryABI, + this.chain.provider + ); + // Use ETCSwap V3 factory to get pool address instead of `Pool.getAddress` to check if pool exists. + const poolAddress = poolId || await uniswapFactory.getPool( + tokenA.address, + tokenB.address, + feeTier + ); + if (poolAddress === constants.AddressZero || poolAddress === undefined || poolAddress === '') { + return null; + } + const poolContract = new Contract( + poolAddress, + IUniswapV3PoolABI, + this.chain.provider + ); + + const [liquidity, slot0, fee] = await Promise.all([ + poolContract.liquidity(), + poolContract.slot0(), + poolContract.fee(), + ]); + const [sqrtPriceX96, tick] = slot0; + + const pool = new Pool( + tokenA, + tokenB, + fee, + sqrtPriceX96, + liquidity, + tick + ); + + return pool; + } + + private async getQuote( + swapRoute: Route, + quoteToken: Token, + amount: CurrencyAmount, + tradeType: TradeType + ) { + const { calldata } = await SwapQuoter.quoteCallParameters( + swapRoute, + amount, + tradeType, + { useQuoterV2: true } + ); + const quoteCallReturnData = await this.chain.provider.call({ + to: this._quoterContractAddress, + data: calldata, + }); + const quoteTokenRawAmount = utils.defaultAbiCoder.decode( + ['uint256'], + quoteCallReturnData + ); + const qouteTokenAmount = CurrencyAmount.fromRawAmount( + quoteToken, + quoteTokenRawAmount.toString() + ); + return qouteTokenAmount; + } +} diff --git a/src/connectors/uniswap/uniswap.ts b/src/connectors/uniswap/uniswap.ts index 58382bcd44..44643a46cd 100644 --- a/src/connectors/uniswap/uniswap.ts +++ b/src/connectors/uniswap/uniswap.ts @@ -39,7 +39,7 @@ import { Ethereum } from '../../chains/ethereum/ethereum'; import { Avalanche } from '../../chains/avalanche/avalanche'; import { Polygon } from '../../chains/polygon/polygon'; import { BinanceSmartChain } from "../../chains/binance-smart-chain/binance-smart-chain"; -import { ExpectedTrade, Uniswapish } from '../../services/common-interfaces'; +import { ExpectedTrade, Uniswapish, UniswapishTrade } from '../../services/common-interfaces'; import { getAddress } from 'ethers/lib/utils'; import { Celo } from '../../chains/celo/celo'; @@ -261,7 +261,7 @@ export class Uniswap implements Uniswapish { const expectedAmount = route.trade.minimumAmountOut( this.getAllowedSlippage(allowedSlippage) ); - return { trade: route.trade, expectedAmount }; + return { trade: route.trade as unknown as UniswapishTrade, expectedAmount }; } else { const pool = await this.getPool(baseToken, quoteToken, this._feeTier, poolId); if (!pool) { @@ -344,7 +344,7 @@ export class Uniswap implements Uniswapish { const expectedAmount = route.trade.maximumAmountIn( this.getAllowedSlippage(allowedSlippage) ); - return { trade: route.trade, expectedAmount }; + return { trade: route.trade as unknown as UniswapishTrade, expectedAmount }; } else { const pool = await this.getPool(quoteToken, baseToken, this._feeTier, poolId); if (!pool) { diff --git a/src/services/connection-manager.ts b/src/services/connection-manager.ts index 517f7eb5e4..a19f607a23 100644 --- a/src/services/connection-manager.ts +++ b/src/services/connection-manager.ts @@ -49,6 +49,7 @@ import { Carbonamm } from '../connectors/carbon/carbonAMM'; import { Balancer } from '../connectors/balancer/balancer'; import { ETCSwapLP } from '../connectors/etcswap/etcswap.lp'; import { EthereumClassicChain } from '../chains/ethereum-classic/ethereum-classic'; +import { ETCSwap } from '../connectors/etcswap/etcswap'; export type ChainUnion = | Algorand @@ -241,6 +242,9 @@ export async function getConnector( connectorInstance = Tinyman.getInstance(network); } else if (connector === 'plenty') { connectorInstance = Plenty.getInstance(network); + } else if (chain === 'ethereum-classic' && connector === 'etcswap' + ) { + connectorInstance = ETCSwap.getInstance(chain, network) } else if (chain === 'ethereum-classic' && connector === 'etcswapLP' ) { connectorInstance = ETCSwapLP.getInstance(chain, network) diff --git a/src/services/schema/etcswap-schema.json b/src/services/schema/etcswap-schema.json index 88e6c06a5b..5ae1f35a96 100644 --- a/src/services/schema/etcswap-schema.json +++ b/src/services/schema/etcswap-schema.json @@ -19,12 +19,16 @@ "routerAddress": { "type": "string" }, "etcswapV3SmartOrderRouterAddress": { "type": "string" }, "etcswapV3NftManagerAddress": { "type": "string" }, - "etcswapV3QuoterV2ContractAddress": { "type": "string" } + "etcswapV3QuoterV2ContractAddress": { "type": "string" }, + "etcswapV3FactoryAddress": { + "type": "string" + } }, "required": [ "routerAddress", "etcswapV3SmartOrderRouterAddress", - "etcswapV3NftManagerAddress" + "etcswapV3NftManagerAddress", + "etcswapV3FactoryAddress" ], "additionalProperties": false } diff --git a/src/templates/etcswap.yml b/src/templates/etcswap.yml index da00dcb546..e0794de6f1 100644 --- a/src/templates/etcswap.yml +++ b/src/templates/etcswap.yml @@ -16,7 +16,7 @@ maximumHops: 4 # Use etcswap Router or Quoter to quote prices. # true - use Smart Order Router # false - use QuoterV2 Contract -useRouter: false +useRouter: true # Fee tier to use for the etcswap Quoter. # Required if `useRouter` is false. @@ -29,3 +29,4 @@ contractAddresses: etcswapV3SmartOrderRouterAddress: '0xEd88EDD995b00956097bF90d39C9341BBde324d1' etcswapV3NftManagerAddress: '0x3CEDe6562D6626A04d7502CC35720901999AB699' etcswapV3QuoterV2ContractAddress: '0x4d8c163400CB87Cbe1bae76dBf36A09FED85d39B' + etcswapV3FactoryAddress: '0x2624E907BcC04f93C8f29d7C7149a8700Ceb8cDC' diff --git a/yarn.lock b/yarn.lock index 92e05d55b2..1a30d80c8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,48 @@ # yarn lockfile v1 +"@_etcswap/smart-order-router@^3.15.0": + version "3.15.0" + resolved "https://registry.yarnpkg.com/@_etcswap/smart-order-router/-/smart-order-router-3.15.0.tgz#896ca0c5c1231be0df126e1b4bc5c4c1d9a003e7" + integrity sha512-2PcPSINeZM3vhkyr/r8JnCmEjjpdf38qt7Rcl5PgspsnFj1ndQUNsm1QsruQFEOW8eEn7PIL0ILBmPDW+8rviQ== + dependencies: + "@uniswap/default-token-list" "^11.2.0" + "@uniswap/permit2-sdk" "^1.2.0" + "@uniswap/router-sdk" "git+https://github.com/etcswap/router-sdk.git#etcswap" + "@uniswap/sdk-core" "git+https://github.com/etcswap/sdk-core.git#etcswap" + "@uniswap/swap-router-contracts" "^1.3.0" + "@uniswap/token-lists" "^1.0.0-beta.31" + "@uniswap/universal-router" "^1.0.1" + "@uniswap/universal-router-sdk" "git+https://github.com/etcswap/universal-router-sdk.git#etcswap" + "@uniswap/v2-sdk" "^3.2.0" + "@uniswap/v3-sdk" "git+https://github.com/etcswap/v3-sdk.git#etcswap" + async-retry "^1.3.1" + await-timeout "^1.1.1" + axios "^0.21.1" + bunyan "^1.8.15" + bunyan-blackhole "^1.1.1" + ethers "^5.7.2" + graphql "^15.5.0" + graphql-request "^3.4.0" + lodash "^4.17.21" + mnemonist "^0.38.3" + node-cache "^5.1.2" + stats-lite "^2.2.0" + +"@_etcswap/v3-sdk@^3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@_etcswap/v3-sdk/-/v3-sdk-3.10.1.tgz#d1ee236c26c10dc2b368f8f882711f657ce516b2" + integrity sha512-hfscgXCJXUjyu8IrFIXIemozlI/EAKVHtHZVshOAVXszeEsz+OkP3pqO0rQ3WMuO9SWG/9WtRHjYMeOWa2AVzg== + dependencies: + "@ethersproject/abi" "^5.0.12" + "@ethersproject/solidity" "^5.0.9" + "@uniswap/sdk-core" "git+https://github.com/etcswap/sdk-core.git#etcswap" + "@uniswap/swap-router-contracts" "git+https://github.com/etcswap/swap-router-contracts.git#etcswap" + "@uniswap/v3-periphery" "git+https://github.com/etcswap/v3-periphery.git#etcswap" + "@uniswap/v3-staker" "git+https://github.com/etcswap/v3-staker.git#etcswap" + tiny-invariant "^1.1.0" + tiny-warning "^1.0.3" + "@aashutoshrathi/word-wrap@^1.2.3": version "1.2.6" resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" @@ -1803,7 +1845,7 @@ "@ethersproject-xdc/properties" "file:../Library/Caches/Yarn/v6/npm-@ethersproject-xdc-wordlists-5.7.0-b87ba8fd-6587-4636-9f68-c0efe680928e-1722024267844/node_modules/@ethersproject-xdc/properties" "@ethersproject-xdc/strings" "file:../Library/Caches/Yarn/v6/npm-@ethersproject-xdc-wordlists-5.7.0-b87ba8fd-6587-4636-9f68-c0efe680928e-1722024267844/node_modules/@ethersproject-xdc/strings" -"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.1.2", "@ethersproject/abi@^5.4.0", "@ethersproject/abi@^5.5.0", "@ethersproject/abi@^5.6.3", "@ethersproject/abi@^5.7.0": +"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.0.12", "@ethersproject/abi@^5.1.2", "@ethersproject/abi@^5.4.0", "@ethersproject/abi@^5.5.0", "@ethersproject/abi@^5.6.3", "@ethersproject/abi@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449" integrity sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA== @@ -1842,7 +1884,7 @@ "@ethersproject/logger" "^5.7.0" "@ethersproject/properties" "^5.7.0" -"@ethersproject/address@5.7.0", "@ethersproject/address@^5.0.2", "@ethersproject/address@^5.4.0", "@ethersproject/address@^5.7.0": +"@ethersproject/address@5.7.0", "@ethersproject/address@^5.0.0", "@ethersproject/address@^5.0.2", "@ethersproject/address@^5.4.0", "@ethersproject/address@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37" integrity sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA== @@ -2106,7 +2148,7 @@ elliptic "6.5.4" hash.js "1.1.7" -"@ethersproject/solidity@5.7.0", "@ethersproject/solidity@^5.0.9", "@ethersproject/solidity@^5.4.0": +"@ethersproject/solidity@5.7.0", "@ethersproject/solidity@^5.0.0", "@ethersproject/solidity@^5.0.9", "@ethersproject/solidity@^5.4.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.7.0.tgz#5e9c911d8a2acce2a5ebb48a5e2e0af20b631cb8" integrity sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA== @@ -5031,7 +5073,7 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@uniswap/default-token-list@^11.13.0": +"@uniswap/default-token-list@^11.13.0", "@uniswap/default-token-list@^11.2.0": version "11.19.0" resolved "https://registry.yarnpkg.com/@uniswap/default-token-list/-/default-token-list-11.19.0.tgz#12d4e40f6c79f794d3e3a71e2d4d9784fb6c967b" integrity sha512-H/YLpxeZUrzT4Ki8mi4k5UiadREiLHg7WUqCv0Qt/VkOjX2mIBhrxCj1Wh61/J7lK0XqOjksfpm6RG1+YErPoQ== @@ -5041,7 +5083,7 @@ resolved "https://registry.yarnpkg.com/@uniswap/lib/-/lib-4.0.1-alpha.tgz#2881008e55f075344675b3bca93f020b028fbd02" integrity sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA== -"@uniswap/permit2-sdk@^1.3.0": +"@uniswap/permit2-sdk@^1.2.0", "@uniswap/permit2-sdk@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@uniswap/permit2-sdk/-/permit2-sdk-1.3.0.tgz#b54124e570f0adbaca9d39b2de3054fd7d3798a1" integrity sha512-LstYQWP47dwpQrgqBJ+ysFstne9LgI5FGiKHc2ewjj91MTY8Mq1reocu6U/VDncdR5ef30TUOcZ7gPExRY8r6Q== @@ -5060,6 +5102,28 @@ "@uniswap/v2-sdk" "^4.3.2" "@uniswap/v3-sdk" "^3.11.2" +"@uniswap/router-sdk@git+https://github.com/etcswap/router-sdk.git#etcswap": + version "1.6.0" + resolved "git+https://github.com/etcswap/router-sdk.git#942b98a36bad29ee7fe4ca362f4d02062dc62c44" + dependencies: + "@ethersproject/abi" "^5.5.0" + "@uniswap/sdk-core" "git+https://github.com/etcswap/sdk-core.git#etcswap" + "@uniswap/swap-router-contracts" "git+https://github.com/etcswap/swap-router-contracts.git#etcswap" + "@uniswap/v2-sdk" "^3.2.0" + "@uniswap/v3-sdk" "git+https://github.com/etcswap/v3-sdk.git#etcswap" + +"@uniswap/sdk-core@^4.0.7": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@uniswap/sdk-core/-/sdk-core-4.2.1.tgz#7b8c6fee48446bb67a4e6f2e9cb94c862034a6c3" + integrity sha512-hr7vwYrXScg+V8/rRc2UL/Ixc/p0P7yqe4D/OxzUdMRYr8RZd+8z5Iu9+WembjZT/DCdbTjde6lsph4Og0n1BQ== + dependencies: + "@ethersproject/address" "^5.0.2" + big.js "^5.2.2" + decimal.js-light "^2.5.0" + jsbi "^3.1.4" + tiny-invariant "^1.1.0" + toformat "^2.0.0" + "@uniswap/sdk-core@^5.3.0", "@uniswap/sdk-core@^5.3.1": version "5.3.1" resolved "https://registry.yarnpkg.com/@uniswap/sdk-core/-/sdk-core-5.3.1.tgz#22d753e9ef8666c2f3b4d9a89b9658d169be4ce8" @@ -5075,6 +5139,17 @@ tiny-invariant "^1.1.0" toformat "^2.0.0" +"@uniswap/sdk-core@git+https://github.com/etcswap/sdk-core.git#etcswap": + version "4.0.10" + resolved "git+https://github.com/etcswap/sdk-core.git#af7b64fd4dfb0b1de5375ffd3aed6c152b726331" + dependencies: + "@ethersproject/address" "^5.0.2" + big.js "^5.2.2" + decimal.js-light "^2.5.0" + jsbi "^3.1.4" + tiny-invariant "^1.1.0" + toformat "^2.0.0" + "@uniswap/sdk@3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@uniswap/sdk/-/sdk-3.0.3.tgz#8201c7c72215d0030cb99acc7e661eff895c18a9" @@ -5143,6 +5218,16 @@ dotenv "^14.2.0" hardhat-watcher "^2.1.1" +"@uniswap/swap-router-contracts@git+https://github.com/etcswap/swap-router-contracts.git#etcswap": + version "1.1.0" + resolved "git+https://github.com/etcswap/swap-router-contracts.git#d02515054efae1c8c1c8843b437138cb7966ca2f" + dependencies: + "@openzeppelin/contracts" "3.4.1-solc-0.7-2" + "@uniswap/v2-core" "1.0.1" + "@uniswap/v3-core" "git+https://github.com/etcswap/v3-core.git#etcswap" + "@uniswap/v3-periphery" "git+https://github.com/etcswap/v3-periphery.git#etcswap" + hardhat-watcher "^2.1.1" + "@uniswap/token-lists@^1.0.0-beta.31": version "1.0.0-beta.34" resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.34.tgz#879461f5d4009327a24259bbab797e0f22db58c8" @@ -5162,7 +5247,29 @@ bignumber.js "^9.0.2" ethers "^5.7.0" -"@uniswap/universal-router@1.6.0", "@uniswap/universal-router@^1.6.0": +"@uniswap/universal-router-sdk@git+https://github.com/etcswap/universal-router-sdk.git#etcswap": + version "2.0.1" + resolved "git+https://github.com/etcswap/universal-router-sdk.git#e50ac7aa92ae651dfa462e769133e55dc547253a" + dependencies: + "@uniswap/permit2-sdk" "^1.2.0" + "@uniswap/router-sdk" "git+https://github.com/etcswap/router-sdk.git#etcswap" + "@uniswap/sdk-core" "git+https://github.com/etcswap/sdk-core.git#etcswap" + "@uniswap/universal-router" "1.5.1" + "@uniswap/v2-sdk" "^3.2.0" + "@uniswap/v3-sdk" "git+https://github.com/etcswap/v3-sdk.git#etcswap" + bignumber.js "^9.0.2" + ethers "^5.3.1" + +"@uniswap/universal-router@1.5.1": + version "1.5.1" + resolved "https://registry.yarnpkg.com/@uniswap/universal-router/-/universal-router-1.5.1.tgz#2ce832485eb85093b0cb94a53be20661e1aece70" + integrity sha512-+htTC/nHQXKfY/c+9C1XHMRs7Jz0bX9LQfYn9Hb7WZKZ/YHWhOsCZQylYhksieLYTRam5sQheow747hOZ+QpZQ== + dependencies: + "@openzeppelin/contracts" "4.7.0" + "@uniswap/v2-core" "1.0.1" + "@uniswap/v3-core" "1.0.0" + +"@uniswap/universal-router@1.6.0", "@uniswap/universal-router@^1.0.1", "@uniswap/universal-router@^1.6.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@uniswap/universal-router/-/universal-router-1.6.0.tgz#3d7372e98a0303c70587802ee6841b8b6b42fc6f" integrity sha512-Gt0b0rtMV1vSrgXY3vz5R1RCZENB+rOkbOidY9GvcXrK1MstSrQSOAc+FCr8FSgsDhmRAdft0lk5YUxtM9i9Lg== @@ -5176,6 +5283,17 @@ resolved "https://registry.yarnpkg.com/@uniswap/v2-core/-/v2-core-1.0.1.tgz#af8f508bf183204779938969e2e54043e147d425" integrity sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q== +"@uniswap/v2-sdk@^3.2.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@uniswap/v2-sdk/-/v2-sdk-3.3.0.tgz#76c95d234fe73ca6ad34ba9509f7451955ee0ce7" + integrity sha512-cf5PjoNQN5tNELIOVJsqV4/VeuDtxFw6Zl8oFmFJ6PNoQ8sx+XnGoO0aGKTB/o5II3oQ7820xtY3k47UsXgd6A== + dependencies: + "@ethersproject/address" "^5.0.0" + "@ethersproject/solidity" "^5.0.0" + "@uniswap/sdk-core" "^4.0.7" + tiny-invariant "^1.1.0" + tiny-warning "^1.0.3" + "@uniswap/v2-sdk@^4.3.2", "@uniswap/v2-sdk@^4.4.1": version "4.4.1" resolved "https://registry.yarnpkg.com/@uniswap/v2-sdk/-/v2-sdk-4.4.1.tgz#d0859a2d943cfcf66ec3cd48c2019e393af256a1" @@ -5192,10 +5310,9 @@ resolved "https://registry.yarnpkg.com/@uniswap/v3-core/-/v3-core-1.0.0.tgz#6c24adacc4c25dceee0ba3ca142b35adbd7e359d" integrity sha512-kSC4djMGKMHj7sLMYVnn61k9nu+lHjMIxgg9CDQT+s2QYLoA56GbSK9Oxr+qJXzzygbkrmuY6cwgP6cW2JXPFA== -"@uniswap/v3-core@^1.0.0", "@uniswap/v3-core@^1.0.1": +"@uniswap/v3-core@^1.0.0", "@uniswap/v3-core@^1.0.1", "@uniswap/v3-core@git+https://github.com/etcswap/v3-core.git#etcswap": version "1.0.1" - resolved "https://registry.yarnpkg.com/@uniswap/v3-core/-/v3-core-1.0.1.tgz#b6d2bdc6ba3c3fbd610bdc502395d86cd35264a0" - integrity sha512-7pVk4hEm00j9tc71Y9+ssYpO6ytkeI0y7WE9P6UcmNzhxPePwyAxImuhVsTqWK9YFvzgtvzJHi64pBl4jUzKMQ== + resolved "git+https://github.com/etcswap/v3-core.git#9200d0cbd1aca285004f53811c29674bc9c5e798" "@uniswap/v3-periphery@1.4.1": version "1.4.1" @@ -5220,15 +5337,14 @@ "@uniswap/v3-core" "1.0.0" base64-sol "1.0.1" -"@uniswap/v3-periphery@^1.4.4": +"@uniswap/v3-periphery@^1.4.4", "@uniswap/v3-periphery@git+https://github.com/etcswap/v3-periphery.git#etcswap": version "1.4.4" - resolved "https://registry.yarnpkg.com/@uniswap/v3-periphery/-/v3-periphery-1.4.4.tgz#d2756c23b69718173c5874f37fd4ad57d2f021b7" - integrity sha512-S4+m+wh8HbWSO3DKk4LwUCPZJTpCugIsHrWR86m/OrUyvSqGDTXKFfc2sMuGXCZrD1ZqO3rhQsKgdWg3Hbb2Kw== + resolved "git+https://github.com/etcswap/v3-periphery.git#b6ce8ef8a553597d89b700a604096cac1d0c9485" dependencies: "@openzeppelin/contracts" "3.4.2-solc-0.7" "@uniswap/lib" "^4.0.1-alpha" "@uniswap/v2-core" "^1.0.1" - "@uniswap/v3-core" "^1.0.0" + "@uniswap/v3-core" "git+https://github.com/etcswap/v3-core.git#etcswap" base64-sol "1.0.1" "@uniswap/v3-sdk@^3.11.2", "@uniswap/v3-sdk@^3.13.0", "@uniswap/v3-sdk@^3.13.1": @@ -5245,6 +5361,19 @@ tiny-invariant "^1.1.0" tiny-warning "^1.0.3" +"@uniswap/v3-sdk@git+https://github.com/etcswap/v3-sdk.git#etcswap": + version "3.10.1" + resolved "git+https://github.com/etcswap/v3-sdk.git#ea339f3d6d28814d02b2618b9a8ef045a40d5f7d" + dependencies: + "@ethersproject/abi" "^5.0.12" + "@ethersproject/solidity" "^5.0.9" + "@uniswap/sdk-core" "git+https://github.com/etcswap/sdk-core.git#etcswap" + "@uniswap/swap-router-contracts" "git+https://github.com/etcswap/swap-router-contracts.git#etcswap" + "@uniswap/v3-periphery" "git+https://github.com/etcswap/v3-periphery.git#etcswap" + "@uniswap/v3-staker" "git+https://github.com/etcswap/v3-staker.git#etcswap" + tiny-invariant "^1.1.0" + tiny-warning "^1.0.3" + "@uniswap/v3-staker@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@uniswap/v3-staker/-/v3-staker-1.0.0.tgz#9a6915ec980852479dfc903f50baf822ff8fa66e" @@ -5254,6 +5383,14 @@ "@uniswap/v3-core" "1.0.0" "@uniswap/v3-periphery" "^1.0.1" +"@uniswap/v3-staker@git+https://github.com/etcswap/v3-staker.git#etcswap": + version "1.0.2" + resolved "git+https://github.com/etcswap/v3-staker.git#615e09790d2ec9f2bc079756eb888a2bd2078be6" + dependencies: + "@openzeppelin/contracts" "3.4.1-solc-0.7-2" + "@uniswap/v3-core" "git+https://github.com/etcswap/v3-core.git#etcswap" + "@uniswap/v3-periphery" "git+https://github.com/etcswap/v3-periphery.git#etcswap" + "@vespaiach/axios-fetch-adapter@github:ecadlabs/axios-fetch-adapter": version "0.3.1" resolved "https://codeload.github.com/ecadlabs/axios-fetch-adapter/tar.gz/167684f522e90343b9f3439d9a43ac571e2396f6" @@ -8399,7 +8536,7 @@ ethers@4.0.0-beta.3: uuid "2.0.1" xmlhttprequest "1.8.0" -ethers@5.7.2, ethers@^5.0.19, ethers@^5.6.1, ethers@^5.6.2, ethers@^5.7.0, ethers@^5.7.2: +ethers@5.7.2, ethers@^5.0.19, ethers@^5.3.1, ethers@^5.6.1, ethers@^5.6.2, ethers@^5.7.0, ethers@^5.7.2: version "5.7.2" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== From e9a2e7290609dc275fc503ebd1c6ddd68f0abffb Mon Sep 17 00:00:00 2001 From: vic-en Date: Sun, 8 Sep 2024 18:05:06 -0500 Subject: [PATCH 12/13] v2 and v3 swaps with unit test --- package.json | 7 +- .../ethereum-classic/ethereum-classic.ts | 25 +- src/chains/ethereum/ethereum.validators.ts | 1 + src/connectors/etcswap/etcswap.ts | 191 ++--- src/services/connection-manager.ts | 10 +- .../connectors/etcSwap/etcSwap.routes.test.ts | 678 ++++++++++++++++++ .../connectors/etcSwap/etcSwap.test.ts | 306 ++++++++ yarn.lock | 94 ++- 8 files changed, 1173 insertions(+), 139 deletions(-) create mode 100644 test-bronze/connectors/etcSwap/etcSwap.routes.test.ts create mode 100644 test-bronze/connectors/etcSwap/etcSwap.test.ts diff --git a/package.json b/package.json index 624c667ef0..d992190456 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,10 @@ "test:scripts": "jest -i --verbose ./test-scripts/*.test.ts" }, "dependencies": { - "@_etcswap/v3-sdk": "^3.10.1", - "@_etcswap/smart-order-router": "^3.15.0", - "@cosmjs/amino": "^0.32.2", + "@_etcswap/smart-order-router": "^3.15.2", "@balancer-labs/sdk": "^1.1.5", "@bancor/carbon-sdk": "^0.0.93-DEV", + "@cosmjs/amino": "^0.32.2", "@cosmjs/proto-signing": "^0.31.1", "@cosmjs/stargate": "^0.31.1", "@cosmjs/tendermint-rpc": "^0.32.2", @@ -65,7 +64,7 @@ "@types/uuid": "^8.3.4", "@uniswap/sdk": "3.0.3", "@uniswap/sdk-core": "^5.3.1", - "@uniswap/smart-order-router": "^3.39.0", + "@uniswap/smart-order-router": "^3.46.1", "@uniswap/v3-core": "^1.0.1", "@uniswap/v3-periphery": "^1.1.1", "@uniswap/v3-sdk": "^3.13.1", diff --git a/src/chains/ethereum-classic/ethereum-classic.ts b/src/chains/ethereum-classic/ethereum-classic.ts index 01d5e34cd8..7acf3e925d 100644 --- a/src/chains/ethereum-classic/ethereum-classic.ts +++ b/src/chains/ethereum-classic/ethereum-classic.ts @@ -28,7 +28,7 @@ export class EthereumClassicChain extends EthereumBase implements Ethereumish { config.manualGasPrice, config.gasLimitTransaction, ConfigManagerV2.getInstance().get('server.nonceDbPath'), - ConfigManagerV2.getInstance().get('server.transactionDbPath') + ConfigManagerV2.getInstance().get('server.transactionDbPath'), ); this._chain = config.network.name; this._nativeTokenSymbol = config.nativeCurrencySymbol; @@ -47,13 +47,17 @@ export class EthereumClassicChain extends EthereumBase implements Ethereumish { EthereumClassicChain._instances = {}; } if (!(network in EthereumClassicChain._instances)) { - EthereumClassicChain._instances[network] = new EthereumClassicChain(network); + EthereumClassicChain._instances[network] = new EthereumClassicChain( + network, + ); } return EthereumClassicChain._instances[network]; } - public static getConnectedInstances(): { [name: string]: EthereumClassicChain } { + public static getConnectedInstances(): { + [name: string]: EthereumClassicChain; + } { return EthereumClassicChain._instances; } @@ -71,7 +75,7 @@ export class EthereumClassicChain extends EthereumBase implements Ethereumish { setTimeout( this.updateGasPrice.bind(this), - this._gasPriceRefreshInterval * 1000 + this._gasPriceRefreshInterval * 1000, ); } @@ -89,6 +93,11 @@ export class EthereumClassicChain extends EthereumBase implements Ethereumish { return this._chain; } + // in place for mocking + public get provider() { + return super.provider; + } + getContract(tokenAddress: string, signerOrProvider?: Wallet | Provider) { return new Contract(tokenAddress, abi.ERC20Abi, signerOrProvider); } @@ -96,8 +105,10 @@ export class EthereumClassicChain extends EthereumBase implements Ethereumish { getSpender(reqSpender: string): string { let spender: string; if (reqSpender === 'etcswapLP') { - spender = ETCSwapConfig.config.etcswapV3NftManagerAddress( - this._chain + spender = ETCSwapConfig.config.etcswapV3NftManagerAddress(this._chain); + } else if (reqSpender === 'etcswap') { + spender = ETCSwapConfig.config.etcswapV3SmartOrderRouterAddress( + this._chain, ); } else { spender = reqSpender; @@ -108,7 +119,7 @@ export class EthereumClassicChain extends EthereumBase implements Ethereumish { // cancel transaction async cancelTx(wallet: Wallet, nonce: number): Promise { logger.info( - 'Canceling any existing transaction(s) with nonce number ' + nonce + '.' + 'Canceling any existing transaction(s) with nonce number ' + nonce + '.', ); return super.cancelTxWithGasPrice(wallet, nonce, this._gasPrice * 2); } diff --git a/src/chains/ethereum/ethereum.validators.ts b/src/chains/ethereum/ethereum.validators.ts index d7bd684bde..f061ebf545 100644 --- a/src/chains/ethereum/ethereum.validators.ts +++ b/src/chains/ethereum/ethereum.validators.ts @@ -66,6 +66,7 @@ export const validateSpender: Validator = mkValidator( val === 'carbonamm' || val === 'balancer' || val === 'etcswapLP' || + val === 'etcswap' || isAddress(val)) ); diff --git a/src/connectors/etcswap/etcswap.ts b/src/connectors/etcswap/etcswap.ts index f3bcf653f2..9f13ae8e33 100644 --- a/src/connectors/etcswap/etcswap.ts +++ b/src/connectors/etcswap/etcswap.ts @@ -4,27 +4,29 @@ import { ContractInterface, ContractTransaction, } from '@ethersproject/contracts'; -// import { AlphaRouter } from '@uniswap/smart-order-router'; import { AlphaRouter } from '@_etcswap/smart-order-router'; import routerAbi from '../uniswap/uniswap_v2_router_abi.json'; -import { Trade, SwapRouter } from '@uniswap/router-sdk'; import { FeeAmount, MethodParameters, Pool, SwapQuoter, - Trade as UniswapV3Trade, - Route -} from '@uniswap/v3-sdk'; -import { abi as IUniswapV3PoolABI } from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json'; -import { abi as IUniswapV3FactoryABI } from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Factory.sol/IUniswapV3Factory.json'; + Trade as EtcswapV3Trade, + Route, +} from '@_etcswap/smart-order-router/node_modules/@uniswap/v3-sdk'; +import { + SwapRouter, + Trade, +} from '@_etcswap/smart-order-router/node_modules/@uniswap/router-sdk'; +import { abi as IEtcswapV3PoolABI } from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json'; +import { abi as IEtcswapV3FactoryABI } from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Factory.sol/IUniswapV3Factory.json'; import { Token, + Currency, CurrencyAmount, - Percent, TradeType, - Currency, -} from '@uniswap/sdk-core'; + Percent, +} from '@_etcswap/smart-order-router/node_modules/@uniswap/sdk-core'; import { BigNumber, Transaction, @@ -35,7 +37,11 @@ import { } from 'ethers'; import { logger } from '../../services/logger'; import { percentRegexp } from '../../services/config-manager-v2'; -import { ExpectedTrade, Uniswapish } from '../../services/common-interfaces'; +import { + ExpectedTrade, + Uniswapish, + UniswapishTrade, +} from '../../services/common-interfaces'; import { getAddress } from 'ethers/lib/utils'; import { EthereumClassicChain } from '../../chains/ethereum-classic/ethereum-classic'; import { ETCSwapConfig } from './etcswap.config'; @@ -78,7 +84,7 @@ export class ETCSwap implements Uniswapish { } if (config.useRouter === false && config.quoterContractAddress == null) { throw new Error( - 'Must specify quoter contract address if not using router' + 'Must specify quoter contract address if not using router', ); } this._useRouter = config.useRouter ?? true; @@ -120,7 +126,7 @@ export class ETCSwap implements Uniswapish { token.address, token.decimals, token.symbol, - token.name + token.name, ); } this._ready = true; @@ -191,7 +197,7 @@ export class ETCSwap implements Uniswapish { const nd = allowedSlippage.match(percentRegexp); if (nd) return new Percent(nd[1], nd[2]); throw new Error( - 'Encountered a malformed percent string in the config for ALLOWED_SLIPPAGE.' + 'Encountered a malformed percent string in the config for ALLOWED_SLIPPAGE.', ); } @@ -210,13 +216,13 @@ export class ETCSwap implements Uniswapish { quoteToken: Token, amount: BigNumber, allowedSlippage?: string, - poolId?: string + poolId?: string, ): Promise { const nativeTokenAmount: CurrencyAmount = CurrencyAmount.fromRawAmount(baseToken, amount.toString()); logger.info( - `Fetching trade data for ${baseToken.address}-${quoteToken.address}.` + `Fetching trade data for ${baseToken.address}-${quoteToken.address}.`, ); if (this._useRouter) { @@ -230,28 +236,36 @@ export class ETCSwap implements Uniswapish { undefined, { maxSwapsPerPath: this.maximumHops, - } + }, ); if (!route) { throw new UniswapishPriceError( - `priceSwapIn: no trade pair found for ${baseToken.address} to ${quoteToken.address}.` + `priceSwapIn: no trade pair found for ${baseToken.address} to ${quoteToken.address}.`, ); } logger.info( `Best trade for ${baseToken.address}-${quoteToken.address}: ` + - `${route.trade.executionPrice.toFixed(6)}` + - `${baseToken.symbol}.` + `${route.trade.executionPrice.toFixed(6)}` + + `${baseToken.symbol}.`, ); - const expectedAmount = route.trade.minimumAmountOut( - this.getAllowedSlippage(allowedSlippage) + const expectedAmount = route.trade.maximumAmountIn( + this.getAllowedSlippage(allowedSlippage), ); - return { trade: route.trade, expectedAmount }; + return { + trade: route.trade as unknown as UniswapishTrade, + expectedAmount, + }; } else { - const pool = await this.getPool(baseToken, quoteToken, this._feeTier, poolId); + 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}.` + `priceSwapIn: no trade pair found for ${baseToken.address} to ${quoteToken.address}.`, ); } const swapRoute = new Route([pool], baseToken, quoteToken); @@ -259,9 +273,9 @@ export class ETCSwap implements Uniswapish { swapRoute, quoteToken, nativeTokenAmount, - TradeType.EXACT_INPUT + TradeType.EXACT_INPUT, ); - const trade = UniswapV3Trade.createUncheckedTrade({ + const trade = EtcswapV3Trade.createUncheckedTrade({ route: swapRoute, inputAmount: nativeTokenAmount, outputAmount: quotedAmount, @@ -269,13 +283,13 @@ export class ETCSwap implements Uniswapish { }); logger.info( `Best trade for ${baseToken.address}-${quoteToken.address}: ` + - `${trade.executionPrice.toFixed(6)}` + - `${baseToken.symbol}.` + `${trade.executionPrice.toFixed(6)}` + + `${baseToken.symbol}.`, ); const expectedAmount = trade.minimumAmountOut( - this.getAllowedSlippage(allowedSlippage) + this.getAllowedSlippage(allowedSlippage), ); - return { trade, expectedAmount }; + return { trade: trade as unknown as UniswapishTrade, expectedAmount }; } } @@ -294,12 +308,12 @@ export class ETCSwap implements Uniswapish { baseToken: Token, amount: BigNumber, allowedSlippage?: string, - poolId?: string + poolId?: string, ): Promise { const nativeTokenAmount: CurrencyAmount = CurrencyAmount.fromRawAmount(baseToken, amount.toString()); logger.info( - `Fetching pair data for ${quoteToken.address}-${baseToken.address}.` + `Fetching pair data for ${quoteToken.address}-${baseToken.address}.`, ); if (this._useRouter) { @@ -313,28 +327,36 @@ export class ETCSwap implements Uniswapish { undefined, { maxSwapsPerPath: this.maximumHops, - } + }, ); if (!route) { throw new UniswapishPriceError( - `priceSwapOut: no trade pair found for ${quoteToken.address} to ${baseToken.address}.` + `priceSwapOut: no trade pair found for ${quoteToken.address} to ${baseToken.address}.`, ); } logger.info( `Best trade for ${quoteToken.address}-${baseToken.address}: ` + - `${route.trade.executionPrice.invert().toFixed(6)} ` + - `${baseToken.symbol}.` + `${route.trade.executionPrice.invert().toFixed(6)} ` + + `${baseToken.symbol}.`, ); const expectedAmount = route.trade.maximumAmountIn( - this.getAllowedSlippage(allowedSlippage) + this.getAllowedSlippage(allowedSlippage), ); - return { trade: route.trade, expectedAmount }; + return { + trade: route.trade as unknown as UniswapishTrade, + expectedAmount, + }; } else { - const pool = await this.getPool(quoteToken, baseToken, this._feeTier, poolId); + 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}.` + `priceSwapOut: no trade pair found for ${quoteToken.address} to ${baseToken.address}.`, ); } const swapRoute = new Route([pool], quoteToken, baseToken); @@ -342,9 +364,9 @@ export class ETCSwap implements Uniswapish { swapRoute, quoteToken, nativeTokenAmount, - TradeType.EXACT_OUTPUT + TradeType.EXACT_OUTPUT, ); - const trade = UniswapV3Trade.createUncheckedTrade({ + const trade = EtcswapV3Trade.createUncheckedTrade({ route: swapRoute, inputAmount: quotedAmount, outputAmount: nativeTokenAmount, @@ -352,13 +374,13 @@ export class ETCSwap implements Uniswapish { }); logger.info( `Best trade for ${baseToken.address}-${quoteToken.address}: ` + - `${trade.executionPrice.invert().toFixed(6)}` + - `${baseToken.symbol}.` + `${trade.executionPrice.invert().toFixed(6)}` + + `${baseToken.symbol}.`, ); const expectedAmount = trade.maximumAmountIn( - this.getAllowedSlippage(allowedSlippage) + this.getAllowedSlippage(allowedSlippage), ); - return { trade, expectedAmount }; + return { trade: trade as unknown as UniswapishTrade, expectedAmount }; } } @@ -368,7 +390,7 @@ export class ETCSwap implements Uniswapish { * @param wallet Wallet * @param trade Expected trade * @param gasPrice Base gas price, for pre-EIP1559 transactions - * @param uniswapRouter Router smart contract address + * @param etcswapRouter 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 @@ -378,24 +400,24 @@ export class ETCSwap implements Uniswapish { */ async executeTrade( wallet: Wallet, - trade: Trade, + trade: UniswapishTrade, gasPrice: number, - uniswapRouter: string, + etcswapRouter: string, ttl: number, _abi: ContractInterface, gasLimit: number, nonce?: number, maxFeePerGas?: BigNumber, maxPriorityFeePerGas?: BigNumber, - allowedSlippage?: string + allowedSlippage?: string, ): Promise { const methodParameters: MethodParameters = SwapRouter.swapCallParameters( - trade, + trade as unknown as Trade, { deadlineOrPreviousBlockhash: Math.floor(Date.now() / 1000 + ttl), recipient: wallet.address, slippageTolerance: this.getAllowedSlippage(allowedSlippage), - } + }, ); return this.chain.nonceManager.provideNonce( @@ -406,26 +428,26 @@ export class ETCSwap implements Uniswapish { if (maxFeePerGas !== undefined || maxPriorityFeePerGas !== undefined) { tx = await wallet.sendTransaction({ data: methodParameters.calldata, - to: uniswapRouter, - gasLimit: gasLimit.toFixed(0), - value: methodParameters.value, - nonce: nextNonce, + to: etcswapRouter, + gasLimit: BigNumber.from(String(gasLimit.toFixed(0))), + value: BigNumber.from(methodParameters.value), + nonce: BigNumber.from(String(nextNonce)), maxFeePerGas, maxPriorityFeePerGas, }); } else { tx = await wallet.sendTransaction({ data: methodParameters.calldata, - to: uniswapRouter, - gasPrice: (gasPrice * 1e9).toFixed(0), - gasLimit: gasLimit.toFixed(0), - value: methodParameters.value, - nonce: nextNonce, + to: etcswapRouter, + gasPrice: BigNumber.from(String((gasPrice * 1e9).toFixed(0))), + gasLimit: BigNumber.from(String(gasLimit.toFixed(0))), + value: BigNumber.from(methodParameters.value), + nonce: BigNumber.from(String(nextNonce)), }); } logger.info(JSON.stringify(tx)); return tx; - } + }, ); } @@ -433,26 +455,28 @@ export class ETCSwap implements Uniswapish { tokenA: Token, tokenB: Token, feeTier: FeeAmount, - poolId?: string + poolId?: string, ): Promise { - const uniswapFactory = new Contract( + const etcswapFactory = new Contract( this._factoryAddress, - IUniswapV3FactoryABI, - this.chain.provider + IEtcswapV3FactoryABI, + this.chain.provider, ); // Use ETCSwap V3 factory to get pool address instead of `Pool.getAddress` to check if pool exists. - const poolAddress = poolId || await uniswapFactory.getPool( - tokenA.address, - tokenB.address, - feeTier - ); - if (poolAddress === constants.AddressZero || poolAddress === undefined || poolAddress === '') { + const poolAddress = + poolId || + (await etcswapFactory.getPool(tokenA.address, tokenB.address, feeTier)); + if ( + poolAddress === constants.AddressZero || + poolAddress === undefined || + poolAddress === '' + ) { return null; } const poolContract = new Contract( poolAddress, - IUniswapV3PoolABI, - this.chain.provider + IEtcswapV3PoolABI, + this.chain.provider, ); const [liquidity, slot0, fee] = await Promise.all([ @@ -462,14 +486,7 @@ export class ETCSwap implements Uniswapish { ]); const [sqrtPriceX96, tick] = slot0; - const pool = new Pool( - tokenA, - tokenB, - fee, - sqrtPriceX96, - liquidity, - tick - ); + const pool = new Pool(tokenA, tokenB, fee, sqrtPriceX96, liquidity, tick); return pool; } @@ -478,13 +495,13 @@ export class ETCSwap implements Uniswapish { swapRoute: Route, quoteToken: Token, amount: CurrencyAmount, - tradeType: TradeType + tradeType: TradeType, ) { const { calldata } = await SwapQuoter.quoteCallParameters( swapRoute, amount, tradeType, - { useQuoterV2: true } + { useQuoterV2: true }, ); const quoteCallReturnData = await this.chain.provider.call({ to: this._quoterContractAddress, @@ -492,11 +509,11 @@ export class ETCSwap implements Uniswapish { }); const quoteTokenRawAmount = utils.defaultAbiCoder.decode( ['uint256'], - quoteCallReturnData + quoteCallReturnData, ); const qouteTokenAmount = CurrencyAmount.fromRawAmount( quoteToken, - quoteTokenRawAmount.toString() + quoteTokenRawAmount.toString(), ); return qouteTokenAmount; } diff --git a/src/services/connection-manager.ts b/src/services/connection-manager.ts index a19f607a23..bbf0fba314 100644 --- a/src/services/connection-manager.ts +++ b/src/services/connection-manager.ts @@ -242,12 +242,10 @@ export async function getConnector( connectorInstance = Tinyman.getInstance(network); } else if (connector === 'plenty') { connectorInstance = Plenty.getInstance(network); - } else if (chain === 'ethereum-classic' && connector === 'etcswap' - ) { - connectorInstance = ETCSwap.getInstance(chain, network) - } else if (chain === 'ethereum-classic' && connector === 'etcswapLP' - ) { - connectorInstance = ETCSwapLP.getInstance(chain, network) + } else if (chain === 'ethereum-classic' && connector === 'etcswap') { + connectorInstance = ETCSwap.getInstance(chain, network); + } else if (chain === 'ethereum-classic' && connector === 'etcswapLP') { + connectorInstance = ETCSwapLP.getInstance(chain, network); } else { throw new Error('unsupported chain or connector'); } diff --git a/test-bronze/connectors/etcSwap/etcSwap.routes.test.ts b/test-bronze/connectors/etcSwap/etcSwap.routes.test.ts new file mode 100644 index 0000000000..77711753c0 --- /dev/null +++ b/test-bronze/connectors/etcSwap/etcSwap.routes.test.ts @@ -0,0 +1,678 @@ +import express from 'express'; +import { Express } from 'express-serve-static-core'; +import request from 'supertest'; +import { AmmRoutes } from '../../../src/amm/amm.routes'; +import { patch, unpatch } from '../../../test/services/patch'; +import { gasCostInEthString } from '../../../src/services/base'; +import { ETCSwap } from '../../../src/connectors/etcswap/etcswap'; +import { EthereumClassicChain } from '../../../src/chains/ethereum-classic/ethereum-classic'; +import { patchEVMNonceManager } from '../../../test/evm.nonce.mock'; +let app: Express; +let ethereumclassic: EthereumClassicChain; +let etcSwap: ETCSwap; + +beforeAll(async () => { + app = express(); + app.use(express.json()); + + ethereumclassic = EthereumClassicChain.getInstance('mainnet'); + patchEVMNonceManager(ethereumclassic.nonceManager); + await ethereumclassic.init(); + + etcSwap = ETCSwap.getInstance('ethereum-classic', 'mainnet'); + await etcSwap.init(); + + app.use('/amm', AmmRoutes.router); +}); + +beforeEach(() => { + patchEVMNonceManager(ethereumclassic.nonceManager); +}); + +afterEach(() => { + unpatch(); +}); + +afterAll(async () => { + await ethereumclassic.close(); +}); + +const address: string = '0xFaA12FD102FE8623C9299c72B03E45107F2772B5'; + +const patchGetWallet = () => { + patch(ethereumclassic, 'getWallet', () => { + return { + address: '0xFaA12FD102FE8623C9299c72B03E45107F2772B5', + }; + }); +}; + +const patchInit = () => { + patch(etcSwap, 'init', async () => { + return; + }); +}; + +const patchStoredTokenList = () => { + patch(ethereumclassic, 'tokenList', () => { + return [ + { + chainId: 61, + name: 'WETC', + symbol: 'WETC', + address: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6', + decimals: 18, + }, + { + chainId: 61, + name: 'DAI', + symbol: 'DAI', + address: '0xdc31Ee1784292379Fbb2964b3B9C4124D8F89C60', + decimals: 18, + }, + ]; + }); +}; + +const patchGetTokenBySymbol = () => { + patch(ethereumclassic, 'getTokenBySymbol', (symbol: string) => { + if (symbol === 'WETC') { + return { + chainId: 61, + name: 'WETC', + symbol: 'WETC', + address: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6', + decimals: 18, + }; + } else { + return { + chainId: 61, + name: 'DAI', + symbol: 'DAI', + address: '0xdc31Ee1784292379Fbb2964b3B9C4124D8F89C60', + decimals: 18, + }; + } + }); +}; + +const patchGetTokenByAddress = () => { + patch(etcSwap, 'getTokenByAddress', () => { + return { + chainId: 61, + name: 'WETC', + symbol: 'WETC', + address: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6', + decimals: 18, + }; + }); +}; + +const patchGasPrice = () => { + patch(ethereumclassic, 'gasPrice', () => 100); +}; + +const patchEstimateBuyTrade = () => { + patch(etcSwap, 'estimateBuyTrade', () => { + return { + expectedAmount: { + toSignificant: () => 100, + }, + trade: { + executionPrice: { + invert: jest.fn().mockReturnValue({ + toSignificant: () => 100, + toFixed: () => '100', + }), + }, + }, + }; + }); +}; + +const patchEstimateSellTrade = () => { + patch(etcSwap, 'estimateSellTrade', () => { + return { + expectedAmount: { + toSignificant: () => 100, + }, + trade: { + executionPrice: { + toSignificant: () => 100, + toFixed: () => '100', + }, + }, + }; + }); +}; + +const patchGetNonce = () => { + patch(ethereumclassic.nonceManager, 'getNonce', () => 21); +}; + +const patchExecuteTrade = () => { + patch(etcSwap, 'executeTrade', () => { + return { nonce: 21, hash: '000000000000000' }; + }); +}; + +describe('POST /amm/price', () => { + it('should return 200 for BUY', async () => { + patchGetWallet(); + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patchGasPrice(); + patchEstimateBuyTrade(); + patchGetNonce(); + patchExecuteTrade(); + + await request(app) + .post(`/amm/price`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + quote: 'DAI', + base: 'WETC', + 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(); + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patchGasPrice(); + patchEstimateSellTrade(); + patchGetNonce(); + patchExecuteTrade(); + + await request(app) + .post(`/amm/price`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + quote: 'DAI', + base: 'WETC', + 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(); + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + + await request(app) + .post(`/amm/price`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + quote: 'DOGE', + base: 'WETC', + amount: '10000', + side: 'SELL', + }) + .set('Accept', 'application/json') + .expect(500); + }); + + it('should return 500 for unrecognized base symbol', async () => { + patchGetWallet(); + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + + await request(app) + .post(`/amm/price`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + quote: 'DAI', + base: 'SHIBA', + amount: '10000', + side: 'SELL', + }) + .set('Accept', 'application/json') + .expect(500); + }); + + it('should return 500 for unrecognized base symbol with decimals in the amount and SELL', async () => { + patchGetWallet(); + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + + await request(app) + .post(`/amm/price`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + quote: 'DAI', + base: 'SHIBA', + amount: '10.000', + side: 'SELL', + }) + .set('Accept', 'application/json') + .expect(500); + }); + + it('should return 500 for unrecognized base symbol with decimals in the amount and BUY', async () => { + patchGetWallet(); + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + + await request(app) + .post(`/amm/price`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + quote: 'DAI', + base: 'SHIBA', + amount: '10.000', + side: 'BUY', + }) + .set('Accept', 'application/json') + .expect(500); + }); + + it('should return 500 when the priceSwapIn operation fails', async () => { + patchGetWallet(); + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patch(etcSwap, 'priceSwapIn', () => { + return 'error'; + }); + + await request(app) + .post(`/amm/price`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + quote: 'DOGE', + base: 'WETC', + amount: '10000', + side: 'SELL', + }) + .set('Accept', 'application/json') + .expect(500); + }); + + it('should return 500 when the priceSwapOut operation fails', async () => { + patchGetWallet(); + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patch(etcSwap, 'priceSwapOut', () => { + return 'error'; + }); + + await request(app) + .post(`/amm/price`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + quote: 'DOGE', + base: 'WETC', + amount: '10000', + side: 'BUY', + }) + .set('Accept', 'application/json') + .expect(500); + }); +}); + +describe('POST /amm/trade', () => { + const patchForBuy = () => { + patchGetWallet(); + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patchGasPrice(); + patchEstimateBuyTrade(); + patchGetNonce(); + patchExecuteTrade(); + }; + it('should return 200 for BUY', async () => { + patchForBuy(); + await request(app) + .post(`/amm/trade`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + quote: 'DAI', + base: 'WETC', + 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(app) + .post(`/amm/trade`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + quote: 'DAI', + base: 'WETC', + 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(app) + .post(`/amm/trade`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + quote: 'DAI', + base: 'WETC', + amount: '10000', + address, + side: 'BUY', + nonce: 21, + maxFeePerGas: '5000000000', + maxPriorityFeePerGas: '5000000000', + }) + .set('Accept', 'application/json') + .expect(200); + }); + + const patchForSell = () => { + patchGetWallet(); + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patchGasPrice(); + patchEstimateSellTrade(); + patchGetNonce(); + patchExecuteTrade(); + }; + it('should return 200 for SELL', async () => { + patchForSell(); + await request(app) + .post(`/amm/trade`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + quote: 'DAI', + base: 'WETC', + 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(app) + .post(`/amm/trade`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + quote: 'DAI', + base: 'WETC', + amount: '10000', + address, + side: 'SELL', + nonce: 21, + maxFeePerGas: '5000000000', + maxPriorityFeePerGas: '5000000000', + }) + .set('Accept', 'application/json') + .expect(200); + }); + + it('should return 200 for SELL with limitPrice', async () => { + patchForSell(); + await request(app) + .post(`/amm/trade`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + quote: 'DAI', + base: 'WETC', + amount: '10000', + address, + side: 'SELL', + nonce: 21, + limitPrice: '9', + }) + .set('Accept', 'application/json') + .expect(200); + }); + + it('should return 200 for BUY with limitPrice', async () => { + patchForBuy(); + await request(app) + .post(`/amm/trade`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + quote: 'DAI', + base: 'WETC', + amount: '10000', + address, + side: 'BUY', + nonce: 21, + limitPrice: '999999999999999999999', + }) + .set('Accept', 'application/json') + .expect(200); + }); + + it('should return 500 for BUY with price smaller than limitPrice', async () => { + patchForBuy(); + await request(app) + .post(`/amm/trade`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + quote: 'DAI', + base: 'WETC', + amount: '10000', + address, + side: 'BUY', + nonce: 21, + limitPrice: '9', + }) + .set('Accept', 'application/json') + .expect(500); + }); + + it('should return 500 for SELL with price higher than limitPrice', async () => { + patchForSell(); + await request(app) + .post(`/amm/trade`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + quote: 'DAI', + base: 'WETC', + amount: '10000', + address, + side: 'SELL', + nonce: 21, + limitPrice: '99999999999', + }) + .set('Accept', 'application/json') + .expect(500); + }); + + it('should return 404 when parameters are incorrect', async () => { + patchInit(); + await request(app) + .post(`/amm/trade`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + quote: 'DAI', + base: 'WETC', + amount: 10000, + address: 'da8', + side: 'comprar', + }) + .set('Accept', 'application/json') + .expect(404); + }); + it('should return 500 when the priceSwapIn operation fails', async () => { + patchGetWallet(); + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patch(etcSwap, 'priceSwapIn', () => { + return 'error'; + }); + + await request(app) + .post(`/amm/trade`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + quote: 'DAI', + base: 'WETC', + amount: '10000', + address, + side: 'SELL', + nonce: 21, + maxFeePerGas: '5000000000', + maxPriorityFeePerGas: '5000000000', + }) + .set('Accept', 'application/json') + .expect(500); + }); + + it('should return 500 when the priceSwapOut operation fails', async () => { + patchGetWallet(); + patchInit(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patch(etcSwap, 'priceSwapOut', () => { + return 'error'; + }); + + await request(app) + .post(`/amm/trade`) + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + quote: 'DAI', + base: 'WETC', + amount: '10000', + address, + side: 'BUY', + nonce: 21, + maxFeePerGas: '5000000000', + maxPriorityFeePerGas: '5000000000', + }) + .set('Accept', 'application/json') + .expect(500); + }); +}); + +describe('POST /amm/estimateGas', () => { + it('should return 200 for valid connector', async () => { + patchInit(); + patchGasPrice(); + + await request(app) + .post('/amm/estimateGas') + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: 'etcswap', + }) + .set('Accept', 'application/json') + .expect(200) + .then((res: any) => { + expect(res.body.network).toEqual('mainnet'); + expect(res.body.gasPrice).toEqual(100); + expect(res.body.gasCost).toEqual( + gasCostInEthString(100, etcSwap.gasLimitEstimate), + ); + }); + }); + + it('should return 500 for invalid connector', async () => { + patchInit(); + patchGasPrice(); + + await request(app) + .post('/amm/estimateGas') + .send({ + chain: 'ethereum-classic', + network: 'mainnet', + connector: '-1', + }) + .set('Accept', 'application/json') + .expect(500); + }); +}); diff --git a/test-bronze/connectors/etcSwap/etcSwap.test.ts b/test-bronze/connectors/etcSwap/etcSwap.test.ts new file mode 100644 index 0000000000..0143b2f7f5 --- /dev/null +++ b/test-bronze/connectors/etcSwap/etcSwap.test.ts @@ -0,0 +1,306 @@ +jest.useFakeTimers(); +const { MockProvider } = require('mock-ethers-provider'); +import { patch, unpatch } from '../../../test/services/patch'; +import { UniswapishPriceError } from '../../../src/services/error-handler'; +import { + CurrencyAmount, + TradeType, + Token, +} from '@_etcswap/smart-order-router/node_modules/@uniswap/sdk-core'; +import { + Pair, + Route, +} from '@_etcswap/smart-order-router/node_modules/@uniswap/router-sdk/node_modules/@uniswap/v2-sdk'; +import { Trade } from '@_etcswap/smart-order-router/node_modules/@uniswap/router-sdk'; +import { BigNumber, constants, utils } from 'ethers'; +import { + FACTORY_ADDRESS, + TickMath, + encodeSqrtRatioX96, + Pool as EtcswapV3Pool, + FeeAmount, +} from '@_etcswap/smart-order-router/node_modules/@uniswap/v3-sdk'; +import { EthereumClassicChain } from '../../../src/chains/ethereum-classic/ethereum-classic'; +import { ETCSwap } from '../../../src/connectors/etcswap/etcswap'; +import { ETCSwapConfig } from '../../../src/connectors/etcswap/etcswap.config'; +import { patchEVMNonceManager } from '../../../test/evm.nonce.mock'; + +let ethereumclassic: EthereumClassicChain; +let etcSwap: ETCSwap; +let mockProvider: typeof MockProvider; + +const WETC = new Token( + 3, + '0xd0A1E359811322d97991E03f863a0C30C2cF029C', + 18, + 'WETC', +); + +const DAI = new Token( + 3, + '0x4f96fe3b7a6cf9725f59d353f723c1bdb64ca6aa', + 18, + 'DAI', +); + +const DAI_WETH_POOL_ADDRESS = '0xBEff876AC507446457C2A6bDA9F7021A97A8547f'; +const POOL_SQRT_RATIO_START = encodeSqrtRatioX96(100e6, 100e18); +const POOL_TICK_CURRENT = TickMath.getTickAtSqrtRatio(POOL_SQRT_RATIO_START); +const POOL_LIQUIDITY = 0; +const DAI_WETH_POOL = new EtcswapV3Pool( + WETC, + DAI, + FeeAmount.MEDIUM, + POOL_SQRT_RATIO_START, + POOL_LIQUIDITY, + POOL_TICK_CURRENT, +); + +beforeAll(async () => { + ethereumclassic = EthereumClassicChain.getInstance('mainnet'); + patchEVMNonceManager(ethereumclassic.nonceManager); + await ethereumclassic.init(); +}); + +beforeEach(() => { + patchEVMNonceManager(ethereumclassic.nonceManager); +}); + +afterEach(() => { + unpatch(); +}); + +afterAll(async () => { + await ethereumclassic.close(); +}); + +const patchTrade = (_key: string, error?: Error) => { + patch(etcSwap, '_alphaRouter', { + route() { + if (error) return false; + const WETH_DAI = new Pair( + CurrencyAmount.fromRawAmount(WETC, '2000000000000000000'), + CurrencyAmount.fromRawAmount(DAI, '1000000000000000000'), + ); + const DAI_TO_WETH = new Route([WETH_DAI], DAI, WETC); + return { + quote: CurrencyAmount.fromRawAmount(DAI, '1000000000000000000'), + quoteGasAdjusted: CurrencyAmount.fromRawAmount( + DAI, + '1000000000000000000', + ), + estimatedGasUsed: utils.parseEther('100'), + estimatedGasUsedQuoteToken: CurrencyAmount.fromRawAmount( + DAI, + '1000000000000000000', + ), + estimatedGasUsedUSD: CurrencyAmount.fromRawAmount( + DAI, + '1000000000000000000', + ), + gasPriceWei: utils.parseEther('100'), + trade: new Trade({ + v2Routes: [ + { + routev2: DAI_TO_WETH, + inputAmount: CurrencyAmount.fromRawAmount( + DAI, + '1000000000000000000', + ), + outputAmount: CurrencyAmount.fromRawAmount( + WETC, + '2000000000000000000', + ), + }, + ], + v3Routes: [], + tradeType: TradeType.EXACT_INPUT, + }), + route: [], + blockNumber: BigNumber.from(5000), + }; + }, + }); +}; + +const patchMockProvider = () => { + mockProvider.setMockContract( + FACTORY_ADDRESS, + require('@_etcswap/smart-order-router/node_modules/@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json') + .abi, + ); + mockProvider.stub(FACTORY_ADDRESS, 'getPool', DAI_WETH_POOL_ADDRESS); + + mockProvider.setMockContract( + ETCSwapConfig.config.quoterContractAddress('mainnet'), + // require('@uniswap/swap-router-contracts/artifacts/contracts/lens/QuoterV2.sol/QuoterV2.json') + require('@_etcswap/smart-order-router/node_modules/@uniswap/swap-router-contracts/artifacts/contracts/lens/QuoterV2.sol/QuoterV2.json') + .abi, + ); + mockProvider.stub( + ETCSwapConfig.config.quoterContractAddress('mainnet'), + 'quoteExactInputSingle', + /* amountOut */ 1, + /* sqrtPriceX96After */ 0, + /* initializedTicksCrossed */ 0, + /* gasEstimate */ 0, + ); + mockProvider.stub( + ETCSwapConfig.config.quoterContractAddress('mainnet'), + 'quoteExactOutputSingle', + /* amountIn */ 1, + /* sqrtPriceX96After */ 0, + /* initializedTicksCrossed */ 0, + /* gasEstimate */ 0, + ); + + mockProvider.setMockContract( + DAI_WETH_POOL_ADDRESS, + require('@_etcswap/smart-order-router/node_modules/@uniswap/v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json') + .abi, + ); + mockProvider.stub( + DAI_WETH_POOL_ADDRESS, + 'slot0', + DAI_WETH_POOL.sqrtRatioX96.toString(), + DAI_WETH_POOL.tickCurrent, + /* observationIndex */ 0, + /* observationCardinality */ 1, + /* observationCardinalityNext */ 1, + /* feeProtocol */ 0, + /* unlocked */ true, + ); + mockProvider.stub(DAI_WETH_POOL_ADDRESS, 'liquidity', 0); + mockProvider.stub(DAI_WETH_POOL_ADDRESS, 'fee', FeeAmount.LOW); + patch(ethereumclassic, 'provider', () => { + return mockProvider; + }); +}; + +const patchGetPool = (address: string | null) => { + mockProvider.setMockContract( + FACTORY_ADDRESS, + require('@_etcswap/smart-order-router/node_modules/@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json') + .abi, + ); + mockProvider.stub(FACTORY_ADDRESS, 'getPool', address); +}; + +const useRouter = async () => { + const config = ETCSwapConfig.config; + config.useRouter = true; + + patch(ETCSwap, '_instances', () => ({})); + etcSwap = ETCSwap.getInstance('ethereum-classic', 'mainnet'); + await etcSwap.init(); +}; + +const useQouter = async () => { + const config = ETCSwapConfig.config; + config.useRouter = false; + config.feeTier = 'MEDIUM'; + + patch(ETCSwap, '_instances', () => ({})); + etcSwap = ETCSwap.getInstance('ethereum-classic', 'mainnet'); + await etcSwap.init(); + + mockProvider = new MockProvider(); + patchMockProvider(); +}; + +describe('verify ETCSwap estimateSellTrade', () => { + describe('when using router', () => { + beforeAll(async () => { + await useRouter(); + }); + + it('Should return an ExpectedTrade when available', async () => { + patchTrade('bestTradeExactIn'); + + const expectedTrade = await etcSwap.estimateSellTrade( + WETC, + DAI, + 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 etcSwap.estimateSellTrade(WETC, DAI, BigNumber.from(1)); + }).rejects.toThrow(UniswapishPriceError); + }); + }); + + describe('when using qouter', () => { + beforeEach(async () => { + await useQouter(); + }); + + // it('Should return an ExpectedTrade when available', async () => { + // patchGetPool(DAI_WETH_POOL_ADDRESS); + + // const expectedTrade = await etcSwap.estimateSellTrade( + // WETC, + // DAI, + // BigNumber.from(1) + // ); + + // expect(expectedTrade).toHaveProperty('trade'); + // expect(expectedTrade).toHaveProperty('expectedAmount'); + // }); + + it('Should throw an error if no pair is available', async () => { + patchGetPool(constants.AddressZero); + + await expect(async () => { + await etcSwap.estimateSellTrade(WETC, DAI, BigNumber.from(1)); + }).rejects.toThrow(Error); + }); + }); +}); + +describe('verify ETCSwap estimateBuyTrade', () => { + describe('when using router', () => { + beforeAll(async () => { + await useRouter(); + }); + + it('Should return an ExpectedTrade when available', async () => { + patchTrade('bestTradeExactOut'); + + const expectedTrade = await etcSwap.estimateBuyTrade( + WETC, + DAI, + 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 etcSwap.estimateBuyTrade(WETC, DAI, BigNumber.from(1)); + }).rejects.toThrow(UniswapishPriceError); + }); + }); + + describe('when using qouter', () => { + beforeEach(async () => { + await useQouter(); + }); + + it('Should throw an error if no pair is available', async () => { + patchGetPool(constants.AddressZero); + + await expect(async () => { + await etcSwap.estimateBuyTrade(WETC, DAI, BigNumber.from(1)); + }).rejects.toThrow(Error); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 1a30d80c8f..928c9dcf46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@_etcswap/smart-order-router@^3.15.0": - version "3.15.0" - resolved "https://registry.yarnpkg.com/@_etcswap/smart-order-router/-/smart-order-router-3.15.0.tgz#896ca0c5c1231be0df126e1b4bc5c4c1d9a003e7" - integrity sha512-2PcPSINeZM3vhkyr/r8JnCmEjjpdf38qt7Rcl5PgspsnFj1ndQUNsm1QsruQFEOW8eEn7PIL0ILBmPDW+8rviQ== +"@_etcswap/smart-order-router@^3.15.2": + version "3.15.2" + resolved "https://registry.yarnpkg.com/@_etcswap/smart-order-router/-/smart-order-router-3.15.2.tgz#989653df5d98158aaa0971a6493ea9a4efc16a3e" + integrity sha512-/XlbjLeWtQEg4HfvebPU5CWbGbq2e9y19YQY2BXtKsjyHli1YNx4yvR3kawFZPhePgENdBYp7l8NFrq2Ovjbcw== dependencies: "@uniswap/default-token-list" "^11.2.0" "@uniswap/permit2-sdk" "^1.2.0" @@ -15,7 +15,7 @@ "@uniswap/token-lists" "^1.0.0-beta.31" "@uniswap/universal-router" "^1.0.1" "@uniswap/universal-router-sdk" "git+https://github.com/etcswap/universal-router-sdk.git#etcswap" - "@uniswap/v2-sdk" "^3.2.0" + "@uniswap/v2-sdk" "git+https://github.com/etcswap/v2-sdk.git#etcswap" "@uniswap/v3-sdk" "git+https://github.com/etcswap/v3-sdk.git#etcswap" async-retry "^1.3.1" await-timeout "^1.1.1" @@ -30,20 +30,6 @@ node-cache "^5.1.2" stats-lite "^2.2.0" -"@_etcswap/v3-sdk@^3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@_etcswap/v3-sdk/-/v3-sdk-3.10.1.tgz#d1ee236c26c10dc2b368f8f882711f657ce516b2" - integrity sha512-hfscgXCJXUjyu8IrFIXIemozlI/EAKVHtHZVshOAVXszeEsz+OkP3pqO0rQ3WMuO9SWG/9WtRHjYMeOWa2AVzg== - dependencies: - "@ethersproject/abi" "^5.0.12" - "@ethersproject/solidity" "^5.0.9" - "@uniswap/sdk-core" "git+https://github.com/etcswap/sdk-core.git#etcswap" - "@uniswap/swap-router-contracts" "git+https://github.com/etcswap/swap-router-contracts.git#etcswap" - "@uniswap/v3-periphery" "git+https://github.com/etcswap/v3-periphery.git#etcswap" - "@uniswap/v3-staker" "git+https://github.com/etcswap/v3-staker.git#etcswap" - tiny-invariant "^1.1.0" - tiny-warning "^1.0.3" - "@aashutoshrathi/word-wrap@^1.2.3": version "1.2.6" resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" @@ -5091,16 +5077,17 @@ ethers "^5.7.0" tiny-invariant "^1.1.0" -"@uniswap/router-sdk@^1.9.2", "@uniswap/router-sdk@^1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@uniswap/router-sdk/-/router-sdk-1.9.3.tgz#0721d1d5eadf220632b062ec34044eadababdd6c" - integrity sha512-vKhYDN+Ne8XLFay97pW3FyMJbmbS4eiQfiTVpv7EblDKUYG2Co0OSaH+kPAuXcvHvcflbyBpp94NCyePjlVltw== +"@uniswap/router-sdk@^1.10.0": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@uniswap/router-sdk/-/router-sdk-1.11.1.tgz#bf3553e10021c2a6d9c87cbd8e3324d0cee52822" + integrity sha512-Nz0h5YUhEUusZ06C8nWIjQssvwche1dxtVY7EEx9eplgQqB9flGHJ8hxqnpEx+h1GB78bo7Qt/aFIiEVL28R8Q== dependencies: "@ethersproject/abi" "^5.5.0" "@uniswap/sdk-core" "^5.3.1" "@uniswap/swap-router-contracts" "^1.3.0" "@uniswap/v2-sdk" "^4.3.2" "@uniswap/v3-sdk" "^3.11.2" + "@uniswap/v4-sdk" "^1.0.0" "@uniswap/router-sdk@git+https://github.com/etcswap/router-sdk.git#etcswap": version "1.6.0" @@ -5124,7 +5111,7 @@ tiny-invariant "^1.1.0" toformat "^2.0.0" -"@uniswap/sdk-core@^5.3.0", "@uniswap/sdk-core@^5.3.1": +"@uniswap/sdk-core@^5.0.0", "@uniswap/sdk-core@^5.3.0", "@uniswap/sdk-core@^5.3.1": version "5.3.1" resolved "https://registry.yarnpkg.com/@uniswap/sdk-core/-/sdk-core-5.3.1.tgz#22d753e9ef8666c2f3b4d9a89b9658d169be4ce8" integrity sha512-XLJY8PcMZnKYBGLABJnLXcr3EgWql3mmnmpHyV1/MmEh9pLJLHYz4HLwVHb8pGDCqpOFX0e+Ei44/qhC7Q5Dsg== @@ -5163,23 +5150,24 @@ tiny-warning "^1.0.3" toformat "^2.0.0" -"@uniswap/smart-order-router@^3.39.0": - version "3.39.0" - resolved "https://registry.yarnpkg.com/@uniswap/smart-order-router/-/smart-order-router-3.39.0.tgz#83fe53e66d3bb9442f1e7049100381298655f129" - integrity sha512-6PHMeJvXp7lpJvX4rE66ofHIJa/OB0s+TSQ802qu7cljj7E0SRDG/QAi3WBXIX3QlTyn1pp4Yvkqk7crtMRkgw== +"@uniswap/smart-order-router@^3.46.1": + version "3.46.2" + resolved "https://registry.yarnpkg.com/@uniswap/smart-order-router/-/smart-order-router-3.46.2.tgz#25923a8652d62cc13b871754c4eba53e1997863d" + integrity sha512-4UoQ3r6dlVaZc+RQzRBk9smzeUqqAbUUO8IafrW0wlZGNazhvZw/yJS0STu5exheDZ+YNwqewnmUAukKG2XB1Q== dependencies: "@eth-optimism/sdk" "^3.2.2" "@types/brotli" "^1.3.4" "@uniswap/default-token-list" "^11.13.0" "@uniswap/permit2-sdk" "^1.3.0" - "@uniswap/router-sdk" "^1.9.2" + "@uniswap/router-sdk" "^1.10.0" "@uniswap/sdk-core" "^5.3.0" "@uniswap/swap-router-contracts" "^1.3.1" "@uniswap/token-lists" "^1.0.0-beta.31" "@uniswap/universal-router" "^1.6.0" - "@uniswap/universal-router-sdk" "^2.2.0" + "@uniswap/universal-router-sdk" "^2.2.4" "@uniswap/v2-sdk" "^4.3.2" "@uniswap/v3-sdk" "^3.13.0" + "@uniswap/v4-sdk" "^1.0.0" async-retry "^1.3.1" await-timeout "^1.1.1" axios "^0.21.1" @@ -5233,17 +5221,18 @@ resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.34.tgz#879461f5d4009327a24259bbab797e0f22db58c8" integrity sha512-Hc3TfrFaupg0M84e/Zv7BoF+fmMWDV15mZ5s8ZQt2qZxUcNw2GQW+L6L/2k74who31G+p1m3GRYbJpAo7d1pqA== -"@uniswap/universal-router-sdk@^2.2.0": - version "2.2.2" - resolved "https://registry.yarnpkg.com/@uniswap/universal-router-sdk/-/universal-router-sdk-2.2.2.tgz#1199b9bf492f496a17175c7aaadbb92c67845790" - integrity sha512-RYW2d+NlAjZJ1ZpJTPTXGgGlyBHnXShNbRkz5ueP3m0CzRAS+1P9Czub1SO8ZgcbZ/y4Po/SW9JXT/j3gnI/XA== +"@uniswap/universal-router-sdk@^2.2.4": + version "2.2.4" + resolved "https://registry.yarnpkg.com/@uniswap/universal-router-sdk/-/universal-router-sdk-2.2.4.tgz#28a7520791d991e5f12a46310f81bb09faac3433" + integrity sha512-6+ErgDDtCJLM2ro/krCKtu6ucUpcaQEEPRrAPuJiMTWbR0UyR+6Otp+KdBcT9LmyzSoXuSHhIRr+6s25no1J6A== dependencies: "@uniswap/permit2-sdk" "^1.3.0" - "@uniswap/router-sdk" "^1.9.3" + "@uniswap/router-sdk" "^1.10.0" "@uniswap/sdk-core" "^5.3.1" "@uniswap/universal-router" "1.6.0" "@uniswap/v2-sdk" "^4.4.1" "@uniswap/v3-sdk" "^3.13.1" + "@uniswap/v4-sdk" "^1.0.0" bignumber.js "^9.0.2" ethers "^5.7.0" @@ -5305,6 +5294,16 @@ tiny-invariant "^1.1.0" tiny-warning "^1.0.3" +"@uniswap/v2-sdk@git+https://github.com/etcswap/v2-sdk.git#etcswap": + version "4.2.2" + resolved "git+https://github.com/etcswap/v2-sdk.git#f466988787bae2f04a2daa6792df4ef75a90bbab" + dependencies: + "@ethersproject/address" "^5.0.0" + "@ethersproject/solidity" "^5.0.0" + "@uniswap/sdk-core" "git+https://github.com/etcswap/sdk-core.git#etcswap" + tiny-invariant "^1.1.0" + tiny-warning "^1.0.3" + "@uniswap/v3-core@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@uniswap/v3-core/-/v3-core-1.0.0.tgz#6c24adacc4c25dceee0ba3ca142b35adbd7e359d" @@ -5347,6 +5346,20 @@ "@uniswap/v3-core" "git+https://github.com/etcswap/v3-core.git#etcswap" base64-sol "1.0.1" +"@uniswap/v3-sdk@3.12.0": + version "3.12.0" + resolved "https://registry.yarnpkg.com/@uniswap/v3-sdk/-/v3-sdk-3.12.0.tgz#2d819aa777578b747c880e0bc86a9718354140c5" + integrity sha512-mUCg9HLKl20h6W8+QtELqN/uaO47/KDSf+EOht+W3C6jt2eGuzSANqS2CY7i8MsAsnZ+MjPhmN+JTOIvf7azfA== + dependencies: + "@ethersproject/abi" "^5.5.0" + "@ethersproject/solidity" "^5.0.9" + "@uniswap/sdk-core" "^5.0.0" + "@uniswap/swap-router-contracts" "^1.3.0" + "@uniswap/v3-periphery" "^1.1.1" + "@uniswap/v3-staker" "1.0.0" + tiny-invariant "^1.1.0" + tiny-warning "^1.0.3" + "@uniswap/v3-sdk@^3.11.2", "@uniswap/v3-sdk@^3.13.0", "@uniswap/v3-sdk@^3.13.1": version "3.13.1" resolved "https://registry.yarnpkg.com/@uniswap/v3-sdk/-/v3-sdk-3.13.1.tgz#67421727b18bb9c449bdf3c92cf3d01530ff3f8f" @@ -5391,6 +5404,17 @@ "@uniswap/v3-core" "git+https://github.com/etcswap/v3-core.git#etcswap" "@uniswap/v3-periphery" "git+https://github.com/etcswap/v3-periphery.git#etcswap" +"@uniswap/v4-sdk@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@uniswap/v4-sdk/-/v4-sdk-1.0.0.tgz#0ea85dd48dec7d83eaa4ff96a347364f6b731317" + integrity sha512-zC4cfOY9pFA6PUOARvmkAndOR0r5yiAwwcaFBxOoZe2kXLoh5wGH3svDZCQ4ZLpiPOevUPl+NXXC/KCEErbw2g== + dependencies: + "@ethersproject/solidity" "^5.0.9" + "@uniswap/sdk-core" "^5.3.1" + "@uniswap/v3-sdk" "3.12.0" + tiny-invariant "^1.1.0" + tiny-warning "^1.0.3" + "@vespaiach/axios-fetch-adapter@github:ecadlabs/axios-fetch-adapter": version "0.3.1" resolved "https://codeload.github.com/ecadlabs/axios-fetch-adapter/tar.gz/167684f522e90343b9f3439d9a43ac571e2396f6" From fb11f329492b62f794d57f8d5ae53d283b65b9ce Mon Sep 17 00:00:00 2001 From: vic-en Date: Sun, 6 Oct 2024 21:56:59 -0500 Subject: [PATCH 13/13] update token source --- src/templates/ethereum-classic.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/templates/ethereum-classic.yml b/src/templates/ethereum-classic.yml index 98ca4083a0..1f4f299b24 100644 --- a/src/templates/ethereum-classic.yml +++ b/src/templates/ethereum-classic.yml @@ -2,8 +2,8 @@ networks: mainnet: chainID: 61 nodeURL: 'https://etc.rivet.link' - tokenListType: FILE - tokenListSource: /home/gateway/conf/lists/ethereum-classic.json + tokenListType: URL + tokenListSource: https://raw.githubusercontent.com/etcswap/tokens/refs/heads/main/ethereum-classic/all.json nativeCurrencySymbol: 'ETC' gasPriceRefreshInterval: 60