diff --git a/package.json b/package.json index 5b577addbf..e8c60a2208 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "web3": "^1.7.3", "winston": "^3.3.3", "winston-daily-rotate-file": "^4.5.5", + "xsswap-sdk": "^1.0.1", "yarn": "^1.22.17" }, "devDependencies": { diff --git a/src/app.ts b/src/app.ts index 49a8508fe0..210037d779 100644 --- a/src/app.ts +++ b/src/app.ts @@ -21,6 +21,7 @@ import { AmmRoutes, AmmLiquidityRoutes, PerpAmmRoutes } from './amm/amm.routes'; import { MadMeerkatConfig } from './connectors/mad_meerkat/mad_meerkat.config'; import { PangolinConfig } from './connectors/pangolin/pangolin.config'; import { QuickswapConfig } from './connectors/quickswap/quickswap.config'; +import { XsswapConfig } from './connectors/xsswap/xsswap.config'; import { TraderjoeConfig } from './connectors/traderjoe/traderjoe.config'; import { UniswapConfig } from './connectors/uniswap/uniswap.config'; import { OpenoceanConfig } from './connectors/openocean/openocean.config'; @@ -83,6 +84,7 @@ gatewayApp.get( uniswap: UniswapConfig.config.availableNetworks, pangolin: PangolinConfig.config.availableNetworks, quickswap: QuickswapConfig.config.availableNetworks, + xsswap: XsswapConfig.config.availableNetworks, sushiswap: SushiswapConfig.config.availableNetworks, openocean: OpenoceanConfig.config.availableNetworks, traderjoe: TraderjoeConfig.config.availableNetworks, @@ -104,6 +106,7 @@ gatewayApp.post( }) ); + // handle any error thrown in the gateway api route gatewayApp.use( ( diff --git a/src/chains/ethereum/ethereum.validators.ts b/src/chains/ethereum/ethereum.validators.ts index 1f86c9a0f0..0c050d722d 100644 --- a/src/chains/ethereum/ethereum.validators.ts +++ b/src/chains/ethereum/ethereum.validators.ts @@ -57,6 +57,7 @@ export const validateSpender: Validator = mkValidator( val === 'viperswap' || val === 'openocean' || val === 'quickswap' || + val === 'xsswap' || val === 'defikingdoms' || val === 'defira' || val === 'mad_meerkat' || diff --git a/src/chains/xdc/xdc.ts b/src/chains/xdc/xdc.ts index bd4234e16b..102bf18972 100644 --- a/src/chains/xdc/xdc.ts +++ b/src/chains/xdc/xdc.ts @@ -4,7 +4,7 @@ import { Contract, Transaction, Wallet } from 'ethers'; import { EthereumBase } from '../ethereum/ethereum-base'; import { getEthereumConfig as getPolygonConfig } from '../ethereum/ethereum.config'; import { Provider } from '@ethersproject/abstract-provider'; -import { UniswapConfig } from '../../connectors/uniswap/uniswap.config'; +import { XsswapConfig } from '../../connectors/xsswap/xsswap.config'; import { Ethereumish } from '../../services/common-interfaces'; import { ConfigManagerV2 } from '../../services/config-manager-v2'; @@ -65,10 +65,8 @@ export class Xdc extends EthereumBase implements Ethereumish { getSpender(reqSpender: string): string { let spender: string; - if (reqSpender === 'uniswap') { - spender = UniswapConfig.config.uniswapV3SmartOrderRouterAddress( - this._chain - ); + if (reqSpender === 'xsswap') { + spender = XsswapConfig.config.routerAddress(this._chain); } else { spender = reqSpender; } diff --git a/src/chains/xdc/xdc.validators.ts b/src/chains/xdc/xdc.validators.ts index 957be623ac..614d61d2ed 100644 --- a/src/chains/xdc/xdc.validators.ts +++ b/src/chains/xdc/xdc.validators.ts @@ -16,11 +16,11 @@ import { export const invalidSpenderError: string = 'The spender param is not a valid xdc address (0x followed by 40 hexidecimal characters).'; -// given a request, look for a key called spender that is 'uniswap', 'sushi' or an Ethereum address +// given a request, look for a key called spender that is 'xsswap' or an Ethereum address export const validateSpender: Validator = mkValidator( 'spender', invalidSpenderError, - (val) => typeof val === 'string' && (val === 'uniswap' || isAddress(val)) + (val) => typeof val === 'string' && (val === 'xsswap' || isAddress(val)) ); export const validateXdcApproveRequest: RequestValidator = diff --git a/src/connectors/connectors.routes.ts b/src/connectors/connectors.routes.ts index 48d7f54636..1308f8ff29 100644 --- a/src/connectors/connectors.routes.ts +++ b/src/connectors/connectors.routes.ts @@ -7,6 +7,7 @@ import { OpenoceanConfig } from './openocean/openocean.config'; import { PangolinConfig } from './pangolin/pangolin.config'; import { PerpConfig } from './perp/perp.config'; import { QuickswapConfig } from './quickswap/quickswap.config'; +import { XsswapConfig } from './xsswap/xsswap.config'; import { SushiswapConfig } from './sushiswap/sushiswap.config'; import { TraderjoeConfig } from './traderjoe/traderjoe.config'; import { UniswapConfig } from './uniswap/uniswap.config'; @@ -50,6 +51,11 @@ export namespace ConnectorsRoutes { trading_type: QuickswapConfig.config.tradingTypes, available_networks: QuickswapConfig.config.availableNetworks, }, + { + name: 'xsswap', + trading_type: XsswapConfig.config.tradingTypes, + available_networks: XsswapConfig.config.availableNetworks, + }, { name: 'perp', trading_type: PerpConfig.config.tradingTypes('perp'), diff --git a/src/connectors/xsswap/xsswap.config.ts b/src/connectors/xsswap/xsswap.config.ts new file mode 100644 index 0000000000..cc0e4154e4 --- /dev/null +++ b/src/connectors/xsswap/xsswap.config.ts @@ -0,0 +1,22 @@ +import { ConfigManagerV2 } from '../../services/config-manager-v2'; +import { AvailableNetworks } from '../../services/config-manager-types'; + +export namespace XsswapConfig { + export interface NetworkConfig { + allowedSlippage: string; + gasLimitEstimate: number; + ttl: number; + routerAddress: (network: string) => string; + tradingTypes: Array; + availableNetworks: Array; + } + + export const config: NetworkConfig = { + allowedSlippage: ConfigManagerV2.getInstance().get('xsswap.allowedSlippage'), + gasLimitEstimate: ConfigManagerV2.getInstance().get('xsswap.gasLimitEstimate'), + ttl: ConfigManagerV2.getInstance().get('xsswap.ttl'), + routerAddress: (network: string) => ConfigManagerV2.getInstance().get('xsswap.contractAddresses.' + network + '.routerAddress'), + tradingTypes: ['EVM_AMM'], + availableNetworks: [{ chain: 'xdc', networks: ['xinfin', 'apothem'] }], + }; +} \ No newline at end of file diff --git a/src/connectors/xsswap/xsswap.ts b/src/connectors/xsswap/xsswap.ts new file mode 100644 index 0000000000..67c0198b40 --- /dev/null +++ b/src/connectors/xsswap/xsswap.ts @@ -0,0 +1,234 @@ +import { percentRegexp } from '../../services/config-manager-v2'; +import { UniswapishPriceError } from '../../services/error-handler'; +import { + BigNumber, + Contract, + ContractInterface, + Transaction, + Wallet, +} from 'ethers'; +import { isFractionString } from '../../services/validators'; +import { XsswapConfig } from './xsswap.config'; +import routerAbi from './xsswap_v2_router_abi.json'; +import { + Fetcher, + Percent, + Router, + Token, + TokenAmount, + Trade, + Pair +} from 'xsswap-sdk'; +import { logger } from '../../services/logger'; +import { Xdc } from '../../chains/xdc/xdc'; +import { ExpectedTrade, Uniswapish } from '../../services/common-interfaces'; + +export class Xsswap implements Uniswapish { + private static _instances: { [name: string]: Xsswap }; + private xdc: Xdc; + private _router: string; + private _routerAbi: ContractInterface; + private _gasLimitEstimate: number; + private _ttl: number; + private chainId; + private tokenList: Record = {}; + private _ready: boolean = false; + + private constructor(network: string) { + const config = XsswapConfig.config; + this.xdc = Xdc.getInstance(network); + this.chainId = this.xdc.chainId; + this._router = config.routerAddress(network); + this._ttl = config.ttl; + this._routerAbi = routerAbi.abi; + this._gasLimitEstimate = config.gasLimitEstimate; + } + + public static getInstance(chain: string, network: string): Xsswap { + if (Xsswap._instances === undefined) { + Xsswap._instances = {}; + } + if (!(chain + network in Xsswap._instances)) { + Xsswap._instances[chain + network] = new Xsswap(network); + } + + return Xsswap._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[address]; + } + + public async init() { + if (!this.xdc.ready()) { + await this.xdc.init(); + } + for (const token of this.xdc.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; + } + + /** + * Router smart contract ABI. + */ + public get routerAbi(): ContractInterface { + return this._routerAbi; + } + + /** + * Default gas limit used to estimate cost 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; + } + + /** + * 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 = XsswapConfig.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): Promise { + const nativeTokenAmount: TokenAmount = new TokenAmount(baseToken, amount.toString()); + logger.info(`Fetching pair data for ${baseToken.address}-${quoteToken.address}.`); + const pair: Pair = await Fetcher.fetchPairData(baseToken, quoteToken, this.xdc.provider); + const trades: Trade[] = Trade.bestTradeExactIn([pair], nativeTokenAmount, quoteToken, { maxHops: 1 }); + if (!trades || trades.length === 0) { + throw new UniswapishPriceError(`priceSwapIn: no trade pair found for ${baseToken} to ${quoteToken}.`); + } + logger.info(`Best trade for ${baseToken.address}-${quoteToken.address}: ${trades[0]}`); + const expectedAmount = trades[0].minimumAmountOut(this.getAllowedSlippage(allowedSlippage)); + return { trade: trades[0], 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): Promise { + const nativeTokenAmount: TokenAmount = new TokenAmount(baseToken, amount.toString()); + logger.info(`Fetching pair data for ${quoteToken.address}-${baseToken.address}.`); + const pair: Pair = await Fetcher.fetchPairData(quoteToken, baseToken, this.xdc.provider); + const trades: Trade[] = Trade.bestTradeExactOut([pair], quoteToken, nativeTokenAmount, { maxHops: 1 }); + if (!trades || trades.length === 0) { + throw new UniswapishPriceError(`priceSwapOut: no trade pair found for ${quoteToken.address} to ${baseToken.address}.`); + } + logger.info(`Best trade for ${quoteToken.address}-${baseToken.address}: ${trades[0]}`); + + const expectedAmount = trades[0].maximumAmountIn(this.getAllowedSlippage(allowedSlippage)); + return { trade: trades[0], expectedAmount }; + } + + /** + * Given a wallet and a Uniswap-ish trade, try to execute it on blockchain. + * + * @param wallet Wallet + * @param trade Expected trade + * @param gasPrice Base gas price, for pre-EIP1559 transactions + * @param xsswapRouter 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, + xsswapRouter: string, + ttl: number, + abi: ContractInterface, + gasLimit: number, + nonce?: number, + maxFeePerGas?: BigNumber, + maxPriorityFeePerGas?: BigNumber, + allowedSlippage?: string + ): Promise { + const result = Router.swapCallParameters(trade, { + ttl, + recipient: wallet.address, + allowedSlippage: this.getAllowedSlippage(allowedSlippage), + }); + + const contract = new Contract(xsswapRouter, abi, wallet); + if (!nonce) { + nonce = await this.xdc.nonceManager.getNextNonce(wallet.address); + } + let tx; + if (maxFeePerGas || maxPriorityFeePerGas) { + tx = await contract[result.methodName](...result.args, { + gasLimit: gasLimit.toFixed(0), + value: result.value, + nonce: nonce, + maxFeePerGas, + maxPriorityFeePerGas, + }); + } else { + tx = await contract[result.methodName](...result.args, { + gasPrice: (gasPrice * 1e9).toFixed(0), + gasLimit: gasLimit.toFixed(0), + value: result.value, + nonce: nonce, + }); + } + + logger.info(tx); + await this.xdc.nonceManager.commitNonce(wallet.address, nonce); + return tx; + } +} \ No newline at end of file diff --git a/src/connectors/xsswap/xsswap_v2_router_abi.json b/src/connectors/xsswap/xsswap_v2_router_abi.json new file mode 100644 index 0000000000..d3ec469d4b --- /dev/null +++ b/src/connectors/xsswap/xsswap_v2_router_abi.json @@ -0,0 +1,975 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_factory", + "type": "address" + }, + { + "internalType": "address", + "name": "_WETH", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "WETH", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountADesired", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountBDesired", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountAMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountBMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "addLiquidity", + "outputs": [ + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountTokenDesired", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "addLiquidityETH", + "outputs": [ + { + "internalType": "uint256", + "name": "amountToken", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveOut", + "type": "uint256" + } + ], + "name": "getAmountIn", + "outputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveOut", + "type": "uint256" + } + ], + "name": "getAmountOut", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + } + ], + "name": "getAmountsIn", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + } + ], + "name": "getAmountsOut", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "reserveB", + "type": "uint256" + } + ], + "name": "quote", + "outputs": [ + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountAMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountBMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "removeLiquidity", + "outputs": [ + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "removeLiquidityETH", + "outputs": [ + { + "internalType": "uint256", + "name": "amountToken", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "removeLiquidityETHSupportingFeeOnTransferTokens", + "outputs": [ + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "approveMax", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "removeLiquidityETHWithPermit", + "outputs": [ + { + "internalType": "uint256", + "name": "amountToken", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountTokenMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountETHMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "approveMax", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "removeLiquidityETHWithPermitSupportingFeeOnTransferTokens", + "outputs": [ + { + "internalType": "uint256", + "name": "amountETH", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountAMin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountBMin", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "approveMax", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "removeLiquidityWithPermit", + "outputs": [ + { + "internalType": "uint256", + "name": "amountA", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountB", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapETHForExactTokens", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactETHForTokens", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactETHForTokensSupportingFeeOnTransferTokens", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactTokensForETH", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactTokensForETHSupportingFeeOnTransferTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactTokensForTokens", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMin", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapExactTokensForTokensSupportingFeeOnTransferTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountInMax", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapTokensForExactETH", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountInMax", + "type": "uint256" + }, + { + "internalType": "address[]", + "name": "path", + "type": "address[]" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "swapTokensForExactTokens", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ] +} \ No newline at end of file diff --git a/src/services/common-interfaces.ts b/src/services/common-interfaces.ts index a9213ce221..a80e6943b5 100644 --- a/src/services/common-interfaces.ts +++ b/src/services/common-interfaces.ts @@ -39,6 +39,12 @@ import { Trade as TradeQuickswap, Fraction as QuickswapFraction, } from 'quickswap-sdk'; +import { + Token as TokenXsswap, + CurrencyAmount as CurrencyAmountXsswap, + Trade as TradeXsswap, + Fraction as XsswapFraction, +} from 'xsswap-sdk'; import { Trade as SushiswapTrade, Token as SushiToken, @@ -99,6 +105,7 @@ export type Tokenish = | TokenPangolin | UniswapCoreToken | TokenQuickswap + | TokenXsswap | TokenTraderjoe | UniswapCoreToken | SushiToken @@ -120,6 +127,7 @@ export type UniswapishTrade = | TradePangolin | UniswapV3Trade | TradeQuickswap + | TradeXsswap | TradeTraderjoe | SushiswapTrade | UniswapV3Trade @@ -142,6 +150,7 @@ export type UniswapishAmount = | CurrencyAmount | CurrencyAmountPangolin | CurrencyAmountQuickswap + | CurrencyAmountXsswap | UniswapCoreCurrencyAmount | CurrencyAmountTraderjoe | SushiCurrencyAmount @@ -154,6 +163,7 @@ export type Fractionish = | UniswapFraction | PangolinFraction | QuickswapFraction + | XsswapFraction | TraderjoeFraction | SushiFraction | DefikingdomsFraction diff --git a/src/services/connection-manager.ts b/src/services/connection-manager.ts index b9ae7d2090..3af48cb655 100644 --- a/src/services/connection-manager.ts +++ b/src/services/connection-manager.ts @@ -10,6 +10,7 @@ import { Openocean } from '../connectors/openocean/openocean'; import { Pangolin } from '../connectors/pangolin/pangolin'; import { Perp } from '../connectors/perp/perp'; import { Quickswap } from '../connectors/quickswap/quickswap'; +import { Xsswap } from '../connectors/xsswap/xsswap'; import { PancakeSwap } from '../connectors/pancakeswap/pancakeswap'; import { Uniswap } from '../connectors/uniswap/uniswap'; import { UniswapLP } from '../connectors/uniswap/uniswap.lp'; @@ -89,6 +90,8 @@ export async function getConnector( connectorInstance = Uniswap.getInstance(chain, network); } else if (chain === 'polygon' && connector === 'quickswap') { connectorInstance = Quickswap.getInstance(chain, network); + } else if (chain === 'xdc' && connector === 'xsswap') { + connectorInstance = Xsswap.getInstance(chain, network); } else if ( (chain === 'ethereum' || chain === 'polygon') && connector === 'uniswapLP' diff --git a/src/services/schema/xsswap-schema.json b/src/services/schema/xsswap-schema.json new file mode 100644 index 0000000000..9505b66fe2 --- /dev/null +++ b/src/services/schema/xsswap-schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "allowedSlippage": { "type": "string" }, + "gasLimitEstimate": { "type": "integer" }, + "ttl": { "type": "integer" }, + "contractAddresses": { + "type": "object", + "patternProperties": { + "^\\w+$": { + "type": "object", + "properties": { + "routerAddress": { "type": "string" } + }, + "required": ["routerAddress"], + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "required": [ + "allowedSlippage", + "gasLimitEstimate", + "ttl", + "contractAddresses" + ] +} diff --git a/src/templates/root.yml b/src/templates/root.yml index 49c622bf08..52c434af01 100644 --- a/src/templates/root.yml +++ b/src/templates/root.yml @@ -39,7 +39,11 @@ configurations: $namespace quickswap: configurationPath: quickswap.yml schemaPath: quickswap-schema.json - + + $namespace xsswap: + configurationPath: xsswap.yml + schemaPath: xsswap-schema.json + $namespace perp: configurationPath: perp.yml schemaPath: perp-schema.json diff --git a/src/templates/xsswap.yml b/src/templates/xsswap.yml new file mode 100644 index 0000000000..931c77c168 --- /dev/null +++ b/src/templates/xsswap.yml @@ -0,0 +1,17 @@ +# allowedSlippage: how much the execution price is allowed to move unfavorably +# from the trade execution price. It uses a rational number for precision. +allowedSlippage: '2/100' + +# the maximum gas used to estimate cost of a xsswap trade. +gasLimitEstimate: 300000 + +# ttl: how long a trade is valid in seconds. After this time passes +# xsswap will not perform the trade, but the gas will still be sent. +ttl: 600 + +contractAddresses: + # constant used for each supported network + xinfin: + routerAddress: '0xf9c5E4f6E627201aB2d6FB6391239738Cf4bDcf9' + apothem: + routerAddress: '0x3F11A24EB45d3c3737365b97A996949dA6c2EdDf' \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index e416b6bd03..75b75bf5e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15043,6 +15043,19 @@ xss@^1.0.8: commander "^2.20.3" cssfilter "0.0.10" +xsswap-sdk@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/xsswap-sdk/-/xsswap-sdk-1.0.1.tgz#39a3b3a5e946b750d8a801a6f1fa78db64b96bbb" + integrity sha512-erBr5eDlzqrxbto+uDkVGE0G5oKZZ05Na3150g8dnEf5zeAnmwl3+ze7cjpeLgCbO2vidCQqgs9dFeGC7KShlw== + dependencies: + "@uniswap/v2-core" "^1.0.0" + big.js "^5.2.2" + decimal.js-light "^2.5.0" + jsbi "^3.1.1" + tiny-invariant "^1.1.0" + tiny-warning "^1.0.3" + toformat "^2.0.0" + xstream@^11.14.0: version "11.14.0" resolved "https://registry.yarnpkg.com/xstream/-/xstream-11.14.0.tgz#2c071d26b18310523b6877e86b4e54df068a9ae5"