# uniswap/index.ts

    /* eslint-disable @typescript-eslint/ban-ts-comment */
    import { ethers, providers } from "ethers";
    import type { CallbackType } from "@evo/utils/ethers/contractHelper";
    import { Token, TokenAmount, Percent, Route, TradeType, Trade, JSBI, CurrencyAmount } from "@evo/libs/uniswap";
    import { Pair } from "@evo/utils/uniswap/uniswapPair";
    import { getAddressByName } from "../../../utils/ethers/addressHelper";
    import { Fetcher } from "@evo/utils/uniswap/uniswapFetcher";
    import { calculateSlippageAmount } from "@evo/utils/uniswap/uniswap";
    import { triggerContractByContractName } from "@evo/utils/ethers/contractHelper";
    import BigNumber from "bignumber.js";
    import { CompactToken, CompactTokenWithAmount, DerivedMintInfo, DerivedBurnInfo } from "../types";
    import { erc20TotalSupply, erc20BalanceOf } from "../erc20";
    import { LandId } from "@evo/config/constants";
    import { TransactionResponse } from "@ethersproject/providers";

    export const uniswapGetWETH = (landId: LandId, chainId: number, symbol = "WETH", name = "Wrapped ether"): Token => {
      const WETHAddress = getAddressByName(landId, "TOKEN_WETH");
      return new Token(chainId, WETHAddress, 18, symbol, name);
    };

    /**
     * Swap Ether to Ring token - Powered by uniswap.
     * @param {string} value - amount for Ring， unit of measurement(wei)
     * @returns {Promise<PromiEvent<any>>}
     */
    export const uniswapBuyRING = async (
      landId: LandId,
      signer: ethers.Signer,
      value: string,
      callback?: CallbackType
    ): Promise<TransactionResponse> => {
      const network = await signer.provider?.getNetwork();

      if (!network) {
        throw Error("no network");
      }

      const RINGAddress = getAddressByName(landId, "TOKEN_RING");

      const RING = new Token(network?.chainId, RINGAddress, 18, "RING", "Darwinia Network Native Token");
      const WETH = uniswapGetWETH(landId, network?.chainId);
      const pair = await Fetcher.fetchPairData(WETH, RING, signer.provider);
      // @ts-ignore
      const route = new Route([pair], WETH);
      const amountIn = value;
      const trade = new Trade(route, new TokenAmount(RING, amountIn), TradeType.EXACT_OUTPUT);
      const slippageTolerance = new Percent("30", "10000"); // 30 bips, or 0.30%

      const amountInMax = trade.maximumAmountIn(slippageTolerance).raw; // needs to be converted to e.g. hex
      const path = [WETH.address, RING.address];
      const to = await signer.getAddress(); // should be a checksummed recipient address
      const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20 minutes from the current Unix time
      const outputAmount = trade.outputAmount.raw; // // needs to be converted to e.g. hex

      return triggerContractByContractName(
        landId,
        signer,
        "uniswapExchange",
        "swapETHForExactTokens",
        [outputAmount.toString(10), path, to, deadline],
        callback,
        {
          value: ethers.BigNumber.from(amountInMax.toString()),
        }
      );
    };

    export const uniswapSellRING = async (
      landId: LandId,
      signer: ethers.Signer,
      value: string,
      callback?: CallbackType
    ): Promise<TransactionResponse> => {
      const network = await signer.provider?.getNetwork();

      if (!network) {
        throw Error("no network");
      }

      const RINGAddress = getAddressByName(landId, "TOKEN_RING");

      const RING = new Token(network?.chainId, RINGAddress, 18, "RING", "Darwinia Network Native Token");
      const WETH = uniswapGetWETH(landId, network?.chainId);

      const pair = await Fetcher.fetchPairData(RING, WETH, signer);
      // @ts-ignore
      const route = new Route([pair], RING);

      const amountIn = value;
      const trade = new Trade(route, new TokenAmount(RING, amountIn), TradeType.EXACT_INPUT);
      const slippageTolerance = new Percent("30", "10000"); // 30 bips, or 0.30%

      const amountOutMin = trade.minimumAmountOut(slippageTolerance).raw; // needs to be converted to e.g. hex
      const path = [RING.address, WETH.address];
      const to = await signer.getAddress(); // should be a checksummed recipient address
      const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20 minutes from the current Unix time
      const inputAmount = trade.inputAmount.raw; // // needs to be converted to e.g. hex

      return triggerContractByContractName(
        landId,
        signer,
        "uniswapExchange",
        "swapExactTokensForETH",
        [inputAmount.toString(10), amountOutMin.toString(10), path, to, deadline],
        callback,
        {
          value: ethers.BigNumber.from(0),
        }
      );
    };

    export const uniswapGetDerivedPairInfo = async (
      provider: ethers.providers.Provider,
      compactTokenA: CompactToken,
      compactTokenB: CompactToken
    ): Promise<Pair> => {
      const network = await provider?.getNetwork();

      // decimals 18 just a random value,is useless, not used in the process of obtaining pair information
      const tokenA = new Token(
        network?.chainId,
        compactTokenA.address,
        compactTokenA.decimals,
        compactTokenA.symbol || "TokenA",
        compactTokenA.name || "TokenA"
      );
      const tokenB = new Token(
        network?.chainId,
        compactTokenB.address,
        compactTokenB.decimals,
        compactTokenB.symbol || "TokenB",
        compactTokenB.name || "TokenB"
      );

      const pair = await Fetcher.fetchPairData(tokenA, tokenB, provider);
      return pair;
    };

    export const uniswapEthToTokenOutputPrice = async (
      landId: LandId,
      provider: ethers.providers.Provider,
      token: CompactToken,
      tokenBought: string
    ): Promise<string[]> => {
      const network = await provider?.getNetwork();

      const RING = new Token(network?.chainId, token.address, token.decimals, token.symbol, token.name);
      const WETH = uniswapGetWETH(landId, network?.chainId);
      const pair = await Fetcher.fetchPairData(WETH, RING, provider);
      // @ts-ignore
      const route = new Route([pair], WETH);
      const amountIn = tokenBought;
      const trade = new Trade(route, new TokenAmount(RING, amountIn), TradeType.EXACT_OUTPUT);
      const slippageTolerance = new Percent("30", "10000");
      const amountInMax = trade.maximumAmountIn(slippageTolerance).raw;

      return [
        new BigNumber(amountInMax.toString(10)).times("1000000000000000000").div(tokenBought).toFixed(0),
        amountInMax.toString(10),
      ];
    };

    export const uniswapTokenToEthInputPrice = async (
      landId: LandId,
      provider: ethers.providers.Provider,
      token: CompactToken,
      tokenBought: string
    ): Promise<string[]> => {
      const network = await provider?.getNetwork();

      const RING = new Token(network?.chainId, token.address, token.decimals, token.symbol, token.name);
      const WETH = uniswapGetWETH(landId, network?.chainId);

      const pair = await Fetcher.fetchPairData(RING, WETH, provider);
      // @ts-ignore
      const route = new Route([pair], RING);
      const amountIn = tokenBought; // 1 WETH
      const trade = new Trade(route, new TokenAmount(RING, amountIn), TradeType.EXACT_INPUT);
      const slippageTolerance = new Percent("30", "10000"); // 30 bips, or 0.30%
      const amountOutMin = trade.minimumAmountOut(slippageTolerance).raw; // needs to be converted to e.g. hex
      return [
        new BigNumber(amountOutMin.toString(10)).times("1000000000000000000").div(tokenBought).toFixed(0),
        amountOutMin.toString(10),
      ];
    };

    export const uniswapAddETHLiquidity = async (
      landId: LandId,
      signer: ethers.Signer,
      tokenA: CompactTokenWithAmount,
      tokenB: CompactTokenWithAmount,
      to: string,
      slippage = 100,
      callback?: CallbackType
    ): Promise<TransactionResponse> => {
      const network = await signer.provider?.getNetwork();

      if (!network || !signer.provider) {
        throw Error("no network");
      }

      const WETH = uniswapGetWETH(landId, network?.chainId);

      const { pair, parsedAmounts } = await uniswapGetDerivedMintInfo(signer.provider, tokenA, tokenB);

      if (!pair || !pair.token0.address || !pair.token1.address) {
        throw Error("no pair");
      }

      const amountsMin: { [x: string]: JSBI } = {
        [pair.token0.address]: calculateSlippageAmount(parsedAmounts[pair.token0.address].raw, slippage)[0],
        [pair.token1.address]: calculateSlippageAmount(parsedAmounts[pair.token1.address].raw, slippage)[0],
      };

      const erc20Token = pair.token0.address.toLowerCase() === WETH.address?.toLowerCase() ? pair.token1 : pair.token0;

      const deadline = Math.floor(Date.now() / 1000) + 60 * 120; // 120 minutes from the current Unix time

      return triggerContractByContractName(
        landId,
        signer,
        "uniswapExchange",
        "addLiquidityETH",
        [
          erc20Token.address,
          parsedAmounts[erc20Token.address].raw.toString(),
          amountsMin[erc20Token.address].toString(),
          amountsMin[WETH.address].toString(),
          to,
          deadline,
        ],
        callback,
        {
          value: ethers.BigNumber.from(parsedAmounts[WETH.address].raw.toString()),
        }
      );
    };

    export const uniswapAddLiquidity = async (
      landId: LandId,
      signer: ethers.Signer,
      tokenA: CompactTokenWithAmount,
      tokenB: CompactTokenWithAmount,
      to: string,
      slippage = 100,
      callback?: CallbackType
    ): Promise<TransactionResponse> => {
      const network = await signer.provider?.getNetwork();

      if (!network || !signer.provider) {
        throw Error("no network");
      }

      const { pair, parsedAmounts } = await uniswapGetDerivedMintInfo(signer.provider, tokenA, tokenB);

      if (!pair || !pair.token0.address || !pair.token1.address) {
        throw Error("no pair");
      }

      const amountsMin: { [x: string]: JSBI } = {
        [pair.token0.address]: calculateSlippageAmount(parsedAmounts[pair.token0.address].raw, slippage)[0],
        [pair.token1.address]: calculateSlippageAmount(parsedAmounts[pair.token1.address].raw, slippage)[0],
      };

      const deadline = Math.floor(Date.now() / 1000) + 60 * 120; // 120 minutes from the current Unix time

      return triggerContractByContractName(
        landId,
        signer,
        "uniswapExchange",
        "addLiquidity",
        [
          pair.token0.address,
          pair.token1.address,
          parsedAmounts[pair.token0.address].raw.toString(),
          parsedAmounts[pair.token1.address].raw.toString(),
          amountsMin[pair.token0.address].toString(),
          amountsMin[pair.token1.address].toString(),
          to,
          deadline,
        ],
        callback
      );
    };

    export const uniswapRemoveLiquidity = async (
      landId: LandId,
      signer: ethers.Signer,
      tokenA: CompactToken,
      tokenB: CompactToken,
      liquidityValue: string,
      to: string,
      slippage = 100,
      callback?: CallbackType
    ): Promise<TransactionResponse> => {
      const network = await signer.provider?.getNetwork();

      if (!network || !signer.provider) {
        throw Error("no network");
      }

      const { pair, parsedAmounts } = await uniswapGetDerivedBurnInfo(
        signer.provider,
        tokenA,
        tokenB,
        ethers.BigNumber.from(liquidityValue),
        to
      );

      if (!pair || !pair.token0.address || !pair.token1.address) {
        throw Error("no pair");
      }

      const amountsMin: { [x: string]: JSBI } = {
        [pair.token0.address]: calculateSlippageAmount(parsedAmounts[pair.token0.address].raw, slippage)[0],
        [pair.token1.address]: calculateSlippageAmount(parsedAmounts[pair.token1.address].raw, slippage)[0],
      };

      const deadline = Math.floor(Date.now() / 1000) + 60 * 120; // 120 minutes from the current Unix time

      return triggerContractByContractName(
        landId,
        signer,
        "uniswapExchange",
        "removeLiquidity",
        [
          pair.token0.address,
          pair.token1.address,
          parsedAmounts[pair.liquidityToken.address].raw.toString(),
          amountsMin[pair.token0.address].toString(),
          amountsMin[pair.token1.address].toString(),
          to,
          deadline,
        ],
        callback
      );
    };

    export const uniswapRemoveETHLiquidity = async (
      landId: LandId,
      signer: ethers.Signer,
      tokenA: CompactToken,
      tokenB: CompactToken,
      liquidityValue: string,
      to: string,
      slippage = 100,
      callback?: CallbackType
    ): Promise<TransactionResponse> => {
      const network = await signer.provider?.getNetwork();

      if (!network || !signer.provider) {
        throw Error("no network");
      }

      const { pair, parsedAmounts } = await uniswapGetDerivedBurnInfo(
        signer.provider,
        tokenA,
        tokenB,
        ethers.BigNumber.from(liquidityValue),
        to
      );
      const WETH = uniswapGetWETH(landId, network?.chainId);

      if (!pair || !pair.token0.address || !pair.token1.address) {
        throw Error("no pair");
      }

      const amountsMin: { [x: string]: JSBI } = {
        [pair.token0.address]: calculateSlippageAmount(parsedAmounts[pair.token0.address].raw, slippage)[0],
        [pair.token1.address]: calculateSlippageAmount(parsedAmounts[pair.token1.address].raw, slippage)[0],
      };

      const erc20Token = pair.token0.address === WETH ? pair.token1 : pair.token0;

      const deadline = Math.floor(Date.now() / 1000) + 60 * 120; // 120 minutes from the current Unix time

      return triggerContractByContractName(
        landId,
        signer,
        "uniswapExchange",
        "removeLiquidityETH",
        [
          erc20Token.address,
          parsedAmounts[pair.liquidityToken.address].raw.toString(),
          amountsMin[erc20Token.address].toString(),
          amountsMin[WETH.address].toString(),
          to,
          deadline,
        ],
        callback
      );
    };

    export const uniswapGetDerivedMintInfo = async (
      provider: ethers.providers.Provider,
      tokenA: CompactTokenWithAmount,
      tokenB: CompactTokenWithAmount
    ): Promise<DerivedMintInfo> => {
      const network = await provider?.getNetwork();

      const pair = await uniswapGetDerivedPairInfo(provider, tokenA, tokenB);
      const totalSupply = new TokenAmount(
        pair.liquidityToken,
        await erc20TotalSupply(provider, pair.liquidityToken.address)
      );

      const independentToken = tokenA.amount.isZero()
        ? {
            token: new Token(network?.chainId, tokenB.address, tokenB.decimals, tokenB.symbol, tokenB.name),
            amount: tokenB.amount,
          }
        : {
            token: new Token(network?.chainId, tokenA.address, tokenA.decimals, tokenA.symbol, tokenA.name),
            amount: tokenA.amount,
          };

      const parsedAmounts = {
        [pair.liquidityToken.address]: totalSupply,
        [pair.token0.address]: new TokenAmount(
          pair.token0,
          independentToken.token.equals(pair.token0)
            ? JSBI.BigInt(independentToken.amount)
            : pair
                .priceOf(independentToken.token)
                // @ts-ignore
                .quote(new CurrencyAmount(independentToken.token, JSBI.BigInt(independentToken.amount))).raw
        ),
        [pair.token1.address]: new TokenAmount(
          pair.token1,
          independentToken.token.equals(pair.token1)
            ? JSBI.BigInt(independentToken.amount)
            : pair
                .priceOf(independentToken.token)
                // @ts-ignore
                .quote(new CurrencyAmount(independentToken.token, JSBI.BigInt(independentToken.amount))).raw
        ),
      };

      return { pair, parsedAmounts };
    };

    export const uniswapGetDerivedBurnInfo = async (
      provider: ethers.providers.Provider,
      tokenA: CompactToken,
      tokenB: CompactToken,
      liquidityValue: ethers.BigNumber,
      to: string
    ): Promise<DerivedBurnInfo> => {
      const pair = await uniswapGetDerivedPairInfo(provider, tokenA, tokenB);

      const lpBalanceStr = await erc20BalanceOf(provider, pair.liquidityToken.address, to);
      const userLiquidity = new TokenAmount(pair.liquidityToken, JSBI.BigInt(lpBalanceStr));
      const totalSupply = new TokenAmount(
        pair.liquidityToken,
        await erc20TotalSupply(provider, pair.liquidityToken.address)
      );

      const liquidityValueA =
        pair &&
        totalSupply &&
        userLiquidity &&
        pair.token0 &&
        new TokenAmount(pair.token0, pair.getLiquidityValue(pair.token0, totalSupply, userLiquidity, false).raw);

      const liquidityValueB =
        pair &&
        totalSupply &&
        userLiquidity &&
        pair.token1 &&
        new TokenAmount(pair.token1, pair.getLiquidityValue(pair.token1, totalSupply, userLiquidity, false).raw);

      const percentToRemove = new Percent(JSBI.BigInt(liquidityValue), userLiquidity.raw);

      const parsedAmounts = {
        [pair.liquidityToken.address]: new TokenAmount(
          userLiquidity.token,
          percentToRemove.multiply(userLiquidity.raw).quotient
        ),
        [pair.token0.address]: new TokenAmount(pair.token0, percentToRemove.multiply(liquidityValueA.raw).quotient),
        [pair.token1.address]: new TokenAmount(pair.token1, percentToRemove.multiply(liquidityValueB.raw).quotient),
      };

      return { pair, parsedAmounts, liquidityPercent: percentToRemove };
    };