synapsecns/sanguine

View on GitHub
packages/synapse-interface/pages/pool/poolManagement/Withdraw.tsx

Summary

Maintainability
A
0 mins
Test Coverage
import _ from 'lodash'
import Grid from '@tw/Grid'
import Slider from 'react-input-slider'
import { useEffect, useMemo, useState } from 'react'
import { waitForTransactionReceipt } from '@wagmi/core'
import { type Address } from 'viem'
import { useTranslations } from 'next-intl'

import { useAppDispatch } from '@/store/hooks'
import { getCoinTextColorCombined } from '@styles/tokens'
import { ALL } from '@constants/withdrawTypes'
import { WithdrawTokenInput } from '@components/TokenInput'
import { approve, withdraw } from '@/utils/actions/approveAndWithdraw'
import { getTokenAllowance } from '@/utils/actions/getTokenAllowance'
import { getSwapDepositContractFields } from '@/utils/getSwapDepositContractFields'
import { calculatePriceImpact } from '@/utils/priceImpact'
import { formatBigIntToString, stringToBigInt } from '@/utils/bigint/format'
import { useSynapseContext } from '@/utils/providers/SynapseProvider'
import { txErrorHandler } from '@/utils/txErrorHandler'
import { isTransactionReceiptError } from '@/utils/isTransactionReceiptError'
import { isTransactionUserRejectedError } from '@/utils/isTransactionUserRejectedError'
import {
  setInputValue,
  setWithdrawQuote,
  setWithdrawType,
  setIsLoading,
  resetPoolWithdraw,
} from '@/slices/poolWithdrawSlice'
import { fetchPoolUserData } from '@/slices/poolUserDataSlice'
import { fetchPoolData } from '@/slices/poolDataSlice'
import { fetchAndStoreSingleNetworkPortfolioBalances } from '@/slices/portfolio/hooks'
import {
  usePoolDataState,
  usePoolUserDataState,
  usePoolWithdrawState,
} from '@/slices/pools/hooks'
import { Token } from '@types'
import RadioButton from '@components/buttons/RadioButton'
import ReceivedTokenSection from '../components/ReceivedTokenSection'
import PriceImpactDisplay from '../components/PriceImpactDisplay'
import WithdrawButton from './WithdrawButton'
import { wagmiConfig } from '@/wagmiConfig'

const Withdraw = ({ address }: { address: string }) => {
  const dispatch = useAppDispatch()
  const { synapseSDK } = useSynapseContext()
  const [percentage, setPercentage] = useState(0)

  const t = useTranslations('Pools')

  const { pool, poolData } = usePoolDataState()
  const { poolUserData } = usePoolUserDataState()
  const { withdrawQuote, inputValue, withdrawType } = usePoolWithdrawState()

  const chainId = pool?.chainId
  const poolDecimals = pool?.decimals[pool?.chainId]
  const { poolAddress } = getSwapDepositContractFields(pool, chainId)

  // An ETH swap pool has nativeTokens vs. most other pools have poolTokens
  const poolSpecificTokens = pool ? pool.nativeTokens ?? pool.poolTokens : []

  const isApproved = useMemo(() => {
    return (
      withdrawQuote?.allowance &&
      stringToBigInt(inputValue, poolDecimals) <= withdrawQuote.allowance
    )
  }, [inputValue, withdrawQuote])

  const calculateMaxWithdraw = async () => {
    if (poolUserData === null || address === null) {
      return
    }
    dispatch(setIsLoading(true))
    try {
      const outputs: Record<
        string,
        {
          value: bigint
          index: number
        }
      > = {}

      const { virtualPrice } = poolData

      if (withdrawType === ALL) {
        const { amounts } = await synapseSDK.calculateRemoveLiquidity(
          chainId,
          poolAddress,
          stringToBigInt(inputValue, poolDecimals)
        )

        outputs[withdrawType] = amounts.map(transformAmount)
      } else {
        const { amount } = await synapseSDK.calculateRemoveLiquidityOne(
          chainId,
          poolAddress,
          stringToBigInt(inputValue, poolDecimals),
          Number(withdrawType)
        )

        outputs[withdrawType] = transformAmount(amount)
      }

      const outputTokensSum = sumBigInts(pool, outputs, withdrawType)

      const priceImpact = calculatePriceImpact(
        stringToBigInt(inputValue, poolDecimals),
        outputTokensSum,
        virtualPrice,
        true
      )

      const allowance = await getTokenAllowance(
        poolAddress,
        pool.addresses[chainId] as Address,
        address as Address,
        chainId
      )
      dispatch(
        setWithdrawQuote({
          priceImpact,
          allowance,
          outputs,
        })
      )
      dispatch(setIsLoading(false))
    } catch (e) {
      dispatch(setIsLoading(false))
      console.log(e)
    }
  }

  useEffect(() => {
    if (
      poolUserData &&
      poolData &&
      address &&
      pool &&
      stringToBigInt(inputValue, poolDecimals) > 0n
    ) {
      calculateMaxWithdraw()
    }
  }, [inputValue, withdrawType])

  const onPercentChange = (percent: number) => {
    if (percent > 100) {
      percent = 100
    }
    setPercentage(percent)
    const numericalOut: string = poolUserData.lpTokenBalance
      ? formatBigIntToString(
          (poolUserData.lpTokenBalance * BigInt(percent)) / BigInt(100),
          poolDecimals
        )
      : ''

    dispatch(setInputValue(numericalOut))
  }

  const onChangeInputValue = (token: Token, value: string) => {
    const bigInt = stringToBigInt(value, token.decimals[chainId])

    if (poolUserData.lpTokenBalance === 0n) {
      dispatch(setInputValue(value))

      setPercentage(0)
      return
    }
    const pn = bigInt
      ? Number(
          (bigInt * BigInt(100)) /
            BigInt(poolUserData.lpTokenBalance.toString())
        )
      : 0

    dispatch(setInputValue(value))

    if (pn > 100) {
      setPercentage(100)
    } else {
      setPercentage(pn)
    }
  }

  const approveTxn = async () => {
    try {
      const tx = approve(
        pool,
        withdrawQuote,
        stringToBigInt(inputValue, poolDecimals),
        chainId
      )

      try {
        await tx
        calculateMaxWithdraw()
      } catch (error) {
        txErrorHandler(error)
      }
    } catch (error) {
      txErrorHandler(error)
    }
  }

  const onResetWithdraw = () => {
    dispatch(fetchPoolUserData({ pool, address: address as Address }))
    dispatch(fetchPoolData({ poolName: String(pool.routerIndex) }))
    dispatch(fetchAndStoreSingleNetworkPortfolioBalances({ address, chainId }))
    dispatch(resetPoolWithdraw())
  }

  const withdrawTxn = async () => {
    try {
      const tx = withdraw(
        pool,
        'ONE_TENTH',
        null,
        stringToBigInt(inputValue, poolDecimals),
        chainId,
        withdrawType,
        withdrawQuote.outputs
      )

      const resolvedTx = await tx

      if (isTransactionUserRejectedError(resolvedTx)) {
        throw Error(resolvedTx)
      }

      await waitForTransactionReceipt(wagmiConfig, {
        hash: resolvedTx?.transactionHash as Address,
        timeout: 60_000,
      })

      onResetWithdraw()
    } catch (error) {
      /**
       * Assume transaction success if transaction receipt error
       * Likely to be rpc related issue
       */
      if (isTransactionReceiptError(error)) {
        onResetWithdraw()
      }
      txErrorHandler(error)
    } finally {
      dispatch(setIsLoading(false))
    }
  }

  return (
    pool && (
      <div>
        <div className="percentage">
          <span className="mr-2 text-white">{t('Withdraw Percentage')} %</span>
          <input
            className={`
              px-2 py-1 w-1/5 rounded-md
              focus:ring-indigo-500 focus:outline-none focus:border-purple-700
              border border-transparent
              bg-[#111111]
              text-gray-300
            `}
            placeholder="0"
            onChange={(e) => onPercentChange(Number(e.currentTarget.value))}
            onFocus={(e) => e.target.select()}
            value={percentage ?? ''}
          />
          <div className="my-2">
            {/* @ts-ignore */}
            <Slider
              axis="x"
              xstep={10}
              xmin={0}
              xmax={100}
              x={percentage ?? 100}
              onChange={(i) => {
                onPercentChange(i.x)
              }}
              styles={{
                track: {
                  backgroundColor: '#E0E7FF',
                  width: '95%',
                },
                active: {
                  backgroundColor: '#B286FF',
                },
                thumb: {
                  backgroundColor: '#CE55FE',
                },
              }}
            />
          </div>
        </div>
        <Grid gap={2} cols={{ xs: 1 }} className="mt-2 mb-4">
          <RadioButton
            checked={withdrawType === ALL}
            onChange={() => {
              dispatch(setWithdrawType(ALL))
            }}
            label="Combo"
            labelClassName={withdrawType === ALL && 'text-indigo-500'}
          />
          {poolSpecificTokens &&
            poolSpecificTokens.map((poolSpecificToken, index) => {
              const checked = withdrawType === index.toString()

              return (
                <RadioButton
                  radioClassName={getCoinTextColorCombined(
                    poolSpecificToken.color
                  )}
                  key={poolSpecificToken?.symbol}
                  checked={checked}
                  onChange={() => {
                    dispatch(setWithdrawType(index.toString()))
                  }}
                  labelClassName={
                    checked &&
                    `${getCoinTextColorCombined(
                      poolSpecificToken.color
                    )} opacity-90`
                  }
                  label={poolSpecificToken.name}
                />
              )
            })}
        </Grid>
        <WithdrawTokenInput
          onChange={(value) => onChangeInputValue(pool, value)}
        />
        <div className="mb-4" />
        <WithdrawButton
          approveTxn={approveTxn}
          withdrawTxn={withdrawTxn}
          isApproved={isApproved}
        />

        {stringToBigInt(inputValue, poolDecimals) > 0n && (
          <div className={` mt-2  bg-bgBase `}>
            <Grid cols={{ xs: 2 }}>
              <div>
                <ReceivedTokenSection
                  poolTokens={poolSpecificTokens}
                  withdrawQuote={withdrawQuote}
                  chainId={chainId}
                />
              </div>
              <div>
                {withdrawQuote.priceImpact && (
                  <PriceImpactDisplay priceImpact={withdrawQuote.priceImpact} />
                )}
              </div>
            </Grid>
          </div>
        )}
      </div>
    )
  )
}

const sumBigInts = (
  pool: Token,
  bigIntMap: Record<string, { value: bigint; index: number }>,
  withdrawType: string
) => {
  if (!pool?.poolTokens) {
    return 0n
  }

  const chainId = pool.chainId

  const currentTokens =
    withdrawType === ALL ? bigIntMap[withdrawType] : bigIntMap

  return pool.poolTokens.reduce((sum, token, index) => {
    if (!currentTokens[index]) {
      return sum
    }

    // Compute the power of 10 using pow10BigInt function
    const scaleFactor = pow10BigInt(
      BigInt(18) - BigInt(token.decimals[chainId])
    )
    const valueToAdd = currentTokens[index].value * scaleFactor

    return sum + valueToAdd
  }, 0n)
}

const pow10BigInt = (n: bigint) => {
  let result = 1n
  for (let i = 0n; i < n; i++) {
    result *= 10n
  }
  return result
}

const transformAmount = (amount) => {
  return {
    value: BigInt(amount.value.toString()),
    index: amount.index,
  }
}

const poolTokenByIndex = (poolTokens: Token[], index: number) => {
  return poolTokens.find((_poolToken, idx) => index === idx)
}

export default Withdraw