import { useCallback, useContext, useMemo, useState, useEffect, } from 'react'; import { Body, Box, Button, Heading, Icon, Logo, MenuItem, } from '@biom3/react'; import { GasEstimateBridgeToL2Result, GasEstimateType, isAddressSanctioned, WrappedBrowserProvider, } from '@imtbl/checkout-sdk'; import { ApproveBridgeResponse, BridgeTxResponse } from '@imtbl/bridge-sdk'; import { useTranslation } from 'react-i18next'; import { formatUnits, parseUnits } from 'ethers'; import { BridgeWidgetViews } from '../../../context/view-context/BridgeViewContextTypes'; import { abbreviateAddress } from '../../../lib/addressUtils'; import { CryptoFiatContext } from '../../../context/crypto-fiat-context/CryptoFiatContext'; import { isWalletConnectProvider, } from '../../../lib/provider'; import { calculateCryptoToFiat, getChainImage, isNativeToken } from '../../../lib/utils'; import { parseChainId } from '../../../lib/chains'; import { DEFAULT_QUOTE_REFRESH_INTERVAL, DEFAULT_TOKEN_DECIMALS, ETH_TOKEN_SYMBOL, IMX_TOKEN_SYMBOL, NATIVE, addChainChangedListener, networkName, removeChainChangedListener, } from '../../../lib'; import { useInterval } from '../../../lib/hooks/useInterval'; import { NetworkSwitchDrawer } from '../../../components/NetworkSwitchDrawer/NetworkSwitchDrawer'; import { useWalletConnect } from '../../../lib/hooks/useWalletConnect'; import { NotEnoughGas } from '../../../components/NotEnoughGas/NotEnoughGas'; import { arrowIconStyles, arrowIconWrapperStyles, bottomMenuItemStyles, bridgeButtonIconLoadingStyle, bridgeReviewHeadingStyles, bridgeReviewWrapperStyles, rawImageStyle, topMenuItemStyles, wcStickerLogoStyles, wcWalletLogoStyles, networkIconStyles, } from './BridgeReviewSummaryStyles'; import { BridgeActions, BridgeContext } from '../context/BridgeContext'; import { SharedViews, ViewActions, ViewContext, } from '../../../context/view-context/ViewContext'; import { Fees } from '../../../components/Fees/Fees'; import { formatBridgeFees } from '../functions/BridgeFees'; import { RawImage } from '../../../components/RawImage/RawImage'; import { getErc20Contract } from '../functions/TransferErc20'; import { WithdrawalQueueDrawer, WithdrawalQueueWarningType, } from '../../../components/WithdrawalQueueDrawer/WithdrawalQueueDrawer'; const testId = 'bridge-review-summary'; export function BridgeReviewSummary() { const { t } = useTranslation(); const { viewDispatch } = useContext(ViewContext); const { bridgeState: { checkout, tokenBridge, from, to, token, amount, tokenBalances, riskAssessment, }, bridgeDispatch, } = useContext(BridgeContext); const { environment } = checkout.config; const { cryptoFiatState } = useContext(CryptoFiatContext); const [loading, setLoading] = useState(false); const [estimates, setEstimates] = useState(undefined); const [gasFee, setGasFee] = useState(''); const [gasFeeFiatValue, setGasFeeFiatValue] = useState(''); const [approveTransaction, setApproveTransaction] = useState< ApproveBridgeResponse | undefined >(undefined); const [transaction, setTransaction] = useState( undefined, ); const [showSwitchNetworkDrawer, setShowSwitchNetworkDrawer] = useState(false); const [fromWalletLogoUrl, setFromWalletLogoUrl] = useState( undefined, ); const [toWalletLogoUrl, setToWalletLogoUrl] = useState( undefined, ); const [fromWalletIsWalletConnect, setFromWalletIsWalletConnect] = useState(false); const [toWalletIsWalletConnect, setToWalletIsWalletConnect] = useState(false); const { isWalletConnectEnabled, getWalletLogoUrl } = useWalletConnect(); // Not enough ETH to cover gas const [showNotEnoughGasDrawer, setShowNotEnoughGasDrawer] = useState(false); const [withdrawalQueueWarning, setWithdrawalQueueWarning] = useState<{ visible: boolean; warningType?: WithdrawalQueueWarningType; threshold?: number; }>({ visible: false }); const isTransfer = useMemo(() => from?.network === to?.network, [from, to]); const isDeposit = useMemo( () => (checkout.config.l2ChainId === to?.network), [from, to, checkout], ); const insufficientFundsForGas = useMemo(() => { if (!estimates) return false; if (!token) return true; const nativeTokenBalance = tokenBalances .find((balance) => isNativeToken(balance.token.address)); let requiredAmount = BigInt(estimates.fees.totalFees); if (isNativeToken(token.address)) { // add native move amount to required amount as they need to cover // the gas + move amount requiredAmount += (parseUnits(amount, token.decimals)); } return !nativeTokenBalance || nativeTokenBalance.balance < requiredAmount; }, [tokenBalances, estimates, token, amount]); const displayAmount = useMemo( () => (token?.symbol ? `${token?.symbol} ${amount}` : `${amount}`), [token, amount], ); const fromFiatAmount = useMemo(() => { if (!amount || !token) return ''; return calculateCryptoToFiat( amount, token.symbol, cryptoFiatState.conversions, ); }, [token, amount]); const fromAddress = useMemo(() => { if (!from) return '-'; return from.walletAddress; }, [from]); const fromNetwork = useMemo(() => from && from.network, [from]); const toAddress = useMemo(() => { if (!to) return '-'; return to.walletAddress; }, [to]); const toNetwork = useMemo(() => to?.network, [to]); const fetchTransferGasEstimate = useCallback(async () => { if (!tokenBridge || !amount || !from || !to || !token) return; const tokenToTransfer = token?.address?.toLowerCase() ?? NATIVE.toUpperCase(); const gasEstimateResult = { gasEstimateType: GasEstimateType.BRIDGE_TO_L2, fees: {}, token: checkout.config.networkMap.get(from!.network)?.nativeCurrency, } as GasEstimateBridgeToL2Result; let estimatePromise: Promise; if (tokenToTransfer === NATIVE.toLowerCase()) { estimatePromise = checkout.providerCall(from.browserProvider, async (provider) => await provider.estimateGas({ to: toAddress, // If 'from' not provided it assumes the transaction is being sent from the zero address. // Estimation will fail unless the amount is within the zero addresses balance. from: fromAddress, value: parseUnits(amount, token.decimals), })); } else { const erc20 = getErc20Contract(tokenToTransfer, await from.browserProvider.getSigner()); estimatePromise = erc20.transfer.estimateGas(toAddress, parseUnits(amount, token.decimals)); } try { const [estimate, gasPrice] = await Promise.all([ estimatePromise, (await from.browserProvider.getFeeData()).gasPrice, ]); const gas = estimate * (gasPrice ?? 0n); const formattedEstimate = formatUnits(gas, DEFAULT_TOKEN_DECIMALS); gasEstimateResult.fees.sourceChainGas = gas; gasEstimateResult.fees.totalFees = gas; setEstimates(gasEstimateResult); setGasFee(formattedEstimate); setGasFeeFiatValue(calculateCryptoToFiat(formattedEstimate, NATIVE.toUpperCase(), cryptoFiatState.conversions)); } catch (e) { // eslint-disable-next-line no-console console.error('Unable to fetch gas estimate', e); } }, [checkout, from, to, token, amount]); const fetchBridgeGasEstimate = useCallback(async () => { if (!tokenBridge || !amount || !from || !to || !token) return; const bundledTxn = await tokenBridge!.getUnsignedBridgeBundledTx({ senderAddress: fromAddress, recipientAddress: toAddress, token: token.address ?? NATIVE.toUpperCase(), amount: parseUnits(amount, token.decimals), sourceChainId: from?.network.toString(), destinationChainId: to?.network.toString(), gasMultiplier: 'auto', }); if (bundledTxn.withdrawalQueueActivated) { setWithdrawalQueueWarning({ visible: true, warningType: WithdrawalQueueWarningType.TYPE_ACTIVE_QUEUE, }); } else if (bundledTxn.delayWithdrawalLargeAmount && bundledTxn.largeTransferThresholds) { const threshold = formatUnits(bundledTxn.largeTransferThresholds, token.decimals); setWithdrawalQueueWarning({ visible: true, warningType: WithdrawalQueueWarningType.TYPE_THRESHOLD, threshold: parseInt(threshold, 10), }); } const unsignedApproveTransaction = { contractToApprove: bundledTxn.contractToApprove, unsignedTx: bundledTxn.unsignedApprovalTx, }; const unsignedTransaction = { feeData: bundledTxn.feeData, unsignedTx: bundledTxn.unsignedBridgeTx, }; setApproveTransaction(unsignedApproveTransaction); setTransaction(unsignedTransaction); // todo: add approval gas fees const transactionFeeData = unsignedTransaction.feeData; const { totalFees, approvalFee } = transactionFeeData; let rawTotalFees = totalFees; if (!unsignedApproveTransaction.unsignedTx) { rawTotalFees = totalFees - approvalFee; transactionFeeData.approvalFee = BigInt(0); } const gasEstimateResult = { gasEstimateType: GasEstimateType.BRIDGE_TO_L2, fees: { ...transactionFeeData, totalFees: rawTotalFees, }, token: checkout.config.networkMap.get(from!.network)?.nativeCurrency, } as GasEstimateBridgeToL2Result; setEstimates(gasEstimateResult); const estimatedAmount = formatUnits( gasEstimateResult?.fees.totalFees || 0, DEFAULT_TOKEN_DECIMALS, ); setGasFee(estimatedAmount); setGasFeeFiatValue( calculateCryptoToFiat( estimatedAmount, gasEstimateResult?.token?.symbol || '', cryptoFiatState.conversions, ), ); }, [checkout, tokenBridge]); useInterval(() => { if (isTransfer) { fetchTransferGasEstimate(); } else { fetchBridgeGasEstimate(); } }, DEFAULT_QUOTE_REFRESH_INTERVAL); const formatFeeBreakdown = useCallback( () => formatBridgeFees(estimates, isDeposit, cryptoFiatState, t), [estimates, isDeposit], ); useEffect(() => { (async () => { setLoading(true); if (isTransfer) { await fetchTransferGasEstimate(); } else { await fetchBridgeGasEstimate(); } setLoading(false); })(); }, []); const handleNetworkSwitch = useCallback((provider: WrappedBrowserProvider) => { bridgeDispatch({ payload: { type: BridgeActions.SET_WALLETS_AND_NETWORKS, from: { browserProvider: provider, walletAddress: from?.walletAddress!, walletProviderInfo: from?.walletProviderInfo!, network: from?.network!, }, to: { browserProvider: to?.browserProvider!, walletAddress: to?.walletAddress!, walletProviderInfo: to?.walletProviderInfo!, network: to?.network!, }, }, }); }, [from?.browserProvider, from?.network, to?.browserProvider, to?.network]); useEffect(() => { if (!from?.browserProvider) return; const handleChainChanged = () => { handleNetworkSwitch(from.browserProvider); setShowSwitchNetworkDrawer(false); }; addChainChangedListener(from.browserProvider, handleChainChanged); // eslint-disable-next-line consistent-return return () => { removeChainChangedListener(from.browserProvider, handleChainChanged); }; }, [from?.browserProvider]); useEffect(() => { if (isWalletConnectEnabled) { const isFromProviderWalletConnect = isWalletConnectProvider(from?.browserProvider); const isToProviderWalletConnect = isWalletConnectProvider(to?.browserProvider); setFromWalletIsWalletConnect(isFromProviderWalletConnect); setToWalletIsWalletConnect(isToProviderWalletConnect); (async () => { if (isFromProviderWalletConnect) { setFromWalletLogoUrl(await getWalletLogoUrl()); } if (isToProviderWalletConnect) { setToWalletLogoUrl(await getWalletLogoUrl()); } })(); } }, [isWalletConnectEnabled, from?.browserProvider, to?.browserProvider]); useEffect(() => { if (insufficientFundsForGas) { setShowNotEnoughGasDrawer(true); } }, [insufficientFundsForGas]); const submitBridge = useCallback(async () => { if (!isTransfer && (!approveTransaction || !transaction)) return; if (!from || !to) return; if (riskAssessment && isAddressSanctioned(riskAssessment)) { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: BridgeWidgetViews.SERVICE_UNAVAILABLE, }, }, }); return; } if (insufficientFundsForGas) { setShowNotEnoughGasDrawer(true); return; } try { const provider = from?.browserProvider?.provider as | { send: (method: string, params: unknown[]) => Promise } | undefined; if (!provider) { setShowSwitchNetworkDrawer(true); return; } const currentChainId = await provider.send('eth_chainId', []); const parsedChainId = parseChainId(currentChainId); if (parsedChainId !== from?.network) { setShowSwitchNetworkDrawer(true); return; } } catch (err) { // eslint-disable-next-line no-console console.error('Current network check failed', err); } viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: BridgeWidgetViews.APPROVE_TRANSACTION, approveTransaction, transaction, }, }, }); }, [ viewDispatch, approveTransaction, transaction, from?.browserProvider, from?.network, from?.walletProviderInfo, to?.browserProvider, to?.network, to?.walletProviderInfo, ]); return ( {t('views.BRIDGE_REVIEW.heading')} {/* From review */} {t('views.BRIDGE_REVIEW.fromLabel.amountHeading')} {(fromWalletIsWalletConnect && fromWalletLogoUrl) ? ( <> ) : (from?.walletProviderInfo && ( ))} {t('views.BRIDGE_REVIEW.fromLabel.heading')} {' '} {abbreviateAddress(fromAddress ?? '')} {fromNetwork && ( )} sx={networkIconStyles} /> )} {/* To review */} {(toWalletIsWalletConnect && toWalletLogoUrl) ? ( <> ) : (to?.walletProviderInfo && ( ))} {t('views.BRIDGE_REVIEW.toLabel.heading')} {' '} {abbreviateAddress(toAddress ?? '')} {toNetwork && ( )} sx={networkIconStyles} /> )} {(estimates && !loading) && ( )} setShowSwitchNetworkDrawer(false)} onNetworkSwitch={handleNetworkSwitch} /> setShowNotEnoughGasDrawer(false)} walletAddress={from?.walletAddress || ''} tokenSymbol={ from?.network === checkout.config.l1ChainId ? ETH_TOKEN_SYMBOL : IMX_TOKEN_SYMBOL } onAddCoinsClick={() => { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: SharedViews.TOP_UP_VIEW, }, }, }); }} /> { setWithdrawalQueueWarning({ visible: false }); viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: BridgeWidgetViews.BRIDGE_FORM, }, }, }); }} onCloseDrawer={() => { setWithdrawalQueueWarning({ visible: false }); }} threshold={withdrawalQueueWarning.threshold} /> ); }