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 };
};