import { useCallback, useMemo, useEffect, useState, useContext } from "react"; import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; import { useLocation } from "react-router-dom"; import useInterval from "@use-it/interval"; import { ethers, BigNumber } from "ethers"; import { bindActionCreators } from "redux"; import { getDepositBox, isValidAddress, TOKENS_LIST, PROVIDERS, TransactionError, ChainId, MAX_APPROVAL_AMOUNT, optimismErc20Pairs, bobaErc20Pairs, tagAddress, validateContractAndChain, } from "utils"; import type { RootState, AppDispatch } from "./"; import { ErrorContext } from "context/ErrorContext"; import { update, disconnect, error as errorAction } from "./connection"; import { token as tokenAction, amount as amountAction, fromChain as fromChainAction, toChain as toChainAction, toAddress as toAddressAction, error as sendErrorAction, } from "./send"; import chainApi, { useAllowance, useBridgeFees } from "./chainApi"; import { add } from "./transactions"; import { deposit as depositAction, toggle } from "./deposits"; import { useERC20 } from "hooks"; import { across } from "@uma/sdk"; import { Bridge } from "arb-ts"; const { clients } = across; const { OptimismBridgeClient } = clients.optimismBridge; const { BobaBridgeClient } = clients.bobaBridge; const FEE_ESTIMATION = "0.004"; // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch = () => useDispatch<AppDispatch>(); export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; function useQuery() { const { search } = useLocation(); return useMemo(() => { const params = new URLSearchParams(search); return Object.fromEntries(params.entries()); }, [search]); } export function useConnection() { const { account, signer, provider, error, chainId, notify, name } = useAppSelector((state) => state.connection); const dispatch = useAppDispatch(); const actions = useMemo( () => bindActionCreators({ update, disconnect, errorAction }, dispatch), [dispatch] ); const isConnected = !!chainId && !!signer && !!account; return { account, chainId, provider, signer, error, isConnected, setUpdate: actions.update, disconnect: actions.disconnect, setError: actions.errorAction, notify, name, }; } // TODO: put this back into global state. Wasnt able to get it working. export function useL2Block() { const { currentlySelectedFromChain } = useAppSelector((state) => state.send); const [latestBlock, setBlock] = useState< ethers.providers.Block | undefined >(); const { addError, removeError, error } = useContext(ErrorContext); useEffect(() => { const provider = PROVIDERS[currentlySelectedFromChain.chainId](); provider .getBlock("latest") .then((res) => { if (error) removeError(); setBlock(res); }) .catch(() => { addError(new Error("Infura issue, please try again later.")); console.error("Error getting latest block"); }); }, [currentlySelectedFromChain.chainId, error, removeError, addError]); useInterval(() => { const provider = PROVIDERS[currentlySelectedFromChain.chainId](); provider .getBlock("latest") .then((block) => { setBlock(block); }) .catch(() => { console.error("Error getting latest block"); }); return () => { provider.removeAllListeners(); }; }, 30 * 1000); return { block: latestBlock }; } export function useSend() { const dispatch = useAppDispatch(); const actions = bindActionCreators( { tokenAction, amountAction, fromChainAction, toChainAction, toAddressAction, sendErrorAction, }, dispatch ); const send = useAppSelector((state) => state.send); const sendAcross = useSendAcross(); const sendOptimism = useSendOptimism(); const sendArbitrum = useSendArbitrum(); const sendBoba = useSendBoba(); const setSend = { setToken: actions.tokenAction, setAmount: actions.amountAction, setFromChain: actions.fromChainAction, setToChain: actions.toChainAction, setToAddress: actions.toAddressAction, setError: actions.sendErrorAction, }; if (send.fromChain === ChainId.MAINNET && send.toChain === ChainId.OPTIMISM) { return { ...send, ...setSend, ...sendOptimism, }; } if (send.fromChain === ChainId.MAINNET && send.toChain === ChainId.ARBITRUM) { return { ...send, ...setSend, ...sendArbitrum, }; } if (send.fromChain === ChainId.MAINNET && send.toChain === ChainId.BOBA) { return { ...send, ...setSend, ...sendBoba, }; } return { ...send, ...setSend, ...sendAcross, }; } export function useSendAcross() { const { referrer } = useQuery(); const { isConnected, chainId, account, signer } = useConnection(); const { fromChain, toChain, toAddress, amount, token, error, currentlySelectedFromChain, currentlySelectedToChain, } = useAppSelector((state) => state.send); const { balance } = useBalance({ chainId: currentlySelectedFromChain.chainId, account, tokenAddress: token, }); const { block } = useL2Block(); const depositBox = getDepositBox(currentlySelectedFromChain.chainId); const { data: allowance } = useAllowance( { chainId: currentlySelectedFromChain.chainId, token, owner: account!, spender: depositBox.address, amount, }, { skip: !account || !isConnected || !depositBox } ); const { approve: rawApprove } = useERC20(token); const canApprove = balance.gte(amount) && amount.gte(0); const hasToApprove = allowance?.hasToApprove ?? false; async function approve() { return rawApprove({ amount: MAX_APPROVAL_AMOUNT, spender: depositBox.address, signer, }); } const hasToSwitchChain = isConnected && currentlySelectedFromChain.chainId !== chainId; const tokenSymbol = TOKENS_LIST[currentlySelectedFromChain.chainId].find( (t) => t.address === token )?.symbol ?? ""; const { data: fees, isFetching: isFetchingFees } = useBridgeFees( { amount, tokenSymbol, blockTime: block?.timestamp!, }, { skip: tokenSymbol === "" || amount.lte(0) || !block?.timestamp } ); const canSend = useMemo( () => currentlySelectedFromChain.chainId && block && currentlySelectedToChain.chainId && amount && token && fees && !isFetchingFees && toAddress && isValidAddress(toAddress) && !hasToApprove && !hasToSwitchChain && !error && !fees.isAmountTooLow && !fees.isLiquidityInsufficient && balance .sub( token === "0x0000000000000000000000000000000000000000" ? BigNumber.from(ethers.utils.parseEther(FEE_ESTIMATION)) : BigNumber.from("0") ) .gte(amount), [ currentlySelectedFromChain.chainId, block, currentlySelectedToChain.chainId, amount, token, fees, isFetchingFees, toAddress, hasToApprove, hasToSwitchChain, error, balance, ] ); const send = useCallback(async () => { if ( !signer || !canSend || !fees || isFetchingFees || !toAddress || !block ) { return {}; } try { const depositBox = getDepositBox( currentlySelectedFromChain.chainId, signer ); const isETH = token === ethers.constants.AddressZero; const value = isETH ? amount : ethers.constants.Zero; const l2Token = isETH ? TOKENS_LIST[currentlySelectedFromChain.chainId][0].address : token; const { instantRelayFee, slowRelayFee } = fees; let timestamp = block.timestamp; const data = depositBox.interface.encodeFunctionData("deposit", [ toAddress, l2Token, amount, slowRelayFee.pct, instantRelayFee.pct, timestamp, ]); // do not tag a referrer if data is not provided as a hex string. const taggedData = referrer && ethers.utils.isAddress(referrer) ? tagAddress(data, referrer) : data; if ( !(await validateContractAndChain( depositBox.address, currentlySelectedFromChain.chainId, signer )) ) { return {}; } const tx = await signer.sendTransaction({ data: taggedData, value, to: depositBox.address, chainId: currentlySelectedFromChain.chainId, }); return { tx, fees }; } catch (e) { throw new TransactionError( depositBox.address, "deposit", toAddress, token, amount, fees.slowRelayFee.pct, fees.instantRelayFee.pct, block.timestamp ); } }, [ amount, block, canSend, depositBox.address, fees, isFetchingFees, currentlySelectedFromChain.chainId, signer, toAddress, token, referrer, ]); return { fromChain, toChain, toAddress, amount, token, error, canSend, canApprove, hasToApprove, hasToSwitchChain, send, approve, fees: isFetchingFees ? undefined : fees, spender: depositBox.address, }; } export function useTransactions() { const { transactions } = useAppSelector((state) => state.transactions); const dispatch = useAppDispatch(); const actions = bindActionCreators({ add }, dispatch); return { transactions, addTransaction: actions.add, }; } export function useDeposits() { const { deposit, showConfirmationScreen } = useAppSelector( (state) => state.deposits ); const dispatch = useAppDispatch(); const actions = bindActionCreators({ depositAction, toggle }, dispatch); return { deposit, showConfirmationScreen, toggle: actions.toggle, addDeposit: actions.depositAction, }; } export { useAllowance, useBalances, useETHBalance, useBridgeFees, } from "./chainApi"; type useBalanceParams = { chainId: ChainId; account?: string; tokenAddress: string; }; export function useBalance({ chainId, account, tokenAddress, }: useBalanceParams) { const { data: allBalances, refetch } = chainApi.endpoints.balances.useQuery( { account: account ?? "", chainId, }, { skip: !account } ); const selectedIndex = useMemo( () => TOKENS_LIST[chainId].findIndex(({ address }) => address === tokenAddress), [chainId, tokenAddress] ); const balance = allBalances?.[selectedIndex] ?? ethers.BigNumber.from(0); return { balance, refetch, }; } export function useSendOptimism() { const [optimismBridge] = useState(new OptimismBridgeClient()); const { isConnected, chainId, account, signer } = useConnection(); const { fromChain, amount, token, currentlySelectedFromChain, currentlySelectedToChain, toAddress, error, } = useAppSelector((state) => state.send); const { block } = useL2Block(); const { balance: balanceStr } = useBalance({ chainId: fromChain, account, tokenAddress: token, }); const bridgeAddress = useMemo(() => { try { return optimismBridge.getL1BridgeAddress(chainId as number); } catch (error) { return ""; } }, [optimismBridge, chainId]); const balance = BigNumber.from(balanceStr); const { data: allowance } = useAllowance( { chainId: fromChain, token, owner: account!, spender: bridgeAddress, amount, }, { skip: !account || !isConnected || !chainId } ); const canApprove = balance.gte(amount) && amount.gte(0); const hasToApprove = allowance?.hasToApprove ?? true; //TODO: Add fees const [fees] = useState({ instantRelayFee: { total: BigNumber.from("0"), pct: BigNumber.from("0"), }, slowRelayFee: { total: BigNumber.from("0"), pct: BigNumber.from("0"), }, lpFee: { total: BigNumber.from("0"), pct: BigNumber.from("0"), }, isAmountTooLow: false, isLiquidityInsufficient: false, }); const send = useCallback(async () => { if (!isConnected || !signer) return {}; if (!(await validateContractAndChain(bridgeAddress, fromChain, signer))) { return {}; } if (token === ethers.constants.AddressZero) { return { tx: await optimismBridge.depositEth(signer, amount), fees, }; } else { const pairToken = optimismErc20Pairs()[token]; if (!pairToken) return {}; return { tx: await optimismBridge.depositERC20(signer, token, pairToken, amount), fees, }; } }, [ amount, fees, token, isConnected, optimismBridge, signer, fromChain, bridgeAddress, ]); const approve = useCallback(() => { if (!signer) return; return optimismBridge.approve(signer, token, MAX_APPROVAL_AMOUNT); }, [optimismBridge, signer, token]); const hasToSwitchChain = isConnected && currentlySelectedFromChain.chainId !== chainId; const canSend = useMemo( () => currentlySelectedFromChain.chainId && block && currentlySelectedToChain.chainId && amount && token && fees && toAddress && isValidAddress(toAddress) && !hasToApprove && !hasToSwitchChain && !error && balance.gte(amount), [ currentlySelectedFromChain.chainId, block, currentlySelectedToChain.chainId, amount, token, fees, toAddress, hasToApprove, hasToSwitchChain, error, balance, ] ); return { canSend, canApprove, hasToApprove, hasToSwitchChain, send, approve, fees, spender: bridgeAddress, }; } export function useSendArbitrum() { const [bridge, setBridge] = useState<Bridge | undefined>(); const [bridgeAddress, setBridgeAddress] = useState(""); const { isConnected, chainId, account, signer } = useConnection(); const { fromChain, toChain, toAddress, amount, token, currentlySelectedFromChain, currentlySelectedToChain, error, } = useAppSelector((state) => state.send); const { block } = useL2Block(); const { balance: balanceStr } = useBalance({ chainId: fromChain, account, tokenAddress: token, }); const balance = BigNumber.from(balanceStr); const [refetchAllowance, { data: allowance }] = chainApi.endpoints.allowance.useLazyQuery(); const canApprove = balance.gte(amount) && amount.gte(0); const hasToApprove = allowance?.hasToApprove ?? true; //TODO: Add fees const [fees] = useState({ instantRelayFee: { total: BigNumber.from("0"), pct: BigNumber.from("0"), }, slowRelayFee: { total: BigNumber.from("0"), pct: BigNumber.from("0"), }, lpFee: { total: BigNumber.from("0"), pct: BigNumber.from("0"), }, isAmountTooLow: false, isLiquidityInsufficient: false, }); useEffect(() => { initBridgeClient(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [signer, account, fromChain, toChain, isConnected]); const initBridgeClient = async () => { if (!signer || !account) return; if (fromChain !== ChainId.MAINNET) return; if (toChain !== ChainId.ARBITRUM) return; if (!isConnected) return; const provider = PROVIDERS[ChainId.ARBITRUM](); try { const bridge = await Bridge.init(signer, provider.getSigner(account)); setBridge(bridge); } catch (error) { console.error(error); } }; const send = useCallback(async () => { if (!bridge || !isConnected) return {}; if ( !(await validateContractAndChain( ( await bridge.l1Bridge.getInbox() ).address, fromChain, bridge.l1Bridge.l1Signer )) ) { return {}; } if (token === ethers.constants.AddressZero) { return { tx: await bridge.depositETH(amount), fees, }; } else { const depositParams = await bridge.getDepositTxParams({ erc20L1Address: token, amount, destinationAddress: toAddress, }); return { tx: await bridge.deposit(depositParams), fees, }; } }, [bridge, amount, fees, token, isConnected, toAddress, fromChain]); const approve = useCallback(() => { if (!bridge) return; return bridge.approveToken(token, MAX_APPROVAL_AMOUNT); }, [bridge, token]); useEffect(() => { if (!bridge || !account || !token || !chainId || !amount) return; bridge.l1Bridge .getGatewayAddress(token) .then((spender) => { setBridgeAddress(spender); return refetchAllowance({ owner: account, spender, chainId, token, amount, }); }) .catch(console.error); }, [bridge, amount, token, chainId, account, refetchAllowance]); const hasToSwitchChain = isConnected && currentlySelectedFromChain.chainId !== chainId; const canSend = useMemo( () => currentlySelectedFromChain.chainId && block && currentlySelectedToChain.chainId && amount && token && fees && toAddress && isValidAddress(toAddress) && !hasToApprove && !hasToSwitchChain && !error && balance.gte(amount), [ currentlySelectedFromChain.chainId, block, currentlySelectedToChain.chainId, amount, token, fees, toAddress, hasToApprove, hasToSwitchChain, error, balance, ] ); return { canSend, canApprove, hasToApprove, hasToSwitchChain, send, approve, fees, spender: bridgeAddress, }; } export function useSendBoba() { const [bobaBridge] = useState(new BobaBridgeClient()); const [bridgeAddress, setBridgeAddress] = useState(""); const { isConnected, chainId, account, signer } = useConnection(); const { fromChain, amount, token, currentlySelectedFromChain, currentlySelectedToChain, toAddress, error, } = useAppSelector((state) => state.send); const { block } = useL2Block(); const { balance: balanceStr } = useBalance({ chainId: fromChain, account, tokenAddress: token, }); const balance = BigNumber.from(balanceStr); const { data: allowance } = useAllowance( { chainId: fromChain, token, owner: account!, spender: bridgeAddress, amount, }, { skip: !account || !isConnected || !chainId } ); const canApprove = balance.gte(amount) && amount.gte(0); const hasToApprove = allowance?.hasToApprove ?? true; //TODO: Add fees const [fees] = useState({ instantRelayFee: { total: BigNumber.from("0"), pct: BigNumber.from("0"), }, slowRelayFee: { total: BigNumber.from("0"), pct: BigNumber.from("0"), }, lpFee: { total: BigNumber.from("0"), pct: BigNumber.from("0"), }, isAmountTooLow: false, isLiquidityInsufficient: false, }); useEffect(() => { if (!bobaBridge) return; bobaBridge .getL1BridgeAddress(chainId as number, PROVIDERS[ChainId.MAINNET]()) .then((address) => { setBridgeAddress(address); }) .catch(() => { setBridgeAddress(""); }); }, [bobaBridge, chainId]); const send = useCallback(async () => { if (!isConnected || !signer) return {}; if (!(await validateContractAndChain(bridgeAddress, fromChain, signer))) { return {}; } if (token === ethers.constants.AddressZero) { return { tx: await bobaBridge.depositEth(signer, amount), fees, }; } else { const pairToken = bobaErc20Pairs()[token]; if (!pairToken) return {}; return { tx: await bobaBridge.depositERC20(signer, token, pairToken, amount), fees, }; } }, [ amount, fees, token, isConnected, bobaBridge, signer, fromChain, bridgeAddress, ]); const approve = useCallback(() => { if (!signer) return; return bobaBridge.approve(signer, token, MAX_APPROVAL_AMOUNT); }, [bobaBridge, signer, token]); const hasToSwitchChain = isConnected && currentlySelectedFromChain.chainId !== chainId; const canSend = useMemo( () => currentlySelectedFromChain.chainId && block && currentlySelectedToChain.chainId && amount && token && fees && toAddress && isValidAddress(toAddress) && !hasToApprove && !hasToSwitchChain && !error && balance.gte(amount), [ currentlySelectedFromChain.chainId, block, currentlySelectedToChain.chainId, amount, token, fees, toAddress, hasToApprove, hasToSwitchChain, error, balance, ] ); return { canSend, canApprove, hasToApprove, hasToSwitchChain, send, approve, fees, spender: bridgeAddress, }; }