synapsecns/sanguine

View on GitHub
packages/synapse-interface/pages/lifi/index.tsx

Summary

Maintainability
A
0 mins
Test Coverage
import { useEffect, useState } from 'react'
import { useAccount, useSwitchChain } from 'wagmi'
import { LandingPageWrapper } from '@layouts/LandingPageWrapper'
import StandardPageContainer from '@layouts/StandardPageContainer'
import { getErc20TokenAllowance } from '@/actions/getErc20TokenAllowance'
import { approveToken } from '@/utils/approveToken'
import { TransactionButton } from '@/components/buttons/TransactionButton'
import { useConnectModal } from '@rainbow-me/rainbowkit'
import Image from 'next/image'
import { CHAINS_BY_ID } from '@/constants/chains'

const CHAIN_IDS = [1, 56, 42161, 10]
const LIFI_SPENDER = '0x1231deb6f5749ef6ce6943a275a1d3e7486f4eae'

const MAX_AMOUNT = 100000000000000000000000000000n

const TOKENS = {
  1: {
    // Ethereum Mainnet
    USDC: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
    USDT: '0xdac17f958d2ee523a2206206994597c13d831ec7',
    WETH: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
  },
  42161: {
    // Arbitrum
    USDC: '0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8',
    USDT: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9',
    WETH: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
  },
  10: {
    // Optimism
    USDC: '0x7F5c764cBc14f9669B88837ca1490cCa17c31607',
    USDT: '0x94b008aA00579c1307B0EF2c499aD98a8ce58e58',
    WETH: '0x4200000000000000000000000000000000000006',
  },
  56: {
    // BSC
    USDC: '0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d',
    USDT: '0x55d398326f99059ff775485246999027b3197955',
    WETH: '0x4DB5a66E937A9F4473fA95b1cAF1d1E1D62E29EA',
  },
}

interface TokenAllowances {
  [chainId: number]: {
    [token: string]: bigint
  }
}

const LifiPage = () => {
  const { address, isConnected, chain } = useAccount()
  const { chains, switchChain: switchNetwork } = useSwitchChain()
  const { openConnectModal } = useConnectModal()
  const [hoveredToken, setHoveredToken] = useState(null)

  const [allowances, setAllowances] = useState<TokenAllowances>({})

  useEffect(() => {
    const fetchAllowances = async () => {
      if (address) {
        const newAllowances: TokenAllowances = {}

        for (const chainId of CHAIN_IDS) {
          newAllowances[chainId] = {}

          for (const [tokenName, tokenAddress] of Object.entries(
            TOKENS[chainId]
          )) {
            const allowance = await getErc20TokenAllowance({
              address,
              chainId,
              tokenAddress: tokenAddress as `0x${string}`,
              spender: LIFI_SPENDER,
            })
            newAllowances[chainId][tokenName] = allowance
          }
        }

        setAllowances(newAllowances)
      }
    }

    if (isConnected) {
      fetchAllowances()
    }
  }, [address, isConnected])

  const handleRevoke = async (chainId: number, tokenName: string) => {
    if (chain?.id !== chainId) {
      await switchNetwork({ chainId: chainId })
    }
    const tokenAddress = TOKENS[chainId][tokenName]
    await approveToken(LIFI_SPENDER, chainId, tokenAddress, 0n)
    setAllowances((prev) => ({
      ...prev,
      [chainId]: {
        ...prev[chainId],
        [tokenName]: 0n,
      },
    }))
  }

  return (
    <LandingPageWrapper>
      <StandardPageContainer connectedChainId={chain?.id} address={address}>
        <div className="flex justify-between">
          <div>
            <div className="text-2xl text-white">
              Revoke Li.fi Approvals (Multi-chain)
            </div>
          </div>
        </div>
        <div className="py-6">
          <div className="pb-3 place-self-center">
            <div>
              <h3 className="text-lg">
                Li.fi / Jumper is investigating an ongoing exploit, and users
                should revoke approvals{' '}
                <a
                  className="underline"
                  target="_blank"
                  href="https://x.com/lifiprotocol/status/1813207291778215955"
                >
                  - Li.fi Tweet
                </a>
              </h3>
              <br />
              <h3 className="text-lg">
                Check to see if you have any approvals at risk below:
              </h3>
              <br />
              {isConnected ? (
                CHAIN_IDS.map((chainId) => (
                  <div key={chainId} className="mb-5 ">
                    <span className="flex items-center gap-1 mb-2">
                      <Image
                        loading="lazy"
                        src={CHAINS_BY_ID[chainId].chainImg}
                        alt={`${chain.name} img`}
                        width="20"
                        height="20"
                        className="w-5 h-5 max-w-fit"
                      />
                      <div className="text-xl">
                        {CHAINS_BY_ID[chainId].name}
                      </div>
                    </span>
                    {Object.entries(allowances[chainId] || {}).map(
                      ([tokenName, allowance]) => {
                        return (
                          <div
                            key={tokenName}
                            className="flex items-center space-x-8 h-[40px]"
                          >
                            <div className="flex justify-between w-1/2">
                              <div>{tokenName} </div>
                              <div
                                onMouseEnter={() =>
                                  setHoveredToken(
                                    allowance > MAX_AMOUNT ? tokenName : null
                                  )
                                }
                                onMouseLeave={() => setHoveredToken(null)}
                                className="hover:cursor-pointer"
                              >
                                {allowance > MAX_AMOUNT && <div>UNLIMITED</div>}
                                {hoveredToken &&
                                  hoveredToken === tokenName &&
                                  allowance > 0n && (
                                    <div className="absolute p-2 mt-4 bg-gray-500">
                                      {allowances[chainId][
                                        hoveredToken
                                      ].toString()}
                                    </div>
                                  )}
                              </div>
                            </div>
                            {allowance > 0n ? (
                              <TransactionButton
                                className="btn btn-primary rounded-md max-w-[250px] h-[40px]"
                                pendingLabel="Revoking..."
                                label={`Revoke ${tokenName} Approval`}
                                onClick={() => handleRevoke(chainId, tokenName)}
                              />
                            ) : (
                              <div className="w-[250px] flex justify-center">
                                -
                              </div>
                            )}
                          </div>
                        )
                      }
                    )}
                  </div>
                ))
              ) : (
                <div className="flex flex-col justify-center h-full p-10">
                  <TransactionButton
                    style={{
                      background:
                        'linear-gradient(90deg, rgba(128, 0, 255, 0.2) 0%, rgba(255, 0, 191, 0.2) 100%)',
                      border: '1px solid #9B6DD7',
                      borderRadius: '4px',
                    }}
                    label="Connect wallet to check for approvals"
                    pendingLabel="Connecting"
                    onClick={() =>
                      new Promise((resolve) => {
                        openConnectModal()
                        resolve(true)
                      })
                    }
                  />
                </div>
              )}
            </div>
          </div>
        </div>
      </StandardPageContainer>
    </LandingPageWrapper>
  )
}

export default LifiPage