synapsecns/sanguine

View on GitHub
packages/synapse-interface/components/Portfolio/Portfolio.tsx

Summary

Maintainability
B
6 hrs
Test Coverage
import React, { useEffect, useState, useMemo } from 'react'
import Fuse from 'fuse.js'
import { useAccount, useAccountEffect } from 'wagmi'
import { Address } from 'viem'
import { useAppDispatch } from '@/store/hooks'
import { setFromChainId } from '@/slices/bridge/reducer'
import { PortfolioTabManager } from './components/PortfolioTabManager'
import {
  NetworkTokenBalances,
  TokenAndBalance,
} from '@/utils/actions/fetchPortfolioBalances'
import { PortfolioContent } from './components/PortfolioContent'
import {
  useFetchPortfolioBalances,
  usePortfolioState,
} from '@/slices/portfolio/hooks'
import {
  PortfolioTabs,
  resetPortfolioState,
  FetchState,
} from '@/slices/portfolio/actions'
import { resetTransactionsState } from '@/slices/transactions/actions'
import { PortfolioState } from '@/slices/portfolio/reducer'
import { useBridgeState } from '@/slices/bridge/hooks'
import { BridgeState, resetBridgeInputs } from '@/slices/bridge/reducer'
import { ViewSearchAddressBanner } from './components/ViewSearchAddressBanner'
import { Activity } from '../Activity/Activity'
import { useSearchInputState } from './hooks/useSearchInputStatus'

export const Portfolio = () => {
  const dispatch = useAppDispatch()
  const { fromChainId }: BridgeState = useBridgeState()
  const {
    activeTab,
    searchInput,
    searchStatus,
    searchedBalances,
  }: PortfolioState = usePortfolioState()
  const { chain, address } = useAccount()

  useAccountEffect({
    onDisconnect() {
      dispatch(resetPortfolioState())
      dispatch(resetTransactionsState())
      dispatch(resetBridgeInputs())
    },
  })

  const { balances: portfolioData, status: fetchState } =
    useFetchPortfolioBalances()

  const filteredPortfolioData: NetworkTokenBalances =
    filterPortfolioBalancesWithBalances(portfolioData)

  const {
    isSearchInputActive,
    isSearchInputAddress,
    isMasqueradeActive,
    masqueradeAddress,
  } = useSearchInputState()

  const filteredSearchedPortfolioDataForBalances = useMemo(() => {
    if (isMasqueradeActive) {
      return {
        balances: filterPortfolioBalancesWithBalances(
          searchedBalances[masqueradeAddress]
        ),
        address: masqueradeAddress,
      }
    }
    return {
      balances: {},
      address: '',
    }
  }, [searchedBalances, isMasqueradeActive, searchInput])

  const flattenedPortfolioData = flattenData(filteredPortfolioData)

  const filteredBySearchInput = filterBySearchInput(
    flattenedPortfolioData,
    searchInput
  )

  useEffect(() => {
    if (!chain) {
      dispatch(setFromChainId(null))
    }
    if (address && chain?.id) {
      dispatch(setFromChainId(chain.id))
    }
  }, [chain])

  const [mounted, setMounted] = useState<boolean>(false)
  useEffect(() => setMounted(true), [])

  return (
    <div
      data-test-id="portfolio"
      className="flex flex-col w-full max-w-lg mx-auto lg:mx-0"
    >
      <PortfolioTabManager />
      <div className="mt-6">
        {mounted && (
          <>
            {searchStatus === FetchState.LOADING && (
              <div className="pb-3 text-secondary">Loading new address...</div>
            )}
            {isMasqueradeActive ? (
              <>
                <ViewSearchAddressBanner
                  viewingAddress={
                    filteredSearchedPortfolioDataForBalances.address as Address
                  }
                />
                <PortfolioContent
                  connectedAddress={
                    filteredSearchedPortfolioDataForBalances.address
                  }
                  connectedChainId={chain?.id}
                  selectedFromChainId={fromChainId}
                  networkPortfolioWithBalances={
                    isSearchInputActive && !isSearchInputAddress
                      ? filteredBySearchInput
                      : filteredSearchedPortfolioDataForBalances.balances
                  }
                  fetchState={searchStatus}
                  visibility={activeTab === PortfolioTabs.PORTFOLIO}
                  searchInputActive={isSearchInputActive}
                  searchStatus={searchStatus}
                  searchInput={searchInput}
                />
              </>
            ) : (
              <div
                className={
                  searchStatus === FetchState.LOADING
                    ? 'opacity-30 cursor-not-allowed'
                    : 'opacity-100'
                }
              >
                <PortfolioContent
                  connectedAddress={address}
                  connectedChainId={chain?.id}
                  selectedFromChainId={fromChainId}
                  networkPortfolioWithBalances={
                    isSearchInputActive
                      ? filteredBySearchInput
                      : filteredPortfolioData
                  }
                  fetchState={fetchState}
                  visibility={activeTab === PortfolioTabs.PORTFOLIO}
                  searchInputActive={isSearchInputActive}
                  searchStatus={searchStatus}
                  searchInput={searchInput}
                />
              </div>
            )}
            <Activity visibility={activeTab === PortfolioTabs.ACTIVITY} />
          </>
        )}
      </div>
    </div>
  )
}

function flattenData(portfolioData: NetworkTokenBalances): TokenAndBalance[] {
  const flattened: TokenAndBalance[] = []
  Object.entries(portfolioData).forEach(([chainId, tokens]) => {
    tokens.forEach((token: TokenAndBalance) => {
      flattened.push({ ...token })
    })
  })
  return flattened
}

function filterBySearchInput(
  portfolioData: TokenAndBalance[],
  searchInput: string
) {
  const searchFiltered: NetworkTokenBalances = {}
  const fuseOptions = {
    includeScore: true,
    threshold: 0.33,
    distance: 20,
    keys: ['queriedChain.name', 'token.name', 'token.symbol'],
  }
  const fuse = new Fuse(portfolioData, fuseOptions)

  if (searchInput.length > 0) {
    fuse
      .search(searchInput)
      .map((i: Fuse.FuseResult<TokenAndBalance>) => i.item)
      .forEach((item) => {
        const chainId = item.queriedChain.id
        if (!searchFiltered[chainId]) {
          searchFiltered[chainId] = []
        }
        searchFiltered[chainId].push(item)
      })
  }

  return searchFiltered
}

export function filterPortfolioBalancesWithBalances(
  balances: NetworkTokenBalances
): NetworkTokenBalances {
  return Object.entries(balances).reduce(
    (filteredBalances: NetworkTokenBalances, [key, tokenWithBalances]) => {
      const filtered = tokenWithBalances.filter(
        (token: TokenAndBalance) => token.balance > 0n
      )
      if (filtered.length > 0) {
        filteredBalances[key] = filtered
      }
      return filteredBalances
    },
    {}
  )
}