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 };