lodash#minBy JavaScript Examples

The following examples show how to use lodash#minBy. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: dataProvider.js    From acy-dex-interface with MIT License 4 votes vote down vote up
export function useTradersData({ from = FIRST_DATE_TS, to = NOW_TS, chainName = "arbitrum" } = {}) {
  const [closedPositionsData, loading, error] = useGraph(`{
    tradingStats(
      first: 1000
      orderBy: timestamp
      orderDirection: desc
      where: { period: "daily", timestamp_gte: ${from}, timestamp_lte: ${to} }
    ) {
      timestamp
      profit
      loss
      profitCumulative
      lossCumulative
      longOpenInterest
      shortOpenInterest
    }
  }`, { chainName })
  const [feesData] = useFeesData({ from, to, chainName })
  const marginFeesByTs = useMemo(() => {
    if (!feesData) {
      return {}
    }

    let feesCumulative = 0
    return feesData.reduce((memo, { timestamp, margin: fees}) => {
      feesCumulative += fees
      memo[timestamp] = {
        fees,
        feesCumulative
      }
      return memo
    }, {})
  }, [feesData])

  let ret = null
  const data = closedPositionsData ? sortBy(closedPositionsData.tradingStats, i => i.timestamp).map(dataItem => {
    const longOpenInterest = dataItem.longOpenInterest / 1e30
    const shortOpenInterest = dataItem.shortOpenInterest / 1e30
    const openInterest = longOpenInterest + shortOpenInterest

    const fees = (marginFeesByTs[dataItem.timestamp]?.fees || 0)
    const feesCumulative = (marginFeesByTs[dataItem.timestamp]?.feesCumulative || 0)

    const profit = dataItem.profit / 1e30
    const loss = dataItem.loss / 1e30
    const profitCumulative = dataItem.profitCumulative / 1e30
    const lossCumulative = dataItem.lossCumulative / 1e30
    const pnlCumulative = profitCumulative - lossCumulative
    const pnl = profit - loss
    return {
      longOpenInterest,
      shortOpenInterest,
      openInterest,
      profit,
      loss: -loss,
      profitCumulative,
      lossCumulative: -lossCumulative,
      pnl,
      pnlCumulative,
      timestamp: dataItem.timestamp
    }
  }) : null

  if (data) {
    const maxProfit = maxBy(data, item => item.profit)?.profit ?? 0
    const maxLoss = minBy(data, item => item.loss)?.loss ?? 0
    const maxProfitLoss = Math.max(maxProfit, -maxLoss)

    const maxPnl = maxBy(data, item => item.pnl)?.pnl ?? 0
    const minPnl = minBy(data, item => item.pnl)?.pnl ?? 0
    const maxCumulativePnl = maxBy(data, item => item.pnlCumulative)?.pnlCumulative ?? 0
    const minCumulativePnl = minBy(data, item => item.pnlCumulative)?.pnlCumulative ?? 0

    const profitCumulative = data[data.length - 1]?.profitCumulative ?? 0
    const lossCumulative = data[data.length - 1]?.lossCumulative ?? 0
    const stats = {
      maxProfit,
      maxLoss,
      maxProfitLoss,
      profitCumulative,
      lossCumulative,
      maxCumulativeProfitLoss: Math.max(profitCumulative, -lossCumulative),

      maxAbsOfPnlAndCumulativePnl: Math.max(
        Math.abs(maxPnl),
        Math.abs(maxCumulativePnl),
        Math.abs(minPnl),
        Math.abs(minCumulativePnl)
      ),
    }

    ret = {
      data,
      stats
    }
  }

  return [ret, loading]
}
Example #2
Source File: useSwapEstimator.js    From origin-dollar with MIT License 4 votes vote down vote up
useSwapEstimator = ({
  swapMode,
  inputAmountRaw,
  selectedCoin,
  priceToleranceValue,
}) => {
  const contracts = useStoreState(ContractStore, (s) => s.contracts)
  const chainId = useStoreState(ContractStore, (s) => s.chainId)
  const coinInfoList = useStoreState(ContractStore, (s) => s.coinInfoList)
  const vaultAllocateThreshold = useStoreState(
    ContractStore,
    (s) => s.vaultAllocateThreshold
  )
  const vaultRebaseThreshold = useStoreState(
    ContractStore,
    (s) => s.vaultRebaseThreshold
  )
  const gasPrice = useStoreState(ContractStore, (s) => s.gasPrice)
  const previousGasPrice = usePrevious(gasPrice)
  const isGasPriceUserOverriden = useStoreState(
    ContractStore,
    (s) => s.isGasPriceUserOverriden
  )

  const balances = useStoreState(AccountStore, (s) => s.balances)

  const { contract: coinToSwapContract, decimals: coinToSwapDecimals } =
    coinInfoList[swapMode === 'mint' ? selectedCoin : 'ousd']
  const coinToSwap = swapMode === 'redeem' ? 'ousd' : selectedCoin

  const [selectedCoinPrev, setSelectedCoinPrev] = useState()

  let coinToReceiveContract, coinToReceiveDecimals

  // do not enter conditional body when redeeming a mix
  if (!(swapMode === 'redeem' && selectedCoin === 'mix')) {
    ;({ contract: coinToReceiveContract, decimals: coinToReceiveDecimals } =
      coinInfoList[swapMode === 'redeem' ? selectedCoin : 'ousd'])
  }

  const allowances = useStoreState(AccountStore, (s) => s.allowances)
  const allowancesLoaded =
    typeof allowances === 'object' &&
    allowances.ousd !== undefined &&
    allowances.usdt !== undefined &&
    allowances.usdc !== undefined &&
    allowances.dai !== undefined

  const account = useStoreState(AccountStore, (s) => s.account)
  const [ethPrice, setEthPrice] = useState(false)
  const [estimationCallback, setEstimationCallback] = useState(null)
  const {
    mintVaultGasEstimate,
    swapUniswapGasEstimate,
    swapCurveGasEstimate,
    swapUniswapV2GasEstimate,
    swapUniswapV2,
    swapCurve,
    quoteUniswap,
    quoteUniswapV2,
    quoteSushiSwap,
    swapSushiswapGasEstimate,
    quoteCurve,
    redeemVaultGasEstimate,
  } = useCurrencySwapper({
    swapMode,
    inputAmountRaw,
    selectedCoin,
    priceToleranceValue,
  })

  const { swapAmount, minSwapAmount } = calculateSwapAmounts(
    inputAmountRaw,
    coinToSwapDecimals,
    priceToleranceValue
  )

  const swapEstimations = useStoreState(ContractStore, (s) => s.swapEstimations)
  const walletConnected = useStoreState(ContractStore, (s) => s.walletConnected)

  useEffect(() => {
    const swapsLoaded = swapEstimations && typeof swapEstimations === 'object'
    const userSelectionExists =
      swapsLoaded &&
      find(
        Object.values(swapEstimations),
        (estimation) => estimation.userSelected
      )

    const selectedSwap =
      swapsLoaded &&
      find(Object.values(swapEstimations), (estimation) =>
        userSelectionExists ? estimation.userSelected : estimation.isBest
      )

    ContractStore.update((s) => {
      s.selectedSwap = selectedSwap
    })
  }, [swapEstimations])

  // just so initial gas price is populated in the settings dropdown
  useEffect(() => {
    fetchGasPrice()
  }, [])

  useEffect(() => {
    /*
     * Weird race condition would happen where estimations were ran with the utils/contracts setting up
     * the contracts with alchemy provider instead of Metamask one. When estimations are ran with that
     * setup, half of the estimations fail with an error.
     */

    /*
     * When function is triggered because of a non user change in gas price, ignore the trigger.
     */
    if (!isGasPriceUserOverriden && previousGasPrice !== gasPrice) {
      return
    }

    if (estimationCallback) {
      clearTimeout(estimationCallback)
    }

    const coinAmountNumber = parseFloat(inputAmountRaw)
    if (!(coinAmountNumber > 0) || Number.isNaN(coinAmountNumber)) {
      ContractStore.update((s) => {
        s.swapEstimations = null
      })
      return
    }

    /* Timeout the execution so it doesn't happen on each key stroke rather aiming
     * to when user has already stopped typing
     */
    const delay = selectedCoin !== selectedCoinPrev ? 0 : 700
    // reset swap estimations here for better UI experience
    if (delay === 0) {
      ContractStore.update((s) => {
        s.swapEstimations = 'loading'
      })
    }
    setEstimationCallback(
      setTimeout(async () => {
        await runEstimations(swapMode, selectedCoin, inputAmountRaw)
      }, delay)
    )
    setSelectedCoinPrev(selectedCoin)
  }, [
    swapMode,
    selectedCoin,
    inputAmountRaw,
    allowancesLoaded,
    walletConnected,
    isGasPriceUserOverriden,
    gasPrice,
  ])

  const gasLimitForApprovingCoin = (coin) => {
    return approveCoinGasLimits[coin]
  }

  const runEstimations = async (mode, selectedCoin, amount) => {
    ContractStore.update((s) => {
      s.swapEstimations = 'loading'
    })
    let usedGasPrice = gasPrice

    let vaultResult,
      flipperResult,
      uniswapResult,
      uniswapV2Result,
      sushiswapResult,
      curveResult,
      ethPrice
    if (swapMode === 'mint') {
      ;[
        vaultResult,
        flipperResult,
        uniswapResult,
        uniswapV2Result,
        sushiswapResult,
        curveResult,
        ethPrice,
      ] = await Promise.all([
        estimateMintSuitabilityVault(),
        estimateSwapSuitabilityFlipper(),
        estimateSwapSuitabilityUniswapV3(),
        estimateSwapSuitabilityUniswapV2(),
        estimateSwapSuitabilitySushiSwap(),
        estimateSwapSuitabilityCurve(),
        fetchEthPrice(),
      ])
    } else {
      ;[
        vaultResult,
        flipperResult,
        uniswapResult,
        uniswapV2Result,
        sushiswapResult,
        curveResult,
        ethPrice,
      ] = await Promise.all([
        estimateRedeemSuitabilityVault(),
        estimateSwapSuitabilityFlipper(),
        estimateSwapSuitabilityUniswapV3(),
        estimateSwapSuitabilityUniswapV2(),
        estimateSwapSuitabilitySushiSwap(),
        estimateSwapSuitabilityCurve(),
        fetchEthPrice(),
      ])
    }

    if (!isGasPriceUserOverriden) {
      usedGasPrice = await fetchGasPrice()
    }

    let estimations = {
      vault: vaultResult,
      flipper: flipperResult,
      uniswap: uniswapResult,
      curve: curveResult,
      uniswapV2: uniswapV2Result,
      sushiswap: sushiswapResult,
    }

    estimations = enrichAndFindTheBest(
      estimations,
      usedGasPrice,
      ethPrice,
      amount
    )

    ContractStore.update((s) => {
      s.swapEstimations = estimations
    })
  }

  const enrichAndFindTheBest = (
    estimations,
    gasPrice,
    ethPrice,
    inputAmountRaw
  ) => {
    Object.keys(estimations).map((estKey) => {
      const value = estimations[estKey]
      // assign names to values, for easier manipulation
      value.name = estKey
      value.isBest = false
      value.userSelected = false

      estimations[estKey] = value
    })

    const canDoSwaps = Object.values(estimations).filter(
      (estimation) => estimation.canDoSwap
    )

    const inputAmount = parseFloat(inputAmountRaw)
    canDoSwaps.map((estimation) => {
      const gasUsdCost = getGasUsdCost(estimation.gasUsed, gasPrice, ethPrice)
      const gasUsdCostNumber = parseFloat(gasUsdCost)
      const amountReceivedNumber = parseFloat(estimation.amountReceived)

      estimation.gasEstimate = gasUsdCost
      if (estimation.approveAllowanceNeeded) {
        estimation.gasEstimateSwap = getGasUsdCost(
          estimation.swapGasUsage,
          gasPrice,
          ethPrice
        )
        estimation.gasEstimateApprove = getGasUsdCost(
          estimation.approveGasUsage,
          gasPrice,
          ethPrice
        )
      }
      estimation.effectivePrice =
        (inputAmount + gasUsdCostNumber) / amountReceivedNumber
    })

    const best = minBy(canDoSwaps, (estimation) => estimation.effectivePrice)

    if (best) {
      best.isBest = true
      canDoSwaps.map((estimation) => {
        if (estimation === best) {
          return
        }

        estimation.diff = estimation.effectivePrice - best.effectivePrice
        estimation.diffPercentage =
          ((best.effectivePrice - estimation.effectivePrice) /
            best.effectivePrice) *
          100
      })
    }

    return estimations
  }

  const getGasUsdCost = (gasLimit, gasPrice, ethPrice) => {
    if (!gasPrice || !ethPrice) {
      return null
    }

    const flooredEth = Math.floor(ethPrice)
    const priceInUsd = ethers.utils.formatUnits(
      gasPrice
        .mul(BigNumber.from(flooredEth))
        .mul(BigNumber.from(gasLimit))
        .toString(),
      18
    )

    return priceInUsd
  }

  const userHasEnoughStablecoin = (coin, swapAmount) => {
    return parseFloat(balances[coin]) > swapAmount
  }

  /* Gives information on suitability of flipper for this swap
   */
  const estimateSwapSuitabilityFlipper = async () => {
    const amount = parseFloat(inputAmountRaw)
    if (amount > 25000) {
      return {
        canDoSwap: false,
        error: 'amount_too_high',
      }
    }

    if (swapMode === 'redeem' && selectedCoin === 'mix') {
      return {
        canDoSwap: false,
        error: 'unsupported',
      }
    }

    const coinToReceiveBn = ethers.utils.parseUnits(
      amount.toString(),
      coinToReceiveDecimals
    )

    const contractCoinBalance = await coinToReceiveContract.balanceOf(
      contracts.flipper.address
    )

    if (contractCoinBalance.lt(coinToReceiveBn)) {
      return {
        canDoSwap: false,
        error: 'not_enough_funds_contract',
      }
    }

    const approveAllowanceNeeded = allowancesLoaded
      ? parseFloat(allowances[coinToSwap].flipper) === 0
      : true
    const swapGasUsage = 90000
    const approveGasUsage = approveAllowanceNeeded
      ? gasLimitForApprovingCoin(coinToSwap)
      : 0
    return {
      // gasLimitForApprovingCoin
      canDoSwap: true,
      gasUsed: swapGasUsage + approveGasUsage,
      swapGasUsage,
      approveGasUsage,
      approveAllowanceNeeded,
      amountReceived: amount,
    }
  }

  /* Gives information on suitability of Curve for this swap
   */
  const estimateSwapSuitabilityCurve = async () => {
    const isRedeem = swapMode === 'redeem'
    if (isRedeem && selectedCoin === 'mix') {
      return {
        canDoSwap: false,
        error: 'unsupported',
      }
    }

    try {
      const priceQuoteBn = await quoteCurve(swapAmount)
      const amountReceived = ethers.utils.formatUnits(
        priceQuoteBn,
        // 18 because ousd has 18 decimals
        isRedeem ? coinToReceiveDecimals : 18
      )

      const approveAllowanceNeeded = allowancesLoaded
        ? parseFloat(allowances[coinToSwap].curve) === 0
        : true

      /* Check if Curve router has allowance to spend coin. If not we can not run gas estimation and need
       * to guess the gas usage.
       *
       * We don't check if positive amount is large enough: since we always approve max_int allowance.
       */
      if (
        approveAllowanceNeeded ||
        !userHasEnoughStablecoin(coinToSwap, parseFloat(inputAmountRaw))
      ) {
        const swapGasUsage = 350000
        const approveGasUsage = approveAllowanceNeeded
          ? gasLimitForApprovingCoin(coinToSwap)
          : 0
        return {
          canDoSwap: true,
          /* This estimate is from the few ones observed on the mainnet:
           * https://etherscan.io/tx/0x3ff7178d8be668649928d86863c78cd249224211efe67f23623017812e7918bb
           * https://etherscan.io/tx/0xbf033ffbaf01b808953ca1904d3b0110b50337d60d89c96cd06f3f9a6972d3ca
           * https://etherscan.io/tx/0x77d98d0307b53e81f50b39132e038a1c6ef87a599a381675ce44038515a04738
           * https://etherscan.io/tx/0xbce1a2f1e76d4b4f900b3952f34f5f53f8be4a65ccff348661d19b9a3827aa04
           *
           */
          gasUsed: swapGasUsage + approveGasUsage,
          swapGasUsage,
          approveGasUsage,
          approveAllowanceNeeded,
          amountReceived,
        }
      }

      const {
        swapAmount: swapAmountQuoted,
        minSwapAmount: minSwapAmountQuoted,
      } = calculateSwapAmounts(
        amountReceived,
        coinToReceiveDecimals,
        priceToleranceValue
      )

      const gasEstimate = await swapCurveGasEstimate(
        swapAmount,
        minSwapAmountQuoted
      )

      return {
        canDoSwap: true,
        gasUsed: gasEstimate,
        amountReceived,
      }
    } catch (e) {
      console.error(
        `Unexpected error estimating curve swap suitability: ${e.message}`
      )
      return {
        canDoSwap: false,
        error: 'unexpected_error',
      }
    }
  }

  const estimateSwapSuitabilityUniswapV2 = async () => {
    return _estimateSwapSuitabilityUniswapV2Variant(false)
  }

  const estimateSwapSuitabilitySushiSwap = async () => {
    return _estimateSwapSuitabilityUniswapV2Variant(true)
  }

  // Gives information on suitability of uniswapV2 / SushiSwap for this swap
  const _estimateSwapSuitabilityUniswapV2Variant = async (
    isSushiSwap = false
  ) => {
    const isRedeem = swapMode === 'redeem'
    if (isRedeem && selectedCoin === 'mix') {
      return {
        canDoSwap: false,
        error: 'unsupported',
      }
    }

    try {
      const priceQuoteValues = isSushiSwap
        ? await quoteSushiSwap(swapAmount)
        : await quoteUniswapV2(swapAmount)
      const priceQuoteBn = priceQuoteValues[priceQuoteValues.length - 1]

      // 18 because ousd has 18 decimals
      const amountReceived = ethers.utils.formatUnits(
        priceQuoteBn,
        isRedeem ? coinToReceiveDecimals : 18
      )

      /* Check if Uniswap router has allowance to spend coin. If not we can not run gas estimation and need
       * to guess the gas usage.
       *
       * We don't check if positive amount is large enough: since we always approve max_int allowance.
       */
      const requiredAllowance = allowancesLoaded
        ? allowances[coinToSwap][
            isSushiSwap ? 'sushiRouter' : 'uniswapV2Router'
          ]
        : 0

      if (requiredAllowance === undefined) {
        throw new Error('Can not find correct allowance for coin')
      }

      const approveAllowanceNeeded = parseFloat(requiredAllowance) === 0

      if (
        approveAllowanceNeeded ||
        !userHasEnoughStablecoin(coinToSwap, parseFloat(inputAmountRaw))
      ) {
        const swapGasUsage = selectedCoin === 'usdt' ? 175000 : 230000
        const approveGasUsage = approveAllowanceNeeded
          ? gasLimitForApprovingCoin(coinToSwap)
          : 0
        return {
          canDoSwap: true,
          /* Some example Uniswap transactions. When 2 swaps are done:
           * - https://etherscan.io/tx/0x436ef157435c93241257fb0b347db7cc1b2c4f73d749c7e5c1181393f3d0aa26
           * - https://etherscan.io/tx/0x504799fecb64a0452f5635245ca313aa5612132dc6fe66c441b61fd98a0e0766
           * - https://etherscan.io/tx/0x2e3429fb9f04819a55f85cfdbbaf78dfbb049bff85be84a324650d77ff98dfc3
           *
           * And Uniswap when 1 swap:
           * - https://etherscan.io/tx/0x6ceca6c6c2a829928bbf9cf97a018b431def8e475577fcc7cc97ed6bd35f9f7b
           * - https://etherscan.io/tx/0x02c1fffb94b06d54e0c6d47da460cb6e5e736e43f928b7e9b2dcd964b1390188
           * - https://etherscan.io/tx/0xe5a35025ec3fe71ece49a4311319bdc16302b7cc16b3e7a95f0d8e45baa922c7
           *
           * Some example Sushiswap transactions. When 2 swaps are done:
           * - https://etherscan.io/tx/0x8e66d8d682b8028fd44c916d4318fee7e69704e9f8e386dd7debbfe3157375c5
           * - https://etherscan.io/tx/0xbb837c5f001a0d71c75db49ddc22bd75b7800e426252ef1f1135e8e543769bea
           * - https://etherscan.io/tx/0xe00ab2125b55fd398b00e361e2fd22f6fc9225e609fb2bb2b712586523c89824
           * - https://etherscan.io/tx/0x5c26312ac2bab17aa8895592faa8dc8607f15912de953546136391ee2e955e92
           *
           * And Sushiswap when 1 swap:
           * - https://etherscan.io/tx/0xa8a0c5d2433bcb6ddbfdfb1db7c55c674714690e353f305e4f3c72878ab6a3a7
           * - https://etherscan.io/tx/0x8d2a273d0451ab48c554f8a97d333f7f62b40804946cbd546dc57e2c009514f0
           *
           * Both contracts have very similar gas usage (since they share a lot of the code base)
           */
          gasUsed: swapGasUsage + approveGasUsage,
          swapGasUsage,
          approveGasUsage,
          approveAllowanceNeeded,
          amountReceived,
        }
      }

      const {
        swapAmount: swapAmountQuoted,
        minSwapAmount: minSwapAmountQuoted,
      } = calculateSwapAmounts(
        amountReceived,
        coinToReceiveDecimals,
        priceToleranceValue
      )

      let gasEstimate

      if (isSushiSwap) {
        gasEstimate = await swapSushiswapGasEstimate(
          swapAmount,
          minSwapAmountQuoted
        )
      } else {
        gasEstimate = await swapUniswapV2GasEstimate(
          swapAmount,
          minSwapAmountQuoted
        )
      }

      return {
        canDoSwap: true,
        gasUsed: gasEstimate,
        amountReceived,
      }
    } catch (e) {
      console.error(
        `Unexpected error estimating ${
          isSushiSwap ? 'sushiSwap' : 'uniswap v2'
        } swap suitability: ${e.message}`
      )

      if (
        (e.data &&
          e.data.message &&
          e.data.message.includes('INSUFFICIENT_OUTPUT_AMOUNT')) ||
        e.message.includes('INSUFFICIENT_OUTPUT_AMOUNT')
      ) {
        return {
          canDoSwap: false,
          error: 'slippage_too_high',
        }
      }

      return {
        canDoSwap: false,
        error: 'unexpected_error',
      }
    }
  }

  /* Gives information on suitability of uniswap for this swap
   */
  const estimateSwapSuitabilityUniswapV3 = async () => {
    const isRedeem = swapMode === 'redeem'
    if (isRedeem && selectedCoin === 'mix') {
      return {
        canDoSwap: false,
        error: 'unsupported',
      }
    }

    try {
      const priceQuote = await quoteUniswap(swapAmount)
      const priceQuoteBn = BigNumber.from(priceQuote)
      // 18 because ousd has 18 decimals
      const amountReceived = ethers.utils.formatUnits(
        priceQuoteBn,
        isRedeem ? coinToReceiveDecimals : 18
      )

      /* Check if Uniswap router has allowance to spend coin. If not we can not run gas estimation and need
       * to guess the gas usage.
       *
       * We don't check if positive amount is large enough: since we always approve max_int allowance.
       */
      if (
        !allowancesLoaded ||
        parseFloat(allowances[coinToSwap].uniswapV3Router) === 0 ||
        !userHasEnoughStablecoin(coinToSwap, parseFloat(inputAmountRaw))
      ) {
        const approveAllowanceNeeded = allowancesLoaded
          ? parseFloat(allowances[coinToSwap].uniswapV3Router) === 0
          : true
        const approveGasUsage = approveAllowanceNeeded
          ? gasLimitForApprovingCoin(coinToSwap)
          : 0
        const swapGasUsage = 165000
        return {
          canDoSwap: true,
          /* This estimate is over the maximum one appearing on mainnet: https://etherscan.io/tx/0x6b1163b012570819e2951fa95a8287ce16be96b8bf18baefb6e738d448188ed5
           * Swap gas costs are usually between 142k - 162k
           *
           * Other transactions here: https://etherscan.io/tokentxns?a=0x129360c964e2e13910d603043f6287e5e9383374&p=6
           */
          // TODO: if usdc / dai are selected it will cost more gas
          gasUsed: swapGasUsage + approveGasUsage,
          approveAllowanceNeeded,
          swapGasUsage,
          approveGasUsage,
          amountReceived,
        }
      }

      const {
        swapAmount: swapAmountQuoted,
        minSwapAmount: minSwapAmountQuoted,
      } = calculateSwapAmounts(
        amountReceived,
        coinToReceiveDecimals,
        priceToleranceValue
      )

      const gasEstimate = await swapUniswapGasEstimate(
        swapAmount,
        minSwapAmountQuoted
      )

      return {
        canDoSwap: true,
        gasUsed: gasEstimate,
        amountReceived,
      }
    } catch (e) {
      console.error(
        `Unexpected error estimating uniswap v3 swap suitability: ${e.message}`
      )

      // local node and mainnet return errors in different formats, this handles both cases
      if (
        (e.data &&
          e.data.message &&
          e.data.message.includes('Too little received')) ||
        e.message.includes('Too little received')
      ) {
        return {
          canDoSwap: false,
          error: 'slippage_too_high',
        }
      }

      return {
        canDoSwap: false,
        error: 'unexpected_error',
      }
    }
  }

  /* Gives information on suitability of vault mint
   */
  const estimateMintSuitabilityVault = async () => {
    const amount = parseFloat(inputAmountRaw)

    try {
      // 18 decimals denominated BN exchange rate value
      const oracleCoinPrice = await contracts.vault.priceUSDMint(
        coinToSwapContract.address
      )
      const amountReceived =
        amount * parseFloat(ethers.utils.formatUnits(oracleCoinPrice, 18))

      const approveAllowanceNeeded = allowancesLoaded
        ? parseFloat(allowances[coinToSwap].vault) === 0
        : true
      // Check if Vault has allowance to spend coin.
      if (
        approveAllowanceNeeded ||
        !userHasEnoughStablecoin(selectedCoin, amount)
      ) {
        const rebaseTreshold = parseFloat(
          ethers.utils.formatUnits(vaultRebaseThreshold, 18)
        )
        const allocateThreshold = parseFloat(
          ethers.utils.formatUnits(vaultAllocateThreshold, 18)
        )

        let swapGasUsage = 220000
        if (amount > allocateThreshold) {
          // https://etherscan.io/tx/0x267da9abae04ae600d33d2c3e0b5772872e6138eaa074ce715afc8975c7f2deb
          swapGasUsage = 2900000
        } else if (amount > rebaseTreshold) {
          // https://etherscan.io/tx/0xc8ac03e33cab4bad9b54a6e6604ef6b8e11126340b93bbca1348167f548ad8fd
          swapGasUsage = 510000
        }

        const approveGasUsage = approveAllowanceNeeded
          ? gasLimitForApprovingCoin(coinToSwap)
          : 0
        return {
          canDoSwap: true,
          gasUsed: swapGasUsage + approveGasUsage,
          swapGasUsage,
          approveGasUsage,
          approveAllowanceNeeded,
          amountReceived,
        }
      }

      const { minSwapAmount: minAmountReceived } = calculateSwapAmounts(
        amountReceived,
        coinToReceiveDecimals,
        priceToleranceValue
      )

      const gasEstimate = await mintVaultGasEstimate(
        swapAmount,
        minAmountReceived
      )

      return {
        canDoSwap: true,
        gasUsed: gasEstimate,
        // TODO: should this be rather done with BigNumbers instead?
        amountReceived,
      }
    } catch (e) {
      console.error(
        `Unexpected error estimating vault swap suitability: ${e.message}`
      )

      // local node and mainnet return errors in different formats, this handles both cases
      if (
        (e.data &&
          e.data.message &&
          e.data.message.includes('Mint amount lower than minimum')) ||
        e.message.includes('Mint amount lower than minimum')
      ) {
        return {
          canDoSwap: false,
          error: 'slippage_too_high',
        }
      }

      return {
        canDoSwap: false,
        error: 'unexpected_error',
      }
    }
  }

  /* Gives information on suitability of vault redeem
   */
  const estimateRedeemSuitabilityVault = async () => {
    if (selectedCoin !== 'mix') {
      return {
        canDoSwap: false,
        error: 'unsupported',
      }
    }

    const amount = parseFloat(inputAmountRaw)
    // Check if Vault has allowance to spend coin.

    let gasEstimate
    try {
      await loadRedeemFee()
      const coinSplits = await _calculateSplits(amount)
      const splitsSum = coinSplits
        .map((coin) => parseFloat(coin.amount))
        .reduce((a, b) => a + b, 0)

      if (!userHasEnoughStablecoin('ousd', amount)) {
        return {
          canDoSwap: true,
          gasUsed: 1500000,
          amountReceived: splitsSum,
          coinSplits,
        }
      }

      const { minSwapAmount: minAmountReceived } = calculateSwapAmounts(
        splitsSum,
        coinToReceiveDecimals,
        priceToleranceValue
      )

      gasEstimate = await redeemVaultGasEstimate(swapAmount, minAmountReceived)

      return {
        canDoSwap: true,
        gasUsed: gasEstimate,
        // TODO: should this be rather done with BigNumbers instead?
        amountReceived: splitsSum,
        coinSplits,
      }
    } catch (e) {
      console.error(`Can not estimate contract call gas used: ${e.message}`)

      const errorIncludes = (errorTxt) => {
        return (
          (e.data && e.data.message && e.data.message.includes(errorTxt)) ||
          e.message.includes(errorTxt)
        )
      }

      // local node and mainnet return errors in different formats, this handles both cases
      if (errorIncludes('Redeem amount lower than minimum')) {
        return {
          canDoSwap: false,
          error: 'slippage_too_high',
        }
        /* Various error messages strategies emit when too much funds attempt to
         * be withdrawn:
         * - "Redeem failed" -> Compound strategy
         * - "5" -> Aave
         * - "Insufficient 3CRV balance" -> Convex
         */
      } else if (
        errorIncludes('Redeem failed') ||
        errorIncludes(`reverted with reason string '5'`) ||
        errorIncludes('Insufficient 3CRV balance')
      ) {
        return {
          canDoSwap: false,
          error: 'liquidity_error',
        }
      }

      return {
        canDoSwap: false,
        error: 'unexpected_error',
      }
    }
  }

  let redeemFee
  const loadRedeemFee = async () => {
    if (!redeemFee) {
      const redeemFeeBn = await contracts.vault.redeemFeeBps()
      redeemFee = parseFloat(ethers.utils.formatUnits(redeemFeeBn, 4))
    }
  }

  // Fetches current eth price
  const fetchEthPrice = async () => {
    // if production
    if (chainId === 1) {
      return await _fetchEthPriceChainlink()
    } else {
      return await _fetchEthPriceCryptoApi()
    }
  }

  const _fetchEthPriceCryptoApi = async () => {
    try {
      const ethPriceRequest = await fetch(
        'https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD'
      )

      // floor so we can convert to BN without a problem
      const ethPrice = BigNumber.from(
        Math.floor(get(await ethPriceRequest.json(), 'USD'))
      )
      setEthPrice(ethPrice)
      return ethPrice
    } catch (e) {
      console.error(`Can not fetch eth prices: ${e.message}`)
    }

    return BigNumber.from(0)
  }

  const _fetchGasPriceChainlink = async () => {
    if (chainId !== 1) {
      throw new Error('Chainlink fast gas supported only on mainnet')
    }

    try {
      const priceFeed =
        await contracts.chainlinkFastGasAggregator.latestRoundData()

      if (!isGasPriceUserOverriden) {
        ContractStore.update((s) => {
          s.gasPrice = priceFeed.answer
        })
      }
      return priceFeed.answer
    } catch (e) {
      console.error('Error happened fetching fast gas chainlink data:', e)
    }

    return BigNumber.from(0)
  }

  const _fetchEthPriceChainlink = async () => {
    try {
      const priceFeed = await contracts.chainlinkEthAggregator.latestRoundData()
      const ethUsdPrice = parseFloat(
        ethers.utils.formatUnits(priceFeed.answer, 8)
      )
      return ethUsdPrice
    } catch (e) {
      console.error('Error happened fetching eth usd chainlink data:', e)
    }

    return 0
  }

  // Fetches current gas price
  const fetchGasPrice = async () => {
    try {
      const gasPriceRequest = await fetchWithTimeout(
        `https://ethgasstation.info/api/ethgasAPI.json?api-key=${process.env.DEFI_PULSE_API_KEY}`,
        // allow for 5 seconds timeout before falling back to chainlink
        {
          timeout: 5000,
        }
      )

      const gasPrice = BigNumber.from(
        get(await gasPriceRequest.json(), 'average') + '00000000'
      )

      if (!isGasPriceUserOverriden) {
        ContractStore.update((s) => {
          s.gasPrice = gasPrice
        })
      }
      return gasPrice
    } catch (e) {
      console.error(
        `Can not fetch gas prices, using chainlink as fallback method: ${e.message}`
      )
    }

    // fallback to chainlink
    return await _fetchGasPriceChainlink()
  }

  const _calculateSplits = async (sellAmount) => {
    const calculateIt = async () => {
      try {
        const assetAmounts = await contracts.vault.calculateRedeemOutputs(
          ethers.utils.parseUnits(sellAmount.toString(), 18)
        )

        const assets = await Promise.all(
          (
            await contracts.vault.getAllAssets()
          ).map(async (address, index) => {
            const coin = Object.keys(contracts).find(
              (coin) =>
                contracts[coin] &&
                contracts[coin].address.toLowerCase() === address.toLowerCase()
            )

            const amount = ethers.utils.formatUnits(
              assetAmounts[index],
              coinInfoList[coin].decimals
            )

            return {
              coin,
              amount,
            }
          })
        )

        return assets
      } catch (err) {
        console.error(err)
        return {}
      }
    }

    return await calculateIt()
  }

  return {
    estimateSwapSuitabilityFlipper,
    estimateMintSuitabilityVault,
    estimateRedeemSuitabilityVault,
    estimateSwapSuitabilityUniswapV3,
    estimateSwapSuitabilityCurve,
  }
}