/ src / components / transactions / Bridge / useGetBridgeMessage.tsx
useGetBridgeMessage.tsx
  1  import { BigNumber, constants, Contract, utils } from 'ethers';
  2  import { formatEther, formatUnits, parseUnits } from 'ethers/lib/utils';
  3  import debounce from 'lodash/debounce';
  4  import { useEffect, useMemo, useState } from 'react';
  5  import { getProvider } from 'src/utils/marketsAndNetworksConfig';
  6  
  7  import { MessageDetails, TokenAmount } from './BridgeActions';
  8  import { getChainSelectorFor, getRouterFor, laneConfig } from './BridgeConfig';
  9  import oracleAbi from './Oracle-abi.json';
 10  import routerAbi from './Router-abi.json';
 11  
 12  export const useGetBridgeMessage = ({
 13    sourceChainId,
 14    destinationChainId,
 15    amount,
 16    sourceTokenAddress,
 17    destinationAccount,
 18    feeToken,
 19    feeTokenOracle,
 20  }: {
 21    sourceChainId: number;
 22    destinationChainId: number;
 23    amount: string;
 24    sourceTokenAddress: string;
 25    destinationAccount: string;
 26    feeToken: string;
 27    feeTokenOracle: string;
 28  }) => {
 29    const [message, setMessage] = useState<MessageDetails>();
 30    const [bridgeFee, setBridgeFee] = useState('');
 31    const [bridgeFeeFormatted, setBridgeFeeFormatted] = useState('');
 32    const [latestAnswer, setLatestAnswer] = useState('');
 33    const [loading, setLoading] = useState(false);
 34    const [error, setError] = useState<string | undefined>();
 35  
 36    const debounced = useMemo(() => {
 37      return debounce(async () => {
 38        const provider = getProvider(sourceChainId);
 39        const sourceRouterAddress = getRouterFor(sourceChainId);
 40        const sourceRouter = new Contract(sourceRouterAddress, routerAbi, provider);
 41  
 42        try {
 43          const tokenAmounts: TokenAmount[] = [
 44            {
 45              token: sourceTokenAddress,
 46              amount: parseUnits(amount, 18).toString() || '0',
 47            },
 48          ];
 49  
 50          const functionSelector = utils.id('CCIP EVMExtraArgsV1').slice(0, 10);
 51  
 52          // "extraArgs" is a structure that can be represented as ['uint256']
 53          // extraArgs are { gasLimit: 0 }
 54          // we set gasLimit specifically to 0 because we are not sending any data so we are not expecting a receiving contract to handle data
 55          const extraArgs = utils.defaultAbiCoder.encode(['uint256'], [0]);
 56          const encodedExtraArgs = functionSelector + extraArgs.slice(2);
 57  
 58          const message: MessageDetails = {
 59            receiver: utils.defaultAbiCoder.encode(['address'], [destinationAccount]),
 60            data: '0x', // no data
 61            tokenAmounts: tokenAmounts,
 62            feeToken: feeToken,
 63            extraArgs: encodedExtraArgs,
 64          };
 65  
 66          const destinationChainSelector = getChainSelectorFor(destinationChainId);
 67          const fees: BigNumber = await sourceRouter.getFee(destinationChainSelector, message);
 68  
 69          const amountBN = utils.parseUnits(amount, 18);
 70          const updatedAmount = amountBN.sub(fees);
 71  
 72          // If the fee token is not the native token, we need to update the tokenAmounts to subtract fees
 73          if (feeToken !== constants.AddressZero) {
 74            message.tokenAmounts = [
 75              {
 76                token: sourceTokenAddress,
 77                amount: updatedAmount.toString(),
 78              },
 79            ];
 80          }
 81  
 82          const sourceLaneConfig = laneConfig.find(
 83            (config) => config.sourceChainId === sourceChainId
 84          );
 85  
 86          if (!sourceLaneConfig) {
 87            setLoading(false);
 88            throw Error(`No lane config found for chain ${sourceChainId}`);
 89          }
 90          let transactionCostUsd;
 91  
 92          if (feeToken === constants.AddressZero) {
 93            console.log('fee token ETH');
 94            // Handling for Ether (native token)
 95            const sourceLaneConfig = laneConfig.find(
 96              (config) => config.sourceChainId === sourceChainId
 97            );
 98            if (!sourceLaneConfig) {
 99              setLoading(false);
100              throw Error(`No lane config found for chain ${sourceChainId}`);
101            }
102  
103            const sourceAssetOracle = new Contract(
104              sourceLaneConfig.wrappedNativeOracle,
105              oracleAbi,
106              provider
107            );
108  
109            const [latestPrice, decimals]: [BigNumber, number] = await Promise.all([
110              sourceAssetOracle.latestAnswer(),
111              sourceAssetOracle.decimals(),
112            ]);
113  
114            const ethUsdPrice = formatUnits(latestPrice, decimals);
115            transactionCostUsd = Number(formatUnits(fees, 18)) * Number(ethUsdPrice);
116          } else {
117            // Handling for GHO or other tokens
118            const sourceLaneConfig = laneConfig.find(
119              (config) => config.sourceChainId === sourceChainId
120            );
121            if (!sourceLaneConfig) {
122              setLoading(false);
123              throw Error(`No lane config found for chain ${sourceChainId}`);
124            }
125  
126            const sourceTokenOracle = new Contract(feeTokenOracle, oracleAbi, provider);
127  
128            const [latestPrice, decimals]: [BigNumber, number] = await Promise.all([
129              sourceTokenOracle.latestAnswer(),
130              sourceTokenOracle.decimals(),
131            ]);
132  
133            const tokenUsdPrice = formatUnits(latestPrice, decimals);
134            transactionCostUsd = Number(formatUnits(fees, 18)) * Number(tokenUsdPrice);
135          }
136  
137          setLatestAnswer(transactionCostUsd.toString());
138          setMessage(message);
139          setBridgeFeeFormatted(formatEther(fees));
140          setBridgeFee(fees.toString());
141        } catch (e) {
142          setError(e.message);
143          console.error(e);
144        } finally {
145          setLoading(false);
146        }
147      }, 500);
148    }, [amount, destinationChainId, sourceChainId, sourceTokenAddress, destinationAccount, feeToken]);
149  
150    useEffect(() => {
151      if (amount && sourceTokenAddress) {
152        setLoading(true);
153        debounced();
154      } else {
155        setLoading(false);
156        setMessage(undefined);
157        setBridgeFee('');
158        setBridgeFeeFormatted('');
159      }
160  
161      return () => {
162        debounced.cancel();
163      };
164    }, [amount, debounced, sourceTokenAddress, feeToken]);
165  
166    return {
167      message,
168      bridgeFee,
169      bridgeFeeFormatted,
170      loading,
171      latestAnswer,
172      error,
173    };
174  };