synapsecns/sanguine

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

Summary

Maintainability
A
0 mins
Test Coverage
import _ from 'lodash'
import { ETH, WETHE, WETH } from '@constants/tokens/bridgeable'
import { AVWETH } from '@/constants/tokens/auxilliary'
import { stringToBigInt, formatBigIntToString } from '@/utils/bigint/format'
import { getTokenAllowance } from '@/utils/actions/getTokenAllowance'
import {
  approve,
  deposit,
  emptyPoolDeposit,
} from '@/utils/actions/approveAndDeposit'
import {
  fetchBalance,
  getBalance,
  waitForTransaction,
  waitForTransactionReceipt,
} from '@wagmi/core'
import { getSwapDepositContractFields } from '@/utils/getSwapDepositContractFields'
import { calculatePriceImpact } from '@/utils/priceImpact'
import { transformCalculateLiquidityInput } from '@/utils/transformCalculateLiquidityInput'
import { isTransactionReceiptError } from '@/utils/isTransactionReceiptError'
import { isTransactionUserRejectedError } from '@/utils/isTransactionUserRejectedError'
import { useState, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { DepositTokenInput } from '@components/TokenInput'
import { Token } from '@types'

import {
  resetPoolDeposit,
  setDepositQuote,
  setInputValue,
  setIsLoading,
  setPool,
} from '@/slices/poolDepositSlice'
import { fetchPoolData } from '@/slices/poolDataSlice'
import { fetchPoolUserData } from '@/slices/poolUserDataSlice'
import { fetchAndStoreSingleNetworkPortfolioBalances } from '@/slices/portfolio/hooks'
import {
  usePoolDataState,
  usePoolUserDataState,
  usePoolDepositState,
} from '@/slices/pools/hooks'
import { swapPoolCalculateAddLiquidity } from '@/actions/swapPoolCalculateAddLiquidity'
import { txErrorHandler } from '@/utils/txErrorHandler'
import LoadingTokenInput from '@components/loading/LoadingTokenInput'
import PriceImpactDisplay from '../components/PriceImpactDisplay'
import DepositButton from './DepositButton'
import { type Address, getAddress, zeroAddress } from 'viem'
import { wagmiConfig } from '@/wagmiConfig'

export const DEFAULT_DEPOSIT_QUOTE = {
  priceImpact: 0n,
  allowances: {},
  routerAddress: '',
}

const Deposit = ({
  chainId,
  address,
}: {
  chainId: number
  address: string
}) => {
  const dispatch: any = useDispatch()

  const { pool, poolData } = usePoolDataState()
  const { poolUserData } = usePoolUserDataState()
  const { depositQuote, inputValue, inputSum, filteredInputValue } =
    usePoolDepositState()

  const { poolAddress } = getSwapDepositContractFields(pool, chainId)

  dispatch(setPool(pool))

  const calculateMaxDeposits = async () => {
    try {
      if (poolUserData == null || address == null) {
        return
      }
      dispatch(setIsLoading(true))
      const { totalLocked, virtualPrice } = poolData

      if (totalLocked > 0 && inputSum > 0n) {
        const input = transformCalculateLiquidityInput(
          chainId,
          pool,
          filteredInputValue.bi
        )

        const inputs: bigint[] = []

        Object.keys(input).forEach((key) => inputs.push(input[key]))

        const amount = await swapPoolCalculateAddLiquidity({
          chainId,
          pool,
          inputs,
        })

        let allowances: Record<string, bigint> = {}
        for (const [tokenAddress, value] of Object.entries(
          filteredInputValue.bi
        )) {
          allowances[tokenAddress] = await getTokenAllowance(
            poolAddress,
            tokenAddress as Address,
            address as Address,
            chainId
          )
        }

        const priceImpact = calculatePriceImpact(
          inputSum,
          amount as bigint,
          virtualPrice
        )

        dispatch(
          setDepositQuote({
            priceImpact: priceImpact,
            allowances,
            routerAddress: poolAddress,
          })
        )
        dispatch(setIsLoading(false))
      } else {
        dispatch(setDepositQuote(DEFAULT_DEPOSIT_QUOTE))
        dispatch(setIsLoading(false))
      }
    } catch (e) {
      dispatch(setIsLoading(false))
      console.log(e)
    }
  }

  useEffect(() => {
    calculateMaxDeposits()
  }, [inputValue, filteredInputValue, pool, chainId, address])

  const onChangeInputValue = (token: Token, value: string) => {
    const bigInt = stringToBigInt(value, token.decimals[chainId]) ?? 0n
    if (chainId && token) {
      const tokenAddr = getAddress(token.addresses[chainId])
      dispatch(
        setInputValue({
          bi: {
            ...inputValue.bi,
            [tokenAddr]: bigInt,
          },
          str: {
            ...inputValue.str,
            [tokenAddr]: value,
          },
        })
      )
    }
  }

  useEffect(() => {
    if (poolData && poolUserData && pool && chainId && address) {
      resetInputs()
    }
  }, [poolUserData])

  const resetInputs = () => {
    dispatch(resetPoolDeposit())
  }

  const approveTxn = async () => {
    try {
      const tx = approve(pool, depositQuote, inputValue.bi, chainId)
      try {
        await tx
        calculateMaxDeposits()
      } catch (error) {
        return txErrorHandler
      }
    } catch (error) {
      return txErrorHandler(error)
    }
  }

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

  const depositTxn = async () => {
    try {
      let tx

      if (poolData.totalLocked === 0) {
        tx = emptyPoolDeposit(pool, filteredInputValue.bi, chainId)
      } else {
        tx = deposit(pool, 'ONE_TENTH', null, filteredInputValue.bi, chainId)
      }

      const resolvedTx = await tx

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

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

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

  return (
    <div className="flex-col">
      <div className="mb-4">
        {pool && poolUserData.tokens && poolData ? (
          poolUserData.tokens.map((tokenObj, i) => {
            return (
              <div
                className={
                  i < poolUserData.tokens.length - 1
                    ? 'border-b border-[#564f58]'
                    : ''
                }
              >
                <SerializedDepositInput
                  key={i}
                  tokenObj={tokenObj}
                  pool={pool}
                  address={address}
                  chainId={chainId}
                  inputValue={inputValue}
                  onChangeInputValue={onChangeInputValue}
                />
              </div>
            )
          })
        ) : (
          <>
            <LoadingTokenInput />
            <LoadingTokenInput />
          </>
        )}
      </div>
      <DepositButton approveTxn={approveTxn} depositTxn={depositTxn} />
      {depositQuote.priceImpact &&
        depositQuote.priceImpact !== 0n &&
        inputSum !== 0n && (
          <PriceImpactDisplay priceImpact={depositQuote.priceImpact} />
        )}
    </div>
  )
}

const SerializedDepositInput = ({
  pool,
  tokenObj,
  address,
  chainId,
  inputValue,
  onChangeInputValue,
}) => {
  const [serializedToken, setSerializedToken] = useState(undefined)
  const balanceToken = getBalanceToken(tokenObj.token, pool)

  useEffect(() => {
    const fetchSerializedData = async () => {
      try {
        const token = await serializeToken(
          address,
          chainId,
          balanceToken,
          tokenObj
        )
        setSerializedToken(token)
      } catch (error) {
        console.log(`error`, error)
      }
    }

    fetchSerializedData()
  }, [])

  return (
    serializedToken && (
      <DepositTokenInput
        token={serializedToken}
        key={serializedToken.symbol}
        rawBalance={serializedToken.rawBalance}
        balanceStr={formatBigIntToString(
          serializedToken.rawBalance,
          serializedToken.decimals[chainId],
          5
        )}
        inputValueStr={inputValue.str[serializedToken.addresses[chainId]]}
        onChange={(value) => onChangeInputValue(serializedToken, value)}
        chainId={chainId}
        address={address}
      />
    )
  )
}

const getBalanceToken = (token: Token, pool: Token) => {
  let balanceToken: Token | undefined
  if (token.symbol == WETH.symbol && !pool.nativeTokens.includes(WETH)) {
    balanceToken = ETH
  } else if (token.symbol == AVWETH.symbol) {
    balanceToken = WETHE
  } else {
    balanceToken = token
  }
  return balanceToken
}

const serializeToken = async (
  address: string,
  chainId: number,
  balanceToken: Token,
  tokenObj: any
) => {
  let fetchedBalance

  if (balanceToken.addresses[chainId] === zeroAddress) {
    fetchedBalance = await getBalance(wagmiConfig, {
      address: address as Address,
      chainId,
    })

    return {
      ...balanceToken,
      rawBalance: fetchedBalance.value,
      balanceStr: formatBigIntToString(
        fetchedBalance.value,
        balanceToken.decimals[chainId],
        5
      ),
    }
  } else if (balanceToken === WETHE) {
    fetchedBalance = await getBalance(wagmiConfig, {
      address: address as Address,
      chainId,
      token: balanceToken.addresses[chainId] as Address,
    })

    return {
      ...balanceToken,
      rawBalance: fetchedBalance.value,
      balanceStr: fetchedBalance.formatted,
    }
  } else {
    return {
      ...balanceToken,
      rawBalance: tokenObj.rawBalance,
      balanceStr: tokenObj.balanceStr,
    }
  }
}

export default Deposit