synapsecns/sanguine

View on GitHub
packages/widget/src/components/Transaction.tsx

Summary

Maintainability
A
2 hrs
Test Coverage
import { useEffect, useMemo, useCallback } from 'react'
import { Chain } from 'types'

import { useAppDispatch } from '@/state/hooks'
import { getTxBlockExplorerLink } from '@/utils/getTxBlockExplorerLink'
import { getExplorerAddressUrl } from '@/utils/getExplorerAddressLink'
import { useBridgeTxStatus } from '@/hooks/useBridgeTxStatus'
import { isNull } from '@/utils/isNull'
import {
  updateTransactionKappa,
  completeTransaction,
  removeTransaction,
} from '@/state/slices/transactions/reducer'
import { useTransactionsState } from '@/state/slices/transactions/hooks'
import { useSynapseContext } from '@/providers/SynapseProvider'
import { TimeRemaining } from '@/components/TimeRemaining'
import { DropdownMenu } from '@/components/ui/DropdownMenu'
import { MenuItem } from '@/components/ui/MenuItem'
import { CHAINS_BY_ID } from '@/constants/chains'
import { TransactionSupport } from '@/components/TransactionSupport'
import { AnimatedProgressBar } from '@/components/AnimatedProgressBar'

export const Transaction = ({
  connectedAddress,
  originAmount,
  originTokenSymbol,
  originChainId,
  destinationChainId,
  originTxHash,
  bridgeModuleName,
  estimatedTime,
  kappa,
  timestamp,
  currentTime,
  isStoredComplete,
}: {
  connectedAddress: string
  originAmount: string
  originTokenSymbol: string
  originChainId: number
  destinationChainId: number
  originTxHash: string
  bridgeModuleName: string
  estimatedTime: number // in seconds
  kappa?: string
  timestamp: number
  currentTime: number
  isStoredComplete: boolean
}) => {
  const dispatch = useAppDispatch()
  const transactions = useTransactionsState()

  const { synapseSDK } = useSynapseContext()

  const [originTxExplorerLink, originExplorerName] = getTxBlockExplorerLink(
    originChainId,
    originTxHash
  )
  const [destExplorerAddressLink, destExplorerName] = getExplorerAddressUrl(
    destinationChainId,
    connectedAddress
  )

  const elapsedTime: number = currentTime - timestamp // in seconds
  const remainingTime: number = estimatedTime - elapsedTime

  const isEstimatedTimeReached: boolean = useMemo(() => {
    if (!currentTime || !estimatedTime || !timestamp) {
      return false
    }
    return currentTime - timestamp > estimatedTime
  }, [estimatedTime, currentTime, timestamp])

  const delayedTime = isEstimatedTimeReached ? remainingTime : null
  const delayedTimeInMin = remainingTime ? Math.floor(remainingTime / 60) : null

  const [isTxComplete, _kappa] = useBridgeTxStatus({
    synapseSDK,
    originChainId,
    destinationChainId,
    originTxHash,
    bridgeModuleName,
    kappa,
    checkStatus: !isStoredComplete || isEstimatedTimeReached,
    currentTime,
  })

  /** Check if store already marked tx as complete, otherwise check hook status */
  const isTxFinalized = isStoredComplete ?? isTxComplete

  const showTransactionSupport =
    !isTxFinalized && delayedTimeInMin ? delayedTimeInMin <= -5 : false

  /** Update tx kappa when available */
  useEffect(() => {
    if (_kappa && originTxHash) {
      dispatch(
        updateTransactionKappa({ originTxHash, kappa: _kappa as string })
      )
    }
  }, [_kappa, dispatch])

  /** Update tx for completion */
  /** Check that we have not already marked tx as complete */
  useEffect(() => {
    const txKappa = kappa ?? _kappa

    if (!isStoredComplete && isTxComplete && originTxHash && txKappa) {
      dispatch(completeTransaction({ originTxHash, kappa: txKappa as string }))
    }
  }, [isStoredComplete, isTxComplete, dispatch, transactions])

  const handleClearTransaction = useCallback(() => {
    dispatch(removeTransaction({ originTxHash }))
  }, [dispatch])

  return (
    <div
      data-test-id="transaction"
      className={`
        flex flex-col relative
        gap-1 justify-end items-center pl-2.5 pr-1.5 py-1
        border border-solid border-[--synapse-border] rounded-md
      `}
      style={{ background: 'var(--synapse-surface)' }}
    >
      <div className="flex flex-wrap-reverse items-center justify-between w-full">
        <TransactionBridgeDetail
          tokenAmount={originAmount}
          originTokenSymbol={originTokenSymbol}
          destinationChain={CHAINS_BY_ID[destinationChainId]}
        />
        <div className="flex items-center justify-end gap-2 grow">
          <DropdownMenu
            menuTitleElement={
              <TimeRemaining
                isComplete={isTxFinalized as boolean}
                remainingTime={remainingTime}
                isDelayed={isEstimatedTimeReached}
                delayedTime={delayedTime}
              />
            }
          >
            {!isNull(originTxExplorerLink) && (
              <MenuItem text={originExplorerName} link={originTxExplorerLink} />
            )}
            {!isNull(destExplorerAddressLink) && (
              <MenuItem
                text={destExplorerName}
                link={destExplorerAddressLink}
              />
            )}
            <MenuItem
              text="Contact Support"
              link="https://discord.gg/synapseprotocol"
            />
            {isTxFinalized && (
              <MenuItem
                text="Clear Transaction"
                link={null}
                onClick={handleClearTransaction}
              />
            )}
          </DropdownMenu>
        </div>
      </div>
      {showTransactionSupport && <TransactionSupport />}
      <div className="absolute bottom-0 w-full px-1 text-[0px]">
        <AnimatedProgressBar
          id={originTxHash}
          startTime={timestamp}
          estDuration={estimatedTime * 2} // 2x buffer
          isComplete={isTxFinalized}
        />
      </div>
    </div>
  )
}

const TransactionBridgeDetail = ({
  tokenAmount,
  originTokenSymbol,
  destinationChain,
}: {
  tokenAmount: string
  originTokenSymbol: string
  destinationChain: Chain
}) => {
  const showAmount = parseFloat(tokenAmount)?.toFixed(6)

  return (
    <div className="flex">
      {showAmount} {originTokenSymbol} to {destinationChain.name}
    </div>
  )
}