import { Box } from '@biom3/react'; import { useCallback, useContext, useState, } from 'react'; import { CheckoutErrorType } from '@imtbl/checkout-sdk'; import { ApproveBridgeResponse, BridgeTxResponse } from '@imtbl/bridge-sdk'; import { useTranslation } from 'react-i18next'; import { parseUnits } from 'ethers'; import { SimpleLayout } from '../../../components/SimpleLayout/SimpleLayout'; import { HeaderNavigation } from '../../../components/Header/HeaderNavigation'; import { sendBridgeWidgetCloseEvent } from '../BridgeWidgetEvents'; import { FooterButton } from '../../../components/Footer/FooterButton'; import { SimpleTextBody } from '../../../components/Body/SimpleTextBody'; import { SharedViews, ViewActions, ViewContext } from '../../../context/view-context/ViewContext'; import { LoadingView } from '../../../views/loading/LoadingView'; import { BridgeContext } from '../context/BridgeContext'; import { WalletApproveHero } from '../../../components/Hero/WalletApproveHero'; import { EventTargetContext } from '../../../context/event-target-context/EventTargetContext'; import { BridgeWidgetViews } from '../../../context/view-context/BridgeViewContextTypes'; import { FooterLogo } from '../../../components/Footer/FooterLogo'; import { NATIVE } from '../../../lib'; import { getErc20Contract } from '../functions/TransferErc20'; export interface BridgeTransaction { approveTransaction: ApproveBridgeResponse; transaction: BridgeTxResponse; } /** * @param bridgeTransaction if provided the transaction is a bridge, if not it's a transfer */ export interface ApproveTransactionProps { bridgeTransaction: BridgeTransaction | undefined; } export function ApproveTransaction({ bridgeTransaction }: ApproveTransactionProps) { const { t } = useTranslation(); const { bridgeState } = useContext(BridgeContext); const { checkout, from, to, token, amount, } = bridgeState; const { viewDispatch } = useContext(ViewContext); const { eventTargetState: { eventTarget } } = useContext(EventTargetContext); // Local state const [actionDisabled, setActionDisabled] = useState(false); const [txProcessing, setTxProcessing] = useState(false); const [loading, setLoading] = useState(false); const [rejectedBridge, setRejectedBridge] = useState(false); // Common error view function const showErrorView = useCallback(() => { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: SharedViews.ERROR_VIEW, error: new Error('No checkout object or no provider object found'), }, }, }); }, [viewDispatch]); const goBack = useCallback(() => { viewDispatch({ payload: { type: ViewActions.GO_BACK, }, }); }, [viewDispatch]); const handleExceptions = (err) => { if (err.type === CheckoutErrorType.UNPREDICTABLE_GAS_LIMIT) { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: BridgeWidgetViews.BRIDGE_FAILURE, reason: 'Unpredictable gas limit', }, }, }); return; } if (err.type === CheckoutErrorType.TRANSACTION_FAILED || err.type === CheckoutErrorType.INSUFFICIENT_FUNDS || (err.receipt && err.receipt.status === 0)) { let reason = 'Transaction failed'; if (err.type === CheckoutErrorType.INSUFFICIENT_FUNDS) reason = 'Insufficient funds'; if (err.receipt && err.receipt.status === 0) reason = 'Transaction failed to settle on chain'; viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: BridgeWidgetViews.BRIDGE_FAILURE, reason, }, }, }); return; } viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: SharedViews.ERROR_VIEW, error: err, }, }, }); }; const handleApproveTransferClick = useCallback(async () => { if (!checkout || !from?.browserProvider) { showErrorView(); return; } if (actionDisabled) return; setActionDisabled(true); setTxProcessing(true); const tokenToTransfer = token?.address?.toLowerCase() ?? NATIVE.toUpperCase(); let txHash: string; try { if (tokenToTransfer === NATIVE.toLowerCase()) { const request = { to: to?.walletAddress, value: parseUnits(amount, token?.decimals), }; const result = await checkout.sendTransaction({ provider: from.browserProvider, transaction: request, }); txHash = result.transactionResponse.hash; } else { const erc20 = getErc20Contract(tokenToTransfer, await from.browserProvider.getSigner()); const parsedAmount = parseUnits(amount, token?.decimals); const response = await checkout.providerCall( from.browserProvider, async () => await erc20.transfer(to?.walletAddress, parsedAmount), ); txHash = response.hash; } viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: BridgeWidgetViews.IN_PROGRESS, transactionHash: txHash, isTransfer: true, }, }, }); } catch (e: any) { setTxProcessing(false); if (e.type === CheckoutErrorType.USER_REJECTED_REQUEST_ERROR) { setRejectedBridge(true); } else { handleExceptions(e); } } finally { setActionDisabled(false); } }, [ checkout, from, showErrorView, viewDispatch, actionDisabled, ]); const handleApproveBridgeClick = useCallback(async () => { let bridgeRejected = false; // Force unwrap as bridgeTransaction being defined is a requirement for this callback to be invoked const { approveTransaction, transaction } = bridgeTransaction!; if (!checkout || !from?.browserProvider || !transaction) { showErrorView(); return; } if (actionDisabled) return; setActionDisabled(true); // Approvals as required if (approveTransaction.unsignedTx) { try { setTxProcessing(true); const approveSpendingResult = await checkout.sendTransaction({ provider: from.browserProvider, transaction: approveTransaction.unsignedTx, }); const approvalReceipt = await approveSpendingResult.transactionResponse.wait(); if (approvalReceipt?.status !== 1) { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: BridgeWidgetViews.BRIDGE_FAILURE, reason: 'Transaction failed to settle on chain', }, }, }); return; } } catch (error: any) { setTxProcessing(false); if (error.type === CheckoutErrorType.USER_REJECTED_REQUEST_ERROR) { setRejectedBridge(true); bridgeRejected = true; } else { handleExceptions(error); } } finally { setActionDisabled(false); } } try { if (bridgeRejected) return; setTxProcessing(true); const sendResult = await checkout.sendTransaction({ provider: from.browserProvider, transaction: transaction.unsignedTx, }); viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: BridgeWidgetViews.IN_PROGRESS, transactionHash: sendResult.transactionResponse.hash, isTransfer: false, }, }, }); const receipt = await sendResult.transactionResponse.wait(); if (receipt?.status === 0) { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: BridgeWidgetViews.BRIDGE_FAILURE, reason: 'Approval transaction failed to settle on chain', }, }, }); } } catch (error: any) { if (error.type === CheckoutErrorType.USER_REJECTED_REQUEST_ERROR) { setRejectedBridge(true); } else { handleExceptions(error); } } finally { setLoading(false); setTxProcessing(false); setActionDisabled(false); } }, [ checkout, from, showErrorView, viewDispatch, bridgeTransaction, actionDisabled, ]); return ( <> {loading && ()} {!loading && ( sendBridgeWidgetCloseEvent(eventTarget)} onBackButtonClick={goBack} /> )} floatHeader heroContent={} footer={( )} > {t('views.APPROVE_TRANSACTION.content.body')} )} ); }