import { RouteResponse } from '@0xsquid/squid-types'; import { Body, ButtCon, Button, EllipsizedText, FramedIcon, FramedImage, Heading, hFlex, Icon, PriceDisplay, Stack, Sticker, useInterval, } from '@biom3/react'; import { t } from 'i18next'; import { useCallback, useContext, useEffect, useMemo, useState, } from 'react'; import { SimpleLayout } from '../../../components/SimpleLayout/SimpleLayout'; import { AddTokensReviewData, AddTokensWidgetViews, } from '../../../context/view-context/AddTokensViewContextTypes'; import { ViewActions, ViewContext, } from '../../../context/view-context/ViewContext'; import { SquidIcon } from '../../../lib/squid/components/SquidIcon'; import { useExecute } from '../../../lib/squid/hooks/useExecute'; import { useRoutes } from '../../../lib/squid/hooks/useRoutes'; import { AddTokensContext } from '../context/AddTokensContext'; import { AddTokensErrorTypes } from '../types'; import { useProvidersContext } from '../../../context/providers-context/ProvidersContext'; import { getRouteChains } from '../../../lib/squid/functions/getRouteChains'; import { getTotalRouteFees } from '../../../lib/squid/functions/getTotalRouteFees'; import { LoadingView } from '../../../views/loading/LoadingView'; import { AddressMissmatchDrawer } from '../../../components/AddressMismatchDrawer/AddressMissmatchDrawer'; import { RouteFees } from '../../../components/RouteFees/RouteFees'; import { EventTargetContext } from '../../../context/event-target-context/EventTargetContext'; import { convertToNetworkChangeableProvider } from '../../../functions/convertToNetworkChangeableProvider'; import { getDurationFormatted } from '../../../functions/getDurationFormatted'; import { getFormattedAmounts, getFormattedNumber } from '../../../functions/getFormattedNumber'; import { SquidFooter } from '../../../lib/squid/components/SquidFooter'; import { SQUID_NATIVE_TOKEN, } from '../../../lib/squid/config'; import { verifyAndSwitchChain } from '../../../lib/squid/functions/verifyAndSwitchChain'; import { useError } from '../hooks/useError'; import { sendAddTokensSuccessEvent, } from '../AddTokensWidgetEvents'; import { useErrorHandler } from '../hooks/useErrorHandler'; import { AddTokensHandoverStep, useHandoverConfig } from '../hooks/useHandoverConfig'; import { getFromAmountData } from '../../../lib/squid/functions/routeCalculation'; interface ReviewProps { data: AddTokensReviewData; showBackButton: boolean; onBackButtonClick?: () => void; onCloseButtonClick?: () => void; } const dividerSx = { content: "''", pos: 'absolute', left: 'base.spacing.x6', w: '1px', bg: 'base.color.translucent.standard.500', }; export function Review({ data, showBackButton = false, onBackButtonClick, onCloseButtonClick, }: ReviewProps) { const { viewDispatch } = useContext(ViewContext); const { addTokensState: { squid, chains, tokens, }, } = useContext(AddTokensContext); const { providersState: { checkout, fromProvider, fromAddress, toAddress, fromProviderInfo, }, } = useProvidersContext(); const { showErrorHandover } = useError(checkout.config.environment); const { eventTargetState: { eventTarget }, } = useContext(EventTargetContext); const [route, setRoute] = useState(); const [proceedDisabled, setProceedDisabled] = useState(true); const [showFeeBreakdown, setShowFeeBreakdown] = useState(false); const [showSecuringQuote, setShowSecuringQuote] = useState(false); const [showAddressMissmatchDrawer, setShowAddressMissmatchDrawer] = useState(false); const { getRoute } = useRoutes(); const { showHandover } = useHandoverConfig(checkout.config.environment); const { onTransactionError } = useErrorHandler(); const { getAllowance, approve, execute, getStatus, } = useExecute(onTransactionError); const getFromAmountAndRoute = async () => { if (!squid || !tokens) return; if (!fromAddress || !toAddress) return; setShowSecuringQuote(true); const updatedAmountData = getFromAmountData( tokens, data.balance, data.toAmount, data.toChainId, data.toTokenAddress === 'native' ? SQUID_NATIVE_TOKEN : data.toTokenAddress, data.additionalBuffer, ); if (!updatedAmountData) return; const routeResponse = await getRoute( squid, updatedAmountData?.fromToken, updatedAmountData?.toToken, toAddress, updatedAmountData.fromAmount, updatedAmountData.toAmount, fromAddress, false, ); setRoute(routeResponse.route); setProceedDisabled(false); setShowSecuringQuote(false); if (routeResponse?.route === undefined) { showErrorHandover(AddTokensErrorTypes.ROUTE_ERROR); } }; const { fromChain, toChain } = useMemo( () => getRouteChains(chains, route), [chains, route], ); const getRouteIntervalIdRef = useInterval(getFromAmountAndRoute, 20000); useEffect(() => { getFromAmountAndRoute(); }, []); const { totalFees, totalFeesUsd } = useMemo( () => getTotalRouteFees(route), [route], ); const openFeeBreakdown = () => { setShowFeeBreakdown(true); }; const routeFees = useMemo(() => { if (totalFeesUsd) { return ( openFeeBreakdown()} size="small" sx={{ ...hFlex, alignItems: 'center', c: 'base.color.text.body.secondary', cursor: 'pointer', }} > {t('views.ADD_TOKENS.fees.includedFees')} {` ${t( 'views.ADD_TOKENS.fees.fiatPricePrefix', )} $${getFormattedAmounts(totalFeesUsd)}`} ); } return ( {t('views.ADD_TOKENS.fees.zeroFees')} ); }, [totalFeesUsd]); const formattedDuration = route ? getDurationFormatted( route.route.estimate.estimatedRouteDuration, t('views.ADD_TOKENS.routeSelection.minutesText'), t('views.ADD_TOKENS.routeSelection.minuteText'), t('views.ADD_TOKENS.routeSelection.secondsText'), ) : ''; const handleTransaction = useCallback(async () => { if (!squid || !fromProvider || !route || !fromProviderInfo) { return; } let currentFromAddress = ''; try { currentFromAddress = await (await fromProvider.getSigner()).getAddress(); } catch (error) { showErrorHandover(AddTokensErrorTypes.PROVIDER_ERROR); return; } if (currentFromAddress !== fromAddress) { setShowAddressMissmatchDrawer(true); return; } clearInterval(getRouteIntervalIdRef.current); setProceedDisabled(true); showHandover(AddTokensHandoverStep.PREPARING, {}); const changeableProvider = await convertToNetworkChangeableProvider( fromProvider, ); const verifyChainResult = await verifyAndSwitchChain( changeableProvider, route.route.params.fromChain, ); if (!verifyChainResult.isChainCorrect) { return; } const allowance = await getAllowance(changeableProvider, route); const { fromAmount } = route.route.params; if (allowance && allowance < BigInt(fromAmount)) { showHandover(AddTokensHandoverStep.REQUEST_APPROVAL, {}); const approveTxnReceipt = await approve(fromProviderInfo, changeableProvider, route); if (!approveTxnReceipt) { return; } showHandover(AddTokensHandoverStep.APPROVAL_CONFIRMED, {}); } showHandover(AddTokensHandoverStep.REQUEST_EXECUTION, {}); const executeTxnReceipt = await execute(squid, fromProviderInfo, changeableProvider, route); if (executeTxnReceipt) { sendAddTokensSuccessEvent(eventTarget, executeTxnReceipt.hash); if (toChain === fromChain) { showHandover(AddTokensHandoverStep.SUCCESS_ZKEVM, { transactionHash: executeTxnReceipt.hash }); return; } showHandover(AddTokensHandoverStep.EXECUTING, { routeDuration: formattedDuration, transactionHash: executeTxnReceipt.hash, }); const status = await getStatus(squid, executeTxnReceipt.hash); const axelarscanUrl = `https://axelarscan.io/gmp/${executeTxnReceipt?.hash}`; if (status?.squidTransactionStatus === 'success') { showHandover(AddTokensHandoverStep.SUCCESS, { axelarscanUrl }); } else if (status?.squidTransactionStatus === 'needs_gas') { showHandover(AddTokensHandoverStep.NEEDS_GAS, { axelarscanUrl }); } else if (status?.squidTransactionStatus === 'partial_success') { showHandover(AddTokensHandoverStep.PARTIAL_SUCCESS, { axelarscanUrl }); } else { showHandover(AddTokensHandoverStep.FAIL, { axelarscanUrl }); } } }, [ route, squid, fromProvider, getRouteIntervalIdRef, approve, showHandover, getAllowance, execute, viewDispatch, ]); return ( } direction="row" sx={{ pt: 'base.spacing.x4', px: 'base.spacing.x5', h: 'base.spacing.x18', w: '100%', }} justifyContent="flex-start" > {showBackButton && ( )} )} > {!!route && ( <> {t('views.ADD_TOKENS.review.heading')} )} circularFrame size="large" /> } emphasized /> {t('views.ADD_TOKENS.review.send')} {' '} {route.route.estimate.fromToken.symbol} {fromChain?.name} {`${t('views.ADD_TOKENS.fees.fiatPricePrefix')} $${ getFormattedAmounts(route?.route.estimate.fromAmountUSD ?? '') }`} {/* */} {/* */} } size="medium" padded /> {t('views.ADD_TOKENS.review.swap')} {' '} {route.route.estimate.fromToken.symbol} {' '} {t('views.ADD_TOKENS.review.to')} {' '} {route.route.estimate.toToken.symbol} {t('views.ADD_TOKENS.review.poweredBySquid')}
1 {route.route.estimate.fromToken.symbol} {' '} = {' '} {getFormattedAmounts( route.route.estimate.exchangeRate, )} {' '} {route.route.estimate.toToken.symbol}
{/* */} {/* */} )} circularFrame size="large" /> } emphasized /> {t('views.ADD_TOKENS.review.receive')} {' '} {route?.route.estimate.toToken.symbol} {toChain?.name} {`${t('views.ADD_TOKENS.fees.fiatPricePrefix')} $${ getFormattedAmounts(route?.route.estimate.toAmountUSD ?? '') }`} {/* */} {/* */} {formattedDuration} {routeFees} )} {!route && !showAddressMissmatchDrawer && showSecuringQuote && ( )} setShowFeeBreakdown(false)} totalAmount={totalFees} totalFiatAmount={totalFeesUsd} /> { setShowAddressMissmatchDrawer(false); viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: AddTokensWidgetViews.ADD_TOKENS, }, }, }); }} />
); }