From 482d10ba7b8df8f45159150916d0907673d15441 Mon Sep 17 00:00:00 2001 From: Nikolai Evseev <65676073+evseevnn@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:22:41 +0100 Subject: [PATCH] TELOS integration --- src/chains/telos/telos.ts | 125 ++++ src/chains/telos/telos.validator.ts | 36 + src/connectors/openocean/openocean.config.ts | 7 +- src/connectors/openocean/openocean.ts | 63 +- src/network/network.controllers.ts | 38 +- src/services/connection-manager.ts | 3 + src/services/wallet/wallet.validators.ts | 48 +- src/templates/lists/telos_evm_tokens.json | 202 +++++ src/templates/openocean.yml | 3 + src/templates/root.yml | 4 + src/templates/telos.yml | 10 + .../openocean/telos.openocean.routes.test.ts | 705 ++++++++++++++++++ 12 files changed, 1174 insertions(+), 70 deletions(-) create mode 100644 src/chains/telos/telos.ts create mode 100644 src/chains/telos/telos.validator.ts create mode 100644 src/templates/lists/telos_evm_tokens.json create mode 100644 src/templates/telos.yml create mode 100644 test-bronze/connectors/openocean/telos.openocean.routes.test.ts diff --git a/src/chains/telos/telos.ts b/src/chains/telos/telos.ts new file mode 100644 index 0000000000..6999242bec --- /dev/null +++ b/src/chains/telos/telos.ts @@ -0,0 +1,125 @@ +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 getTelosConfig } from '../ethereum/ethereum.config'; +import { Provider } from '@ethersproject/abstract-provider'; +import { OpenoceanConfig } from '../../connectors/openocean/openocean.config'; +import { Ethereumish } from '../../services/common-interfaces'; +import { ConfigManagerV2 } from '../../services/config-manager-v2'; +import { EVMController } from '../ethereum/evm.controllers'; + +export class Telos extends EthereumBase implements Ethereumish { + private static _instances: { [name: string]: Telos }; + private _gasPrice: number; + private _gasPriceRefreshInterval: number | null; + private _nativeTokenSymbol: string; + private _chain: string; + public controller; + + private constructor(network: string) { + const config = getTelosConfig('telos', network); + super( + 'telos', + 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): Telos { + if (Telos._instances === undefined) { + Telos._instances = {}; + } + if (!(network in Telos._instances)) { + Telos._instances[network] = new Telos(network); + } + + return Telos._instances[network]; + } + + public static getConnectedInstances(): { [name: string]: Telos } { + return Telos._instances; + } + + // 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 === 'openocean') { + spender = OpenoceanConfig.config.routerAddress('telos', 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); + } + + /** + * Automatically update the prevailing gas price on the network. + */ + async updateGasPrice(): Promise { + if (this._gasPriceRefreshInterval === null) { + return; + } + + const gasPrice = await this.getGasPrice(); + if (gasPrice !== null) { + this._gasPrice = gasPrice; + } else { + logger.info('gasPrice is unexpectedly null.'); + } + + setTimeout( + this.updateGasPrice.bind(this), + this._gasPriceRefreshInterval * 1000, + ); + } + + async close() { + await super.close(); + if (this._chain in Telos._instances) { + delete Telos._instances[this._chain]; + } + } +} diff --git a/src/chains/telos/telos.validator.ts b/src/chains/telos/telos.validator.ts new file mode 100644 index 0000000000..be98de35ef --- /dev/null +++ b/src/chains/telos/telos.validator.ts @@ -0,0 +1,36 @@ +import { + mkRequestValidator, + mkValidator, + RequestValidator, + Validator, + validateAmount, + validateToken, + validateTokenSymbols, +} from '../../services/validators'; +import { + isAddress, + validateNonce, + validateAddress, +} from '../ethereum/ethereum.validators'; + +export const invalidSpenderError: string = + 'The spender param is not a valid Telos address (0x followed by 40 hexidecimal characters).'; + +// given a request, look for a key called spender that is 'uniswap' or an Ethereum address +export const validateSpender: Validator = mkValidator( + 'spender', + invalidSpenderError, + (val) => typeof val === 'string' && (val === 'openocean' || isAddress(val)), +); + +export const validateAvalancheApproveRequest: RequestValidator = + mkRequestValidator([ + validateAddress, + validateSpender, + validateToken, + validateAmount, + validateNonce, + ]); + +export const validateAvalancheAllowancesRequest: RequestValidator = + mkRequestValidator([validateAddress, validateSpender, validateTokenSymbols]); diff --git a/src/connectors/openocean/openocean.config.ts b/src/connectors/openocean/openocean.config.ts index ef562b04ea..3f65e207c3 100644 --- a/src/connectors/openocean/openocean.config.ts +++ b/src/connectors/openocean/openocean.config.ts @@ -14,10 +14,10 @@ export namespace OpenoceanConfig { export const config: NetworkConfig = { allowedSlippage: ConfigManagerV2.getInstance().get( - 'openocean.allowedSlippage' + 'openocean.allowedSlippage', ), gasLimitEstimate: ConfigManagerV2.getInstance().get( - `openocean.gasLimitEstimate` + `openocean.gasLimitEstimate`, ), ttl: ConfigManagerV2.getInstance().get('openocean.ttl'), routerAddress: (chain: string, network: string) => @@ -26,7 +26,7 @@ export namespace OpenoceanConfig { chain + '.' + network + - '.routerAddress' + '.routerAddress', ), tradingTypes: ['AMM'], chainType: 'EVM', @@ -37,6 +37,7 @@ export namespace OpenoceanConfig { { chain: 'harmony', networks: ['mainnet'] }, { chain: 'binance-smart-chain', networks: ['mainnet'] }, { chain: 'cronos', networks: ['mainnet'] }, + { chain: 'telos', networks: ['evm'] }, ], }; } diff --git a/src/connectors/openocean/openocean.ts b/src/connectors/openocean/openocean.ts index 493f9c5e2a..e62de69b49 100644 --- a/src/connectors/openocean/openocean.ts +++ b/src/connectors/openocean/openocean.ts @@ -19,6 +19,7 @@ import { Polygon } from '../../chains/polygon/polygon'; import { Harmony } from '../../chains/harmony/harmony'; import { BinanceSmartChain } from '../../chains/binance-smart-chain/binance-smart-chain'; import { Cronos } from '../../chains/cronos/cronos'; +import { Telos } from '../../chains/telos/telos'; import { ExpectedTrade, Uniswapish } from '../../services/common-interfaces'; import { HttpException, @@ -34,7 +35,7 @@ export function newFakeTrade( tokenIn: Token, tokenOut: Token, tokenInAmount: BigNumber, - tokenOutAmount: BigNumber + tokenOutAmount: BigNumber, ): Trade { const baseAmount = new TokenAmount(tokenIn, tokenInAmount.toString()); const quoteAmount = new TokenAmount(tokenOut, tokenOutAmount.toString()); @@ -47,7 +48,7 @@ export function newFakeTrade( tokenIn, tokenOut, tokenInAmount.toBigInt(), - tokenOutAmount.toBigInt() + tokenOutAmount.toBigInt(), ); return trade; } @@ -99,6 +100,8 @@ export class Openocean implements Uniswapish { return BinanceSmartChain.getInstance(network); } else if (this._chain === 'cronos') { return Cronos.getInstance(network); + } else if (this._chain === 'telos') { + return Telos.getInstance(network); } else { throw new Error('unsupported chain'); } @@ -124,7 +127,7 @@ export class Openocean implements Uniswapish { token.address, token.decimals, token.symbol, - token.name + token.name, ); } this._ready = true; @@ -189,7 +192,7 @@ export class Openocean implements Uniswapish { const nd = allowedSlippage.match(percentRegexp); if (nd) return Number(nd[1]); 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.', ); } @@ -206,10 +209,10 @@ export class Openocean implements Uniswapish { async estimateSellTrade( baseToken: Token, quoteToken: Token, - amount: BigNumber + amount: BigNumber, ): Promise { logger.info( - `estimateSellTrade getting amounts out baseToken(${baseToken.symbol}): ${baseToken.address} - quoteToken(${quoteToken.symbol}): ${quoteToken.address}.` + `estimateSellTrade getting amounts out baseToken(${baseToken.symbol}): ${baseToken.address} - quoteToken(${quoteToken.symbol}): ${quoteToken.address}.`, ); const reqAmount = new Decimal(amount.toString()) @@ -228,7 +231,7 @@ export class Openocean implements Uniswapish { amount: reqAmount, gasPrice: gasPrice, }, - } + }, ); } catch (e) { if (e instanceof Error) { @@ -236,14 +239,14 @@ export class Openocean implements Uniswapish { throw new HttpException( 500, TRADE_FAILED_ERROR_MESSAGE + e.message, - TRADE_FAILED_ERROR_CODE + TRADE_FAILED_ERROR_CODE, ); } else { logger.error('Unknown error trying to get trade info.'); throw new HttpException( 500, UNKNOWN_ERROR_MESSAGE, - UNKNOWN_ERROR_ERROR_CODE + UNKNOWN_ERROR_ERROR_CODE, ); } } @@ -255,30 +258,30 @@ export class Openocean implements Uniswapish { ) { const quoteData = quoteRes.data.data; logger.info( - `estimateSellTrade quoteData inAmount(${baseToken.symbol}): ${quoteData.inAmount}, outAmount(${quoteToken.symbol}): ${quoteData.outAmount}` + `estimateSellTrade quoteData inAmount(${baseToken.symbol}): ${quoteData.inAmount}, outAmount(${quoteToken.symbol}): ${quoteData.outAmount}`, ); const amounts = [quoteData.inAmount, quoteData.outAmount]; const maximumOutput = new TokenAmount( quoteToken, - amounts[1].toString() + amounts[1].toString(), ); const trade = newFakeTrade( baseToken, quoteToken, BigNumber.from(amounts[0]), - BigNumber.from(amounts[1]) + BigNumber.from(amounts[1]), ); return { trade: trade, expectedAmount: maximumOutput }; } else { 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}.`, ); } } throw new HttpException( quoteRes.status, `Could not get trade info. ${quoteRes.statusText}`, - TRADE_FAILED_ERROR_CODE + TRADE_FAILED_ERROR_CODE, ); } @@ -295,10 +298,10 @@ export class Openocean implements Uniswapish { async estimateBuyTrade( quoteToken: Token, baseToken: Token, - amount: BigNumber + amount: BigNumber, ): Promise { logger.info( - `estimateBuyTrade getting amounts in quoteToken(${quoteToken.symbol}): ${quoteToken.address} - baseToken(${baseToken.symbol}): ${baseToken.address}.` + `estimateBuyTrade getting amounts in quoteToken(${quoteToken.symbol}): ${quoteToken.address} - baseToken(${baseToken.symbol}): ${baseToken.address}.`, ); const reqAmount = new Decimal(amount.toString()) @@ -317,7 +320,7 @@ export class Openocean implements Uniswapish { amount: reqAmount, gasPrice: gasPrice, }, - } + }, ); } catch (e) { if (e instanceof Error) { @@ -325,14 +328,14 @@ export class Openocean implements Uniswapish { throw new HttpException( 500, TRADE_FAILED_ERROR_MESSAGE + e.message, - TRADE_FAILED_ERROR_CODE + TRADE_FAILED_ERROR_CODE, ); } else { logger.error('Unknown error trying to get trade info.'); throw new HttpException( 500, UNKNOWN_ERROR_MESSAGE, - UNKNOWN_ERROR_ERROR_CODE + UNKNOWN_ERROR_ERROR_CODE, ); } } @@ -343,7 +346,7 @@ export class Openocean implements Uniswapish { ) { const quoteData = quoteRes.data.data; logger.info( - `estimateBuyTrade reverseData inAmount(${quoteToken.symbol}): ${quoteData.reverseAmount}, outAmount(${baseToken.symbol}): ${quoteData.inAmount}` + `estimateBuyTrade reverseData inAmount(${quoteToken.symbol}): ${quoteData.reverseAmount}, outAmount(${baseToken.symbol}): ${quoteData.inAmount}`, ); const amounts = [quoteData.reverseAmount, quoteData.inAmount]; const minimumInput = new TokenAmount(quoteToken, amounts[0].toString()); @@ -351,19 +354,19 @@ export class Openocean implements Uniswapish { quoteToken, baseToken, BigNumber.from(amounts[0]), - BigNumber.from(amounts[1]) + BigNumber.from(amounts[1]), ); return { trade: trade, expectedAmount: minimumInput }; } else { throw new UniswapishPriceError( - `priceSwapIn: no trade pair found for ${baseToken} to ${quoteToken}.` + `priceSwapIn: no trade pair found for ${baseToken} to ${quoteToken}.`, ); } } throw new HttpException( quoteRes.status, `Could not get trade info. ${quoteRes.statusText}`, - TRADE_FAILED_ERROR_CODE + TRADE_FAILED_ERROR_CODE, ); } @@ -391,10 +394,10 @@ export class Openocean implements Uniswapish { gasLimit: number, nonce?: number, maxFeePerGas?: BigNumber, - maxPriorityFeePerGas?: BigNumber + maxPriorityFeePerGas?: BigNumber, ): Promise { logger.info( - `executeTrade ${openoceanRouter}-${ttl}-${abi}-${gasPrice}-${gasLimit}-${nonce}-${maxFeePerGas}-${maxPriorityFeePerGas}.` + `executeTrade ${openoceanRouter}-${ttl}-${abi}-${gasPrice}-${gasLimit}-${nonce}-${maxFeePerGas}-${maxPriorityFeePerGas}.`, ); const inToken: any = trade.route.input; const outToken: any = trade.route.output; @@ -412,7 +415,7 @@ export class Openocean implements Uniswapish { gasPrice: gasPrice.toString(), referrer: '0x3fb06064b88a65ba9b9eb840dbb5f3789f002642', }, - } + }, ); } catch (e) { if (e instanceof Error) { @@ -420,14 +423,14 @@ export class Openocean implements Uniswapish { throw new HttpException( 500, TRADE_FAILED_ERROR_MESSAGE + e.message, - TRADE_FAILED_ERROR_CODE + TRADE_FAILED_ERROR_CODE, ); } else { logger.error('Unknown error trying to get trade info.'); throw new HttpException( 500, UNKNOWN_ERROR_MESSAGE, - UNKNOWN_ERROR_ERROR_CODE + UNKNOWN_ERROR_ERROR_CODE, ); } } @@ -451,13 +454,13 @@ export class Openocean implements Uniswapish { logger.info(JSON.stringify(tx)); return tx; - } + }, ); } throw new HttpException( swapRes.status, `Could not get trade info. ${swapRes.statusText}`, - TRADE_FAILED_ERROR_CODE + TRADE_FAILED_ERROR_CODE, ); } } diff --git a/src/network/network.controllers.ts b/src/network/network.controllers.ts index bb4b165158..fb16e1a43b 100644 --- a/src/network/network.controllers.ts +++ b/src/network/network.controllers.ts @@ -7,6 +7,7 @@ import { Polygon } from '../chains/polygon/polygon'; import { Xdc } from '../chains/xdc/xdc'; import { Tezos } from '../chains/tezos/tezos'; import { Kujira } from '../chains/kujira/kujira'; +import { Telos } from '../chains/telos/telos'; import { HttpException, UNKNOWN_CHAIN_ERROR_CODE, @@ -23,7 +24,7 @@ import { Osmosis } from '../chains/osmosis/osmosis'; import { XRPL } from '../chains/xrpl/xrpl'; export async function getStatus( - req: StatusRequest + req: StatusRequest, ): Promise { const statuses: StatusResponse[] = []; let connections: any[] = []; @@ -37,14 +38,14 @@ export async function getStatus( if (req.chain) { try { connections.push( - await getInitializedChain(req.chain, req.network as string) + await getInitializedChain(req.chain, req.network as string), ); } catch (e) { if (e instanceof UnsupportedChainException) { throw new HttpException( 500, UNKNOWN_KNOWN_CHAIN_ERROR_MESSAGE(req.chain), - UNKNOWN_CHAIN_ERROR_CODE + UNKNOWN_CHAIN_ERROR_CODE, ); } throw e; @@ -52,67 +53,72 @@ export async function getStatus( } else { const algorandConnections = Algorand.getConnectedInstances(); connections = connections.concat( - algorandConnections ? Object.values(algorandConnections) : [] + algorandConnections ? Object.values(algorandConnections) : [], ); const avalancheConnections = Avalanche.getConnectedInstances(); connections = connections.concat( - avalancheConnections ? Object.values(avalancheConnections) : [] + avalancheConnections ? Object.values(avalancheConnections) : [], ); const harmonyConnections = Harmony.getConnectedInstances(); connections = connections.concat( - harmonyConnections ? Object.values(harmonyConnections) : [] + harmonyConnections ? Object.values(harmonyConnections) : [], ); const ethereumConnections = Ethereum.getConnectedInstances(); connections = connections.concat( - ethereumConnections ? Object.values(ethereumConnections) : [] + ethereumConnections ? Object.values(ethereumConnections) : [], ); const polygonConnections = Polygon.getConnectedInstances(); connections = connections.concat( - polygonConnections ? Object.values(polygonConnections) : [] + polygonConnections ? Object.values(polygonConnections) : [], ); const xdcConnections = Xdc.getConnectedInstances(); connections = connections.concat( - xdcConnections ? Object.values(xdcConnections) : [] + xdcConnections ? Object.values(xdcConnections) : [], ); const cronosConnections = Cronos.getConnectedInstances(); connections = connections.concat( - cronosConnections ? Object.values(cronosConnections) : [] + cronosConnections ? Object.values(cronosConnections) : [], ); const nearConnections = Near.getConnectedInstances(); connections = connections.concat( - nearConnections ? Object.values(nearConnections) : [] + nearConnections ? Object.values(nearConnections) : [], ); const bscConnections = BinanceSmartChain.getConnectedInstances(); connections = connections.concat( - bscConnections ? Object.values(bscConnections) : [] + bscConnections ? Object.values(bscConnections) : [], ); const tezosConnections = Tezos.getConnectedInstances(); connections = connections.concat( - tezosConnections ? Object.values(tezosConnections) : [] + tezosConnections ? Object.values(tezosConnections) : [], ); const xrplConnections = XRPL.getConnectedInstances(); connections = connections.concat( - xrplConnections ? Object.values(xrplConnections) : [] + xrplConnections ? Object.values(xrplConnections) : [], ); const kujiraConnections = Kujira.getConnectedInstances(); connections = connections.concat( - kujiraConnections ? Object.values(kujiraConnections) : [] + kujiraConnections ? Object.values(kujiraConnections) : [], + ); + + const telosConnections = Telos.getConnectedInstances(); + connections = connections.concat( + telosConnections ? Object.values(telosConnections) : [], ); const osmosisConnections = Osmosis.getConnectedInstances(); connections = connections.concat( - osmosisConnections ? Object.values(osmosisConnections) : [] + osmosisConnections ? Object.values(osmosisConnections) : [], ); } diff --git a/src/services/connection-manager.ts b/src/services/connection-manager.ts index 456d1eeedf..dc356026bc 100644 --- a/src/services/connection-manager.ts +++ b/src/services/connection-manager.ts @@ -6,6 +6,7 @@ import { Harmony } from '../chains/harmony/harmony'; import { Polygon } from '../chains/polygon/polygon'; import { Xdc } from '../chains/xdc/xdc'; import { Tezos } from '../chains/tezos/tezos'; +import { Telos } from '../chains/telos/telos'; import { Osmosis } from '../chains/osmosis/osmosis'; import { XRPL, XRPLish } from '../chains/xrpl/xrpl'; import { MadMeerkat } from '../connectors/mad_meerkat/mad_meerkat'; @@ -141,6 +142,8 @@ export async function getChainInstance( connection = XRPL.getInstance(network); } else if (chain === 'kujira') { connection = Kujira.getInstance(network); + } else if (chain === 'telos') { + connection = Telos.getInstance(network); } else { connection = undefined; } diff --git a/src/services/wallet/wallet.validators.ts b/src/services/wallet/wallet.validators.ts index d58a0db605..faf67a5f58 100644 --- a/src/services/wallet/wallet.validators.ts +++ b/src/services/wallet/wallet.validators.ts @@ -76,74 +76,79 @@ export const validatePrivateKey: Validator = mkSelectingValidator( algorand: mkValidator( 'privateKey', invalidAlgorandPrivateKeyOrMnemonicError, - (val) => typeof val === 'string' && isAlgorandPrivateKeyOrMnemonic(val) + (val) => typeof val === 'string' && isAlgorandPrivateKeyOrMnemonic(val), ), ethereum: mkValidator( 'privateKey', invalidEthPrivateKeyError, - (val) => typeof val === 'string' && isEthPrivateKey(val) + (val) => typeof val === 'string' && isEthPrivateKey(val), ), cronos: mkValidator( 'privateKey', invalidEthPrivateKeyError, - (val) => typeof val === 'string' && isEthPrivateKey(val) + (val) => typeof val === 'string' && isEthPrivateKey(val), ), avalanche: mkValidator( 'privateKey', invalidEthPrivateKeyError, - (val) => typeof val === 'string' && isEthPrivateKey(val) + (val) => typeof val === 'string' && isEthPrivateKey(val), ), harmony: mkValidator( 'privateKey', invalidEthPrivateKeyError, - (val) => typeof val === 'string' && isEthPrivateKey(val) + (val) => typeof val === 'string' && isEthPrivateKey(val), ), near: mkValidator( 'privateKey', invalidNearPrivateKeyError, - (val) => typeof val === 'string' && isNearPrivateKey(val) + (val) => typeof val === 'string' && isNearPrivateKey(val), ), cosmos: mkValidator( 'privateKey', invalidCosmosPrivateKeyError, - (val) => typeof val === 'string' && isCosmosPrivateKey(val) + (val) => typeof val === 'string' && isCosmosPrivateKey(val), ), osmosis: mkValidator( 'privateKey', invalidCosmosPrivateKeyError, - (val) => typeof val === 'string' && isCosmosPrivateKey(val) + (val) => typeof val === 'string' && isCosmosPrivateKey(val), ), polygon: mkValidator( 'privateKey', invalidEthPrivateKeyError, - (val) => typeof val === 'string' && isEthPrivateKey(val) + (val) => typeof val === 'string' && isEthPrivateKey(val), ), 'binance-smart-chain': mkValidator( 'privateKey', invalidEthPrivateKeyError, - (val) => typeof val === 'string' && isEthPrivateKey(val) + (val) => typeof val === 'string' && isEthPrivateKey(val), ), xdc: mkValidator( 'privateKey', invalidEthPrivateKeyError, - (val) => typeof val === 'string' && isEthPrivateKey(val) + (val) => typeof val === 'string' && isEthPrivateKey(val), ), tezos: mkValidator( 'privateKey', invalidTezosPrivateKeyError, - (val) => typeof val === 'string' && isTezosPrivateKey(val) + (val) => typeof val === 'string' && isTezosPrivateKey(val), ), xrpl: mkValidator( 'privateKey', invalidXRPLPrivateKeyError, - (val) => typeof val === 'string' && isXRPLSeedKey(val) + (val) => typeof val === 'string' && isXRPLSeedKey(val), ), kujira: mkValidator( 'privateKey', invalidKujiraPrivateKeyError, - (val) => typeof val === 'string' && isKujiraPrivateKey(val) + (val) => typeof val === 'string' && isKujiraPrivateKey(val), ), - } + telos: mkValidator( + 'privateKey', + invalidEthPrivateKeyError, + (val) => typeof val === 'string' && isEthPrivateKey(val), + ), + }, ); export const invalidChainError: string = @@ -177,33 +182,34 @@ export const validateChain: Validator = mkValidator( val === 'binance-smart-chain' || val === 'tezos' || val === 'xrpl' || - val === 'kujira') + val === 'kujira' || + val === 'telos'), ); export const validateNetwork: Validator = mkValidator( 'network', invalidNetworkError, - (val) => typeof val === 'string' + (val) => typeof val === 'string', ); export const validateAddress: Validator = mkValidator( 'address', invalidAddressError, - (val) => typeof val === 'string' + (val) => typeof val === 'string', ); export const validateAccountID: Validator = mkValidator( 'accountId', invalidAccountIDError, (val) => typeof val === 'string', - true + true, ); export const validateMessage: Validator = mkValidator( 'message', invalidMessageError, (val) => typeof val === 'string', - true + true, ); export const validateAddWalletRequest: RequestValidator = mkRequestValidator([ @@ -214,7 +220,7 @@ export const validateAddWalletRequest: RequestValidator = mkRequestValidator([ ]); export const validateRemoveWalletRequest: RequestValidator = mkRequestValidator( - [validateAddress, validateChain] + [validateAddress, validateChain], ); export const validateWalletSignRequest: RequestValidator = mkRequestValidator([ diff --git a/src/templates/lists/telos_evm_tokens.json b/src/templates/lists/telos_evm_tokens.json new file mode 100644 index 0000000000..21d87baf89 --- /dev/null +++ b/src/templates/lists/telos_evm_tokens.json @@ -0,0 +1,202 @@ +{ + "name": "Telos Evm", + "tokens": [ + { + "address": "0x26Ed0F16e777C94A6FE798F9E20298034930Bae8", + "chainId": 40, + "decimals": 18, + "logoURI": "https://raw.githubusercontent.com/sushiswap/list/master/logos/token-logos/network/telos/0x26Ed0F16e777C94A6FE798F9E20298034930Bae8.jpg", + "name": "Binance Coin", + "symbol": "BNB" + }, + + { + "address": "0x7627b27594bc71e6Ab0fCE755aE8931EB1E12DAC", + "chainId": 40, + "decimals": 8, + "logoURI": "https://raw.githubusercontent.com/sushiswap/list/master/logos/token-logos/network/telos/0x7627b27594bc71e6Ab0fCE755aE8931EB1E12DAC.jpg", + "name": "Bitcoin", + "symbol": "BTC.b" + }, + + { + "address": "0xA0fB8cd450c8Fd3a11901876cD5f17eB47C6bc50", + "chainId": 40, + "decimals": 18, + "logoURI": "https://raw.githubusercontent.com/sushiswap/list/master/logos/token-logos/network/telos/0xA0fB8cd450c8Fd3a11901876cD5f17eB47C6bc50.jpg", + "name": "Ethereum", + "symbol": "ETH" + }, + + { + "address": "0x76aE0b4C828DdCa1841a4FE394Af5D8679Baf118", + "chainId": 40, + "decimals": 9, + "logoURI": "https://raw.githubusercontent.com/sushiswap/list/master/logos/token-logos/network/telos/0x76aE0b4C828DdCa1841a4FE394Af5D8679Baf118.jpg", + "name": "ShibaTelos Coin", + "symbol": "SC" + }, + { + "address": "0xB4B01216a5Bc8F1C8A33CD990A1239030E60C905", + "chainId": 40, + "decimals": 18, + "logoURI": "https://raw.githubusercontent.com/sushiswap/list/master/logos/token-logos/network/telos/0xB4B01216a5Bc8F1C8A33CD990A1239030E60C905.jpg", + "name": "Staked TLOS", + "symbol": "STLOS" + }, + + { + "address": "0x8D97Cea50351Fb4329d591682b148D43a0C3611b", + "chainId": 40, + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/sushiswap/list/master/logos/token-logos/network/telos/0x8D97Cea50351Fb4329d591682b148D43a0C3611b.jpg", + "name": "USD Coin", + "symbol": "USDC" + }, + + { + "address": "0x975Ed13fa16857E83e7C493C7741D556eaaD4A3f", + "chainId": 40, + "decimals": 6, + "logoURI": "https://raw.githubusercontent.com/sushiswap/list/master/logos/token-logos/network/telos/0x975Ed13fa16857E83e7C493C7741D556eaaD4A3f.jpg", + "name": "Tether Stable Coin", + "symbol": "USDT" + }, + { + "address": "0xD102cE6A4dB07D247fcc28F366A623Df0938CA9E", + "chainId": 40, + "decimals": 18, + "logoURI": "https://raw.githubusercontent.com/sushiswap/list/master/logos/token-logos/network/telos/0xD102cE6A4dB07D247fcc28F366A623Df0938CA9E.jpg", + "name": "Wrapped Telos", + "symbol": "WTLOS" + }, + { + "chainId": 40, + "address": "0x11fbfdf906d32753fa2a083dbd4fb25c1094c6c4", + "symbol": "APISH", + "logoURI": "https://raw.githubusercontent.com/telosnetwork/token-list/main/logos/0x11fbfdf906d32753fa2a083dbd4fb25c1094c6c4.png", + "name": "APISH ME", + "decimals": 9 + }, + { + "chainId": 40, + "address": "0xE1C110E1B1b4A1deD0cAf3E42BfBdbB7b5d7cE1C", + "symbol": "ELK", + "logoURI": "https://raw.githubusercontent.com/elkfinance/tokens/main/logos/telos/0xeEeEEb57642040bE42185f49C52F7E9B38f8eeeE/logo.png", + "name": "ELK", + "decimals": 18 + }, + { + "chainId": 40, + "address": "0xeEeEEb57642040bE42185f49C52F7E9B38f8eeeE", + "symbol": "ELK", + "logoURI": "https://raw.githubusercontent.com/elkfinance/tokens/main/logos/telos/0xeEeEEb57642040bE42185f49C52F7E9B38f8eeeE/logo.png", + "name": "ELK", + "decimals": 18 + }, + { + "chainId": 40, + "address": "0xCC47EB13916a76e262b0EE48A71e3c7953091e7a", + "symbol": "SWAP", + "logoURI": "https://raw.githubusercontent.com/evm20/tokens/main/swaptoken.svg", + "name": "SWAP", + "decimals": 18 + }, + { + "chainId": 40, + "address": "0xa84df7aFbcbCC1106834a5feD9453bd1219B1fb5", + "symbol": "Arc", + "name": "Archly Arc v1", + "logoURI": "https://raw.githubusercontent.com/ArchlyFi/token-list/main/logos/arc-logo.png", + "decimals": 18 + }, + { + "chainId": 40, + "address": "0xE56c325a68b489812081E8A7b60b4017fd2AD280", + "symbol": "PE", + "name": "Positron", + "logoURI": "https://raw.githubusercontent.com/OmniDexFinance/tokenLogo/master/0xE56c325a68b489812081E8A7b60b4017fd2AD280.png", + "decimals": 4 + }, + { + "chainId": 40, + "address": "0x7e1cfe10949A6086A28C38aA4A43fDeAB34f198A", + "symbol": "DECO", + "name": "Destiny Coin", + "logoURI": "https://api.dstor.cloud/ipfs/QmVx1uSPTW7UQWGbz3ba5Nf7DPVyieEdRnQGHogo7t9Pw6", + "decimals": 4 + }, + { + "chainId": 40, + "address": "0xE35b6D08050fef8E2bA2b1ED9C8f966a2346A500", + "symbol": "WAG", + "name": "WagyuSwap Token", + "logoURI": "https://raw.githubusercontent.com/wagyuswapapp/assets/master/blockchains/telos/assets/0xe35b6d08050fef8e2ba2b1ed9c8f966a2346a500/logo.png", + "decimals": 18 + }, + { + "chainId": 40, + "address": "0xac45ede2098bc989dfe0798b4630872006e24c3f", + "symbol": "SLUSH", + "name": "Swapsicle SLUSH", + "logoURI": "https://raw.githubusercontent.com/swapsicledex/swapsicle-token-list/master/logos/telos/0xaC45EDe2098bc989Dfe0798B4630872006e24c3f/logo.png", + "decimals": 18 + }, + { + "chainId": 40, + "address": "0xaae65b3b41f7c372c729b59b08ca93d53e9b79b3", + "symbol": "ICE", + "name": "Swapsicle ICE", + "logoURI": "https://raw.githubusercontent.com/swapsicledex/swapsicle-token-list/master/logos/telos/0xaae65b3b41f7c372c729b59b08ca93d53e9b79b3/logo.svg", + "decimals": 18 + }, + { + "chainId": 40, + "address": "0xfB319EA5DDEd8cFe8Bcf9c720ed380b98874Bf63", + "symbol": "RBN", + "name": "Robinos", + "logoURI": "https://raw.githubusercontent.com/telosnetwork/token-list/main/logos/0xfB319EA5DDEd8cFe8Bcf9c720ed380b98874Bf63.png", + "decimals": 6 + }, + { + "chainId": 40, + "address": "0x2f15F85a6c346C0a2514Af70075259e503E7137B", + "symbol": "DMMY", + "logoURI": "https://raw.githubusercontent.com/telosnetwork/token-list/master/logos/dmmy.svg", + "name": "dummy☻DAO", + "decimals": 18 + }, + { + "chainId": 40, + "address": "0xd0208c3BE89002f62e42141d4542b15F45FB48aB", + "symbol": "FORT", + "name": "Fortis Coin", + "logoURI": "https://fortisnetwork.io/logos/White.svg", + "decimals": 18 + }, + { + "chainId": 40, + "address": "0x7097Ee02465FB494841740B1a2b63c21Eed655E7", + "symbol": "BANANA", + "name": "Banana", + "logoURI": "https://raw.githubusercontent.com/telosnetwork/token-list/master/logos/banana.png", + "decimals": 4 + }, + { + "chainId": 40, + "address": "0xe8876189A80B2079D8C0a7867e46c50361D972c1", + "symbol": "Arc", + "name": "Archly Arc v2", + "logoURI": "https://raw.githubusercontent.com/ArchlyFi/token-list/main/logos/arc-logo.png", + "decimals": 18 + }, + { + "chainId": 40, + "name": "RF", + "symbol": "RF", + "address": "0xb99C43d3bce4c8Ad9B95a4A178B04a7391b2a6EB", + "decimals": 18, + "logoURI": "https://raw.githubusercontent.com/telosnetwork/token-list/master/logos/RF.webp" + } + ] +} diff --git a/src/templates/openocean.yml b/src/templates/openocean.yml index 39427738b8..7a1ff1ce78 100644 --- a/src/templates/openocean.yml +++ b/src/templates/openocean.yml @@ -28,3 +28,6 @@ contractAddresses: cronos: mainnet: routerAddress: '0x6352a56caadC4F1E25CD6c75970Fa768A3304e64' + telos: + evm: + routerAddress: '0x6352a56caadC4F1E25CD6c75970Fa768A3304e64' diff --git a/src/templates/root.yml b/src/templates/root.yml index d15d5b0028..5c0cd4e193 100644 --- a/src/templates/root.yml +++ b/src/templates/root.yml @@ -135,3 +135,7 @@ configurations: $namespace balancer: configurationPath: balancer.yml schemaPath: cronos-connector-schema.json + + $namespace telos: + configurationPath: telos.yml + schemaPath: ethereum-schema.json diff --git a/src/templates/telos.yml b/src/templates/telos.yml new file mode 100644 index 0000000000..7a714cfdd8 --- /dev/null +++ b/src/templates/telos.yml @@ -0,0 +1,10 @@ +networks: + evm: + chainID: 40 + nodeURL: https://mainnet.telos.net/evm + tokenListType: FILE + tokenListSource: conf/lists/telos_evm_tokens.json + nativeCurrencySymbol: TLOS + +manualGasPrice: 600 +gasLimitTransaction: 1000000 diff --git a/test-bronze/connectors/openocean/telos.openocean.routes.test.ts b/test-bronze/connectors/openocean/telos.openocean.routes.test.ts new file mode 100644 index 0000000000..9c0dc1a807 --- /dev/null +++ b/test-bronze/connectors/openocean/telos.openocean.routes.test.ts @@ -0,0 +1,705 @@ +import request from 'supertest'; +import { Telos } from '../../../src/chains/telos/telos'; +import { Openocean } from '../../../src/connectors/openocean/openocean'; +import { patchEVMNonceManager } from '../../../test/evm.nonce.mock'; +import { patch, unpatch } from '../../../test/services/patch'; +import { gasCostInEthString } from '../../../src/services/base'; +import { AmmRoutes } from '../../../src/amm/amm.routes'; +import express from 'express'; +import { Express } from 'express-serve-static-core'; +let app: Express; +let telos: Telos; +let openocean: Openocean; + +beforeAll(async () => { + app = express(); + app.use(express.json()); + + telos = Telos.getInstance('goerli'); + patchEVMNonceManager(telos.nonceManager); + await telos.init(); + + openocean = Openocean.getInstance('telos', 'goerli'); + await openocean.init(); + + app.use('/amm', AmmRoutes.router); +}); + +beforeEach(() => { + patchEVMNonceManager(telos.nonceManager); +}); + +afterEach(() => { + unpatch(); +}); + +afterAll(async () => { + await telos.close(); +}); + +const address: string = '0xFaA12FD102FE8623C9299c72B03E45107F2772B5'; + +const patchGetWallet = () => { + patch(telos, 'getWallet', () => { + return { + address: '0xFaA12FD102FE8623C9299c72B03E45107F2772B5', + }; + }); +}; + +const patchInit = () => { + patch(openocean, 'init', async () => { + return; + }); +}; + +const patchStoredTokenList = () => { + patch(telos, 'tokenList', () => { + return [ + { + chainId: 137, + name: 'USDC', + symbol: 'USDC', + address: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', + decimals: 6, + }, + { + chainId: 137, + name: 'TLOS', + symbol: 'TLOS', + address: '0x0000000000000000000000000000000000001010', + decimals: 18, + }, + ]; + }); +}; + +const patchGetTokenBySymbol = () => { + patch(telos, 'getTokenBySymbol', (symbol: string) => { + if (symbol === 'USDC') { + return { + chainId: 137, + name: 'USDC', + symbol: 'USDC', + address: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', + decimals: 6, + }; + } else { + return { + chainId: 137, + name: 'TLOS', + symbol: 'TLOS', + address: '0x0000000000000000000000000000000000001010', + decimals: 18, + }; + } + }); +}; + +const patchGetTokenByAddress = () => { + patch(openocean, 'getTokenByAddress', () => { + return { + chainId: 137, + name: 'USDC', + symbol: 'USDC', + address: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', + decimals: 6, + }; + }); +}; + +const patchGasPrice = () => { + patch(telos, 'gasPrice', () => 100); +}; + +const patchEstimateBuyTrade = () => { + patch(openocean, 'estimateBuyTrade', () => { + return { + expectedAmount: { + toSignificant: () => 100, + }, + trade: { + executionPrice: { + invert: jest.fn().mockReturnValue({ + toSignificant: () => 100, + toFixed: () => '100', + }), + }, + }, + }; + }); +}; + +const patchEstimateSellTrade = () => { + patch(openocean, 'estimateSellTrade', () => { + return { + expectedAmount: { + toSignificant: () => 100, + }, + trade: { + executionPrice: { + toSignificant: () => 100, + toFixed: () => '100', + }, + }, + }; + }); +}; + +const patchGetNonce = () => { + patch(telos.nonceManager, 'getNonce', () => 21); +}; + +const patchExecuteTrade = () => { + patch(openocean, 'executeTrade', () => { + return { nonce: 21, hash: '000000000000000' }; + }); +}; + +describe('POST /amm/price', () => { + it('should return 200 for BUY', async () => { + patchGetWallet(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patchGasPrice(); + patchEstimateBuyTrade(); + patchGetNonce(); + patchExecuteTrade(); + await request(app) + .post(`/amm/price`) + .send({ + chain: 'telos', + network: 'goerli', + connector: 'openocean', + quote: 'BUSD', + base: 'USDC', + amount: '0.01', + side: 'BUY', + }) + .set('Accept', 'application/json') + .expect(200) + .then((res: any) => { + expect(res.body.amount).toEqual('0.010000'); + expect(res.body.rawAmount).toEqual('10000'); + }); + }); + + it('should return 200 for SELL', async () => { + patchGetWallet(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patchGasPrice(); + patchEstimateSellTrade(); + patchGetNonce(); + patchExecuteTrade(); + await request(app) + .post(`/amm/price`) + .send({ + chain: 'telos', + network: 'goerli', + connector: 'openocean', + quote: 'USDC', + base: 'BUSD', + amount: '10000', + side: 'SELL', + }) + .set('Accept', 'application/json') + .expect(200) + .then((res: any) => { + expect(res.body.amount).toEqual('10000.000000'); + expect(res.body.rawAmount).toEqual('10000000000'); + }); + }); + + it('should return 500 for unrecognized quote symbol', async () => { + patchGetWallet(); + patchStoredTokenList(); + patch(telos, 'getTokenBySymbol', (symbol: string) => { + if (symbol === 'WETH') { + return { + chainId: 1, + name: 'WETH', + symbol: 'WETH', + address: '0xd0A1E359811322d97991E03f863a0C30C2cF029C', + decimals: 18, + }; + } else { + return null; + } + }); + patchGetTokenByAddress(); + await request(app) + .post(`/amm/price`) + .send({ + chain: 'telos', + network: 'goerli', + connector: 'openocean', + quote: 'USDC', + base: 'bDAI', + amount: '10000', + side: 'SELL', + }) + .set('Accept', 'application/json') + .expect(500); + }); + + it('should return 500 for unrecognized base symbol', async () => { + patchGetWallet(); + patchStoredTokenList(); + patch(telos, 'getTokenBySymbol', (symbol: string) => { + if (symbol === 'WETH') { + return { + chainId: 1, + name: 'WETH', + symbol: 'WETH', + address: '0xd0A1E359811322d97991E03f863a0C30C2cF029C', + decimals: 18, + }; + } else { + return null; + } + }); + patchGetTokenByAddress(); + await request(app) + .post(`/amm/price`) + .send({ + chain: 'telos', + network: 'goerli', + connector: 'openocean', + quote: 'USDC', + base: 'bDAI', + 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: 'telos', + network: 'goerli', + connector: 'openocean', + quote: 'USDC', + base: 'bDAI', + 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: 'telos', + network: 'goerli', + connector: 'openocean', + quote: 'USDC', + base: 'bDAI', + 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(openocean, 'priceSwapIn', () => { + return 'error'; + }); + + await request(app) + .post(`/amm/price`) + .send({ + chain: 'telos', + network: 'goerli', + connector: 'openocean', + quote: 'USDC', + base: 'bDAI', + 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(openocean, 'priceSwapOut', () => { + return 'error'; + }); + + await request(app) + .post(`/amm/price`) + .send({ + chain: 'telos', + network: 'goerli', + connector: 'openocean', + quote: 'USDC', + base: 'bDAI', + amount: '10000', + side: 'BUY', + }) + .set('Accept', 'application/json') + .expect(500); + }); +}); + +describe('POST /amm/trade', () => { + const patchForBuy = () => { + patchGetWallet(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patchGasPrice(); + patchEstimateBuyTrade(); + patchGetNonce(); + patchExecuteTrade(); + }; + it('should return 200 for BUY', async () => { + patchForBuy(); + await request(app) + .post(`/amm/trade`) + .send({ + chain: 'telos', + network: 'goerli', + connector: 'openocean', + quote: 'BUSD', + base: 'USDC', + amount: '0.01', + 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: 'telos', + network: 'goerli', + connector: 'openocean', + quote: 'BUSD', + base: 'USDC', + amount: '0.01', + 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: 'telos', + network: 'goerli', + connector: 'openocean', + quote: 'BUSD', + base: 'USDC', + amount: '0.01', + address, + side: 'BUY', + nonce: 21, + maxFeePerGas: '5000000000', + maxPriorityFeePerGas: '5000000000', + }) + .set('Accept', 'application/json') + .expect(200); + }); + + const patchForSell = () => { + patchGetWallet(); + patchStoredTokenList(); + patchGetTokenBySymbol(); + patchGetTokenByAddress(); + patchGasPrice(); + patchEstimateSellTrade(); + patchGetNonce(); + patchExecuteTrade(); + }; + it('should return 200 for SELL', async () => { + patchForSell(); + await request(app) + .post(`/amm/trade`) + .send({ + chain: 'telos', + network: 'goerli', + connector: 'openocean', + quote: 'USDC', + base: 'BUSD', + 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: 'telos', + network: 'goerli', + connector: 'openocean', + quote: 'USDC', + base: 'BUSD', + amount: '10000', + address, + side: 'SELL', + nonce: 21, + maxFeePerGas: '5000000000', + maxPriorityFeePerGas: '5000000000', + }) + .set('Accept', 'application/json') + .expect(200); + }); + + it('should return 404 when parameters are incorrect', async () => { + await request(app) + .post(`/amm/trade`) + .send({ + chain: 'telos', + network: 'goerli', + connector: 'openocean', + quote: 'USDC', + base: 'BUSD', + amount: 10000, + address: 'da8', + side: 'comprar', + }) + .set('Accept', 'application/json') + .expect(404); + }); + + it('should return 500 when base token is unknown', async () => { + patchForSell(); + patch(telos, 'getTokenBySymbol', (symbol: string) => { + if (symbol === 'USDC') { + return { + chainId: 43114, + name: 'USDC', + symbol: 'USDC', + address: '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E', + decimals: 6, + }; + } else { + return null; + } + }); + + await request(app) + .post(`/amm/trade`) + .send({ + chain: 'telos', + network: 'goerli', + connector: 'openocean', + quote: 'USDC', + base: 'BITCOIN', + amount: '10000', + address, + side: 'BUY', + nonce: 21, + maxFeePerGas: '5000000000', + maxPriorityFeePerGas: '5000000000', + }) + .set('Accept', 'application/json') + .expect(500); + }); + + it('should return 500 when quote token is unknown', async () => { + patchForSell(); + patch(telos, 'getTokenBySymbol', (symbol: string) => { + if (symbol === 'USDC') { + return { + chainId: 43114, + name: 'USDC', + symbol: 'USDC', + address: '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E', + decimals: 6, + }; + } else { + return null; + } + }); + + await request(app) + .post(`/amm/trade`) + .send({ + chain: 'telos', + network: 'goerli', + connector: 'openocean', + quote: 'BITCOIN', + base: 'USDC', + amount: '10000', + address, + side: 'BUY', + nonce: 21, + maxFeePerGas: '5000000000', + maxPriorityFeePerGas: '5000000000', + }) + .set('Accept', 'application/json') + .expect(500); + }); + + it('should return 200 for SELL with limitPrice', async () => { + patchForSell(); + await request(app) + .post(`/amm/trade`) + .send({ + chain: 'telos', + network: 'goerli', + connector: 'openocean', + quote: 'USDC', + base: 'BUSD', + 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: 'telos', + network: 'goerli', + connector: 'openocean', + quote: 'BUSD', + base: 'USDC', + amount: '0.01', + address, + side: 'BUY', + nonce: 21, + limitPrice: '999999999999999999999', + }) + .set('Accept', 'application/json') + .expect(200); + }); + + it('should return 200 for SELL with price higher than limitPrice', async () => { + patchForSell(); + await request(app) + .post(`/amm/trade`) + .send({ + chain: 'telos', + network: 'goerli', + connector: 'openocean', + quote: 'USDC', + base: 'BUSD', + amount: '10000', + address, + side: 'SELL', + nonce: 21, + limitPrice: '99999999999', + }) + .set('Accept', 'application/json') + .expect(500); + }); + + it('should return 200 for BUY with price less than limitPrice', async () => { + patchForBuy(); + await request(app) + .post(`/amm/trade`) + .send({ + chain: 'telos', + network: 'goerli', + connector: 'openocean', + quote: 'BUSD', + base: 'USDC', + amount: '0.01', + address, + side: 'BUY', + nonce: 21, + limitPrice: '9', + }) + .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: 'telos', + network: 'goerli', + connector: 'openocean', + }) + .set('Accept', 'application/json') + .expect(200) + .then((res: any) => { + expect(res.body.network).toEqual('goerli'); + expect(res.body.gasPrice).toEqual(100); + expect(res.body.gasCost).toEqual( + gasCostInEthString(100, openocean.gasLimitEstimate), + ); + }); + }); + + it('should return 500 for invalid connector', async () => { + patchInit(); + patchGasPrice(); + + await request(app) + .post('/amm/estimateGas') + .send({ + chain: 'telos', + network: 'goerli', + connector: 'pangolin', + }) + .set('Accept', 'application/json') + .expect(500); + }); +});