BridgeActions.tsx
1 import { Trans } from '@lingui/macro'; 2 import { BoxProps } from '@mui/material'; 3 import { useQueryClient } from '@tanstack/react-query'; 4 import { Contract } from 'ethers'; 5 import { parseUnits } from 'ethers/lib/utils'; 6 import React, { useEffect } from 'react'; 7 import { useApprovalTx } from 'src/hooks/useApprovalTx'; 8 import { useApprovedAmount } from 'src/hooks/useApprovedAmount'; 9 import { useModalContext } from 'src/hooks/useModal'; 10 import { useWeb3Context } from 'src/libs/hooks/useWeb3Context'; 11 import { useRootStore } from 'src/store/root'; 12 import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping'; 13 import { GHO_SYMBOL } from 'src/utils/ghoUtilities'; 14 import { getProvider } from 'src/utils/marketsAndNetworksConfig'; 15 16 import { TxActionsWrapper } from '../TxActionsWrapper'; 17 import { APPROVAL_GAS_LIMIT, checkRequiresApproval } from '../utils'; 18 import { bridgeGasLimit, getChainSelectorFor, getRouterFor } from './BridgeConfig'; 19 import routerAbi from './Router-abi.json'; 20 21 export interface TokenAmount { 22 token: string; 23 amount: string; 24 } 25 26 export interface MessageDetails { 27 receiver: string; 28 data: string; 29 tokenAmounts: TokenAmount[]; 30 feeToken: string; 31 extraArgs: string; 32 } 33 34 export interface BridgeActionProps extends BoxProps { 35 amountToBridge: string; 36 isWrongNetwork: boolean; 37 symbol: string; 38 blocked: boolean; 39 decimals: number; 40 sourceChainId: number; 41 destinationChainId: number; 42 tokenAddress: string; 43 fees: string; 44 message: MessageDetails | undefined; 45 isCustomFeeToken: boolean; 46 } 47 48 export const BridgeActions = React.memo( 49 ({ 50 amountToBridge, 51 isWrongNetwork, 52 sx, 53 symbol, 54 blocked, 55 decimals, 56 tokenAddress, 57 sourceChainId, 58 destinationChainId, 59 message, 60 fees, 61 isCustomFeeToken, 62 ...props 63 }: BridgeActionProps) => { 64 const { sendTx } = useWeb3Context(); 65 const queryClient = useQueryClient(); 66 const addTransaction = useRootStore((state) => state.addTransaction); 67 68 const { 69 approvalTxState, 70 mainTxState, 71 loadingTxns, 72 setLoadingTxns, 73 setApprovalTxState, 74 setMainTxState, 75 setGasLimit, 76 setTxError, 77 } = useModalContext(); 78 const user = useRootStore((state) => state.account); 79 80 const { 81 data: approvedAmount, 82 refetch: fetchApprovedAmount, 83 isFetching: fetchingApprovedAmount, 84 isFetchedAfterMount, 85 } = useApprovedAmount({ 86 chainId: sourceChainId, 87 token: tokenAddress, 88 spender: getRouterFor(sourceChainId), 89 }); 90 91 setLoadingTxns(fetchingApprovedAmount); 92 93 const requiresApproval = 94 Number(amountToBridge) !== 0 && 95 checkRequiresApproval({ 96 approvedAmount: approvedAmount ? approvedAmount.toString() : '0', 97 amount: amountToBridge, 98 signedAmount: '0', 99 }); 100 101 if (requiresApproval && approvalTxState?.success) { 102 // There was a successful approval tx, but the approval amount is not enough. 103 // Clear the state to prompt for another approval. 104 setApprovalTxState({}); 105 } 106 107 useEffect(() => { 108 if (!isFetchedAfterMount) { 109 fetchApprovedAmount(); 110 } 111 }, [fetchApprovedAmount, isFetchedAfterMount]); 112 113 const { approval } = useApprovalTx({ 114 usePermit: false, 115 approvedAmount: { 116 amount: approvedAmount?.toString() || '0', 117 user: user, 118 token: tokenAddress, 119 spender: getRouterFor(sourceChainId), 120 }, 121 requiresApproval, 122 assetAddress: tokenAddress, 123 symbol: GHO_SYMBOL, 124 decimals: 18, 125 signatureAmount: amountToBridge, 126 onApprovalTxConfirmed: fetchApprovedAmount, 127 chainId: sourceChainId, 128 amountToApprove: parseUnits(amountToBridge || '0', 18).toString(), 129 }); 130 131 // Update gas estimation 132 useEffect(() => { 133 let gasLimit = 0; 134 gasLimit = Number(bridgeGasLimit); 135 if (requiresApproval && !approvalTxState.success) { 136 gasLimit += Number(APPROVAL_GAS_LIMIT); 137 } 138 139 setGasLimit(gasLimit.toString()); 140 }, [requiresApproval, approvalTxState, setGasLimit]); 141 142 const action = async () => { 143 try { 144 setMainTxState({ ...mainTxState, loading: true }); 145 const provider = getProvider(sourceChainId); 146 147 const sourceRouterAddress = getRouterFor(sourceChainId); 148 const sourceRouter = new Contract(sourceRouterAddress, routerAbi, provider); 149 150 const destinationChainSelector = getChainSelectorFor(destinationChainId); 151 152 const txFeeOptions: { value?: string } = {}; 153 154 if (!isCustomFeeToken) { 155 txFeeOptions.value = fees; 156 } 157 158 const populatedTransaction = await sourceRouter.populateTransaction.ccipSend( 159 destinationChainSelector, 160 message, 161 txFeeOptions 162 ); 163 164 const response = await sendTx(populatedTransaction); 165 166 queryClient.invalidateQueries({ queryKey: ['sendRequests', user] }); 167 168 setMainTxState({ 169 txHash: response.hash, 170 loading: false, 171 success: true, 172 }); 173 174 addTransaction(response.hash, { 175 action: 'bridge', 176 txState: 'success', 177 asset: tokenAddress, 178 amount: amountToBridge, 179 assetName: GHO_SYMBOL, 180 }); 181 182 setTxError(undefined); 183 } catch (error) { 184 const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false); 185 setTxError(parsedError); 186 setMainTxState({ 187 txHash: undefined, 188 loading: false, 189 }); 190 } 191 }; 192 193 return ( 194 <TxActionsWrapper 195 blocked={blocked} 196 mainTxState={mainTxState} 197 approvalTxState={approvalTxState} 198 isWrongNetwork={isWrongNetwork} 199 requiresAmount 200 amount={amountToBridge} 201 symbol={symbol} 202 preparingTransactions={loadingTxns || !fees} 203 actionText={<Trans>Bridge {symbol}</Trans>} 204 actionInProgressText={<Trans>Bridging {symbol}</Trans>} 205 handleApproval={approval} 206 handleAction={action} 207 requiresApproval={requiresApproval} 208 tryPermit={false} // permit not availabe 209 sx={sx} 210 {...props} 211 /> 212 ); 213 } 214 );