import { ArrowRightOutlined, EditOutlined, LoadingOutlined, PauseCircleOutlined, } from '@ant-design/icons' import { Web3Provider } from '@ethersproject/providers' import { AcceptSlippageUpdateHookParams, ExecutionSettings, StatusMessage, Token } from '@lifi/sdk' import { useWeb3React } from '@web3-react/core' import { Button, Divider, Modal, Row, Space, Timeline, Tooltip, Typography } from 'antd' import { constants } from 'ethers' import { useEffect, useState } from 'react' import { useStepReturnInfo } from '../hooks/useStepReturnInfo' import LiFi from '../LiFi' import { isWalletConnectWallet, readForegroundRoute, storeForegroundRoute, storeRoute, } from '../services/localStorage' import { switchChain, switchChainAndAddToken } from '../services/metamask' import Notification, { NotificationType } from '../services/notifications' import { renderProcessError, renderProcessMessage, renderSubstatusmessage, } from '../services/processRenderer' import { copyToClipboard, formatTokenAmount, parseSecondsAsTime } from '../services/utils' import { getChainById, isCrossStep, isLifiStep, Route, Step } from '../types' import { getChainAvatar, getToolAvatar } from './Avatars/Avatars' import Clock from './Clock' import LoadingIndicator from './LoadingIndicator' import { FurtherLinks } from './SwappingMainButtonFiles/FurtherLinks' import { SwappingModalInfoMessages } from './SwappingModalInfoMessages' import { WalletConnectChainSwitchModal } from './WalletConnectChainSwitchModal' interface SwapSettings { infiniteApproval: boolean } interface SwappingProps { route: Route settings: SwapSettings updateRoute: Function onSwapDone: Function fixedRecipient?: boolean } const Swapping = ({ route, updateRoute, settings, onSwapDone, fixedRecipient = false, }: SwappingProps) => { const [localRoute, setLocalRoute] = useState(route) const [swapStartedAt, setSwapStartedAt] = useState() const [swapDoneAt, setSwapDoneAt] = useState() const [isSwapping, setIsSwapping] = useState(false) const [alerts] = useState>([]) const [showWalletConnectChainSwitchModal, setShowWalletConnectChainSwitchModal] = useState<{ show: boolean chainId: number promiseResolver?: Function }>({ show: false, chainId: 1 }) const [showAcceptSlippageUpdateModal, setShowAcceptSlippageUpdateModal] = useState<{ show: boolean oldToAmount?: string newToAmount?: string toToken?: Token oldSlippage?: number newSlippage?: number promiseResolver?: Function }>({ show: false }) const routeReturnInfo = useStepReturnInfo(localRoute.steps[localRoute.steps.length - 1]) // Wallet const web3 = useWeb3React() useEffect(() => { setLocalRoute(route) }, [route]) useEffect(() => { // check if route is eligible for automatic resuming const foregroundRouteID = readForegroundRoute() if (!foregroundRouteID) { const allDone = localRoute.steps.every((step) => step.execution?.status === 'DONE') const isFailed = localRoute.steps.some((step) => step.execution?.status === 'FAILED') const alreadyStarted = localRoute.steps.some((step) => step.execution) if (!allDone && !isFailed && alreadyStarted) { resumeExecution() } } }, []) const parseExecution = (step: Step) => { if (!step.execution) { return [] } return step.execution.process.map((process, index, processList) => { const type = process.status === 'DONE' ? 'success' : process.status === 'FAILED' ? 'danger' : undefined const hasFailed = process.status === 'FAILED' const isLastPendingProcess = index === processList.length - 1 && process.status === 'PENDING' return (

{renderProcessMessage(process)}

{hasFailed && ( <> {renderProcessError(process)}
{!!step.execution?.process.some((process) => process.txHash) && ( )}
)}
) }) } const parseStepToTimeline = (step: Step, index: number) => { const executionSteps = parseExecution(step) const isDone = step.execution && step.execution.status === 'DONE' const isLoading = isSwapping && step.execution && step.execution.status === 'PENDING' const isPaused = !isSwapping && step.execution && step.execution.status === 'PENDING' const color = isDone ? 'green' : step.execution ? 'blue' : 'gray' const executionDuration = !!step.estimate.executionDuration && ( <>
Estimated duration: {parseSecondsAsTime(step.estimate.executionDuration)} min ) const executionItem = [ : isPaused ? : null}> {executionSteps} , ] switch (step.type) { case 'swap': { return [

Swap on {getToolAvatar(step)}

{formatTokenAmount(step.action.fromToken, step.estimate?.fromAmount)}{' '} {' '} {formatTokenAmount(step.action.toToken, step.estimate?.toAmount)} {executionDuration}
, !!step.execution || localRoute.steps.length - 1 === index ? executionItem : <>, ] } case 'cross': { const { action, estimate } = step return [

Transfer from {getChainAvatar(getChainById(action.fromChainId).key)} to{' '} {getChainAvatar(getChainById(action.toChainId).key)} via {getToolAvatar(step)}

{formatTokenAmount(action.fromToken, estimate.fromAmount)} {' '} {formatTokenAmount(action.toToken, estimate.toAmount)} {executionDuration}
, !!step.execution || localRoute.steps.length - 1 === index ? executionItem : <>, ] } case 'lifi': { return [

LI.FI Contract from {getChainAvatar(getChainById(step.action.fromChainId).key)} to{' '} {getChainAvatar(getChainById(step.action.toChainId).key)} via {getToolAvatar(step)}

{formatTokenAmount(step.action.fromToken, step.estimate?.fromAmount)}{' '} {' '} {formatTokenAmount(step.action.toToken, step.estimate?.toAmount)} {executionDuration}
, !!step.execution || localRoute.steps.length - 1 === index ? executionItem : <>, ] } default: // eslint-disable-next-line no-console console.warn('should never reach here') } } const startCrossChainSwap = async () => { if (!web3.account || !web3.library) return const signer = web3.library.getSigner() const executionSettings: ExecutionSettings = { updateCallback, switchChainHook, acceptSlippageUpdateHook, infiniteApproval: settings.infiniteApproval, } storeRoute(localRoute) storeForegroundRoute(localRoute) setIsSwapping(true) setSwapStartedAt(Date.now()) try { await LiFi.executeRoute(signer, localRoute, executionSettings) } catch (e) { // eslint-disable-next-line no-console console.warn('Execution failed!', localRoute) // eslint-disable-next-line no-console console.error(e) Notification.showNotification(NotificationType.TRANSACTION_ERROR) setIsSwapping(false) return } setIsSwapping(false) setSwapDoneAt(Date.now()) Notification.showNotification(NotificationType.TRANSACTION_SUCCESSFULL) onSwapDone() } const resumeExecution = async () => { if (!web3.account || !web3.library) return const executionSettings: ExecutionSettings = { updateCallback, switchChainHook, acceptSlippageUpdateHook, infiniteApproval: settings.infiniteApproval, } setIsSwapping(true) storeForegroundRoute(localRoute) try { await LiFi.resumeRoute(web3.library.getSigner(), localRoute, executionSettings) } catch (e) { // eslint-disable-next-line no-console console.warn('Execution failed!', localRoute) // eslint-disable-next-line no-console console.error(e) Notification.showNotification(NotificationType.TRANSACTION_ERROR) setIsSwapping(false) return } setIsSwapping(false) setSwapDoneAt(Date.now()) Notification.showNotification(NotificationType.TRANSACTION_SUCCESSFULL) onSwapDone() } const switchChainHook = async (requiredChainId: number) => { if (!web3.account || !web3.library) return if (isWalletConnectWallet(web3.account)) { let promiseResolver const signerAwaiter = new Promise((resolve) => (promiseResolver = resolve)) setShowWalletConnectChainSwitchModal({ show: true, chainId: requiredChainId, promiseResolver, }) await signerAwaiter return web3.library.getSigner() } if ((await web3.library.getSigner().getChainId()) !== requiredChainId) { // Fallback is Metamask const switched = await switchChain(requiredChainId) if (!switched) { throw new Error('Chain was not switched') } } return web3.library.getSigner() } const acceptSlippageUpdateHook = async ( params: AcceptSlippageUpdateHookParams, // eslint-disable-next-line max-params ) => { if (!web3.account || !web3.library) return false let promiseResolver const awaiter = new Promise((resolve) => (promiseResolver = resolve)) setShowAcceptSlippageUpdateModal({ show: true, oldToAmount: params.oldToAmount, newToAmount: params.newToAmount, toToken: params.toToken, oldSlippage: params.oldSlippage, newSlippage: params.newSlippage, promiseResolver, }) const accept = await awaiter return accept } // called on every execution status change const updateCallback = (updatedRoute: Route) => { setLocalRoute(updatedRoute) storeRoute(updatedRoute) updateRoute(updatedRoute) } const getMainButton = () => { // PENDING if (isSwapping) { return <> } const isCrossChainSwap = !!localRoute.steps.find( (step) => isCrossStep(step) || isLifiStep(step), ) // DONE const isDone = localRoute.steps.filter((step) => step.execution?.status !== 'DONE').length === 0 if (isDone) { const lastStepProcesses = localRoute.steps[localRoute.steps.length - 1].execution?.process const lastProcess = lastStepProcesses?.[lastStepProcesses?.length - 1] const infoMessage = !!routeReturnInfo?.totalBalanceOfReceivedToken ? ( <> {!routeReturnInfo.receivedAmount.isZero() && fixedRecipient && ( <> You sent{` `} {formatTokenAmount( routeReturnInfo.receivedToken, routeReturnInfo.receivedAmount.toString(), )}
)} {!routeReturnInfo.receivedAmount.isZero() && !fixedRecipient && ( <> {`You received ${formatTokenAmount( routeReturnInfo.receivedToken, routeReturnInfo.receivedAmount.toString(), )} on ${routeReturnInfo.toChain.name}`} )} ) : ( '' ) const substatusmessage = lastProcess ? renderSubstatusmessage(lastProcess.status as StatusMessage, lastProcess?.substatus) : undefined const infoTitle = ( <> Done {substatusmessage && ` - ${substatusmessage}`} ) return ( {infoTitle} {routeReturnInfo?.totalBalanceOfReceivedToken && (routeReturnInfo.totalBalanceOfReceivedToken.address === constants.AddressZero ? ( {infoMessage} ) : ( switchChainAndAddToken( routeReturnInfo.toChain.id, routeReturnInfo.totalBalanceOfReceivedToken!, ) }> {infoMessage} ))} ) } // FAILED const isFailed = localRoute.steps.some((step) => step.execution?.status === 'FAILED') if (isFailed) { return ( ) } const chainSwitchRequired = localRoute.steps.some( (step) => step.execution?.status === 'CHAIN_SWITCH_REQUIRED', ) if (chainSwitchRequired) { return <> } // NOT_STARTED return ( ) } const getCurrentProcess = () => { for (const step of localRoute.steps) { if (step.execution?.process) { for (const process of step.execution?.process) { if (process.status === 'ACTION_REQUIRED' || process.status === 'PENDING') { return process } } } } return null } const getCurrentStep = () => { for (const step of localRoute.steps) { if (step.execution && step.execution.status !== 'DONE') { return step } } return null } const currentProcess = getCurrentProcess() const currentStep = getCurrentStep() return ( <> {alerts}
{/* Steps */} {localRoute.steps.map(parseStepToTimeline)}
{swapStartedAt ? ( ) : (   )}
{getMainButton()}
{isSwapping && currentProcess && currentProcess.status === 'ACTION_REQUIRED' && ( <> {renderProcessMessage(currentProcess)} )} {isSwapping && currentProcess && currentProcess.status === 'PENDING' && ( <> {renderProcessMessage(currentProcess)} {renderSubstatusmessage(currentProcess.status, currentProcess.substatus)} )} setShowWalletConnectChainSwitchModal({ show: false, chainId: 1, }) } onCancel={() => setShowWalletConnectChainSwitchModal({ show: false, chainId: 1, }) } footer={null}> { if (showWalletConnectChainSwitchModal.promiseResolver) { showWalletConnectChainSwitchModal.promiseResolver() } setShowWalletConnectChainSwitchModal({ show: false, chainId: 1, }) }} /> { if (showAcceptSlippageUpdateModal.promiseResolver) { showAcceptSlippageUpdateModal.promiseResolver(true) } setShowAcceptSlippageUpdateModal({ show: false, }) }} onCancel={() => { if (showAcceptSlippageUpdateModal.promiseResolver) { showAcceptSlippageUpdateModal.promiseResolver(false) } setShowAcceptSlippageUpdateModal({ show: false, }) }}> Warning The conditions of your transaction have changed. Do you accept the recalculated estimates? If you refuse, this transaction will not proceed and no funds will be sent. old return amount:{' '} {showAcceptSlippageUpdateModal.toToken && showAcceptSlippageUpdateModal.oldToAmount && formatTokenAmount( showAcceptSlippageUpdateModal.toToken!, showAcceptSlippageUpdateModal.oldToAmount!, )}{' '}
new return amount:{' '} {showAcceptSlippageUpdateModal.toToken && showAcceptSlippageUpdateModal.newToAmount && formatTokenAmount( showAcceptSlippageUpdateModal.toToken!, showAcceptSlippageUpdateModal.newToAmount!, )}
old slippage: {showAcceptSlippageUpdateModal.oldSlippage! * 100 || '~'} %
new slippage: {showAcceptSlippageUpdateModal.newSlippage! * 100 || '~'} %
) } export default Swapping