'use client'; /* eslint-disable react-hooks/rules-of-hooks */ import { getChainExplorer } from '@/core/network/getChainExplorer'; import { useValue } from '@/internal/hooks/useValue'; import { baseSvg } from '@/internal/svg/baseSvg'; import { coinbaseLogoSvg } from '@/internal/svg/coinbaseLogoSvg'; import type { Token } from '@/token'; import { createContext, useCallback, useContext, useEffect, useState, } from 'react'; import { type Address, type Hex, erc20Abi, formatUnits } from 'viem'; import { useAccount, useConfig } from 'wagmi'; import { getBalance, readContract } from 'wagmi/actions'; import { DEFAULT_BRIDGEABLE_TOKENS } from '../constants'; import { useChainConfig } from '../hooks/useAppchainConfig'; import { useDeposit } from '../hooks/useDeposit'; import { useWithdraw } from '../hooks/useWithdraw'; import type { ChainWithIcon } from '../types'; import type { BridgeParams } from '../types'; import type { AppchainBridgeContextType } from '../types'; import type { AppchainBridgeProviderProps } from '../types'; import { defaultPriceFetcher } from '../utils/defaultPriceFetcher'; const AppchainBridgeContext = createContext< AppchainBridgeContextType | undefined >(undefined); export const AppchainBridgeProvider = ({ children, chain, appchain, bridgeableTokens = DEFAULT_BRIDGEABLE_TOKENS, handleFetchPrice = defaultPriceFetcher, }: AppchainBridgeProviderProps) => { // Source network const [from, setFrom] = useState({ ...chain, icon: baseSvg, }); // Destination network const [to, setTo] = useState({ ...appchain.chain, icon: appchain.icon || coinbaseLogoSvg, }); // op-enclave configuration https://github.com/base/op-enclave/blob/main/contracts/src/DeployChain.sol const { config, error } = useChainConfig({ l2ChainId: chain.id, appchainChainId: appchain.chain.id, }); if (error) { console.error(error); throw new Error( 'Error loading chain configuration. Ensure you have the correct chain ID.', ); } if (bridgeableTokens.length === 0) { throw new Error( 'Bridgeable tokens must be provided as a parameter to AppchainBridge.', ); } if (!config) { return null; } // Wagmi hooks const { address } = useAccount(); const wagmiConfig = useConfig(); // Bridge params const [bridgeParams, setBridgeParams] = useState({ amount: '', amountUSD: '0.00', token: bridgeableTokens[0], recipient: address, }); // Bridge state const [isPriceLoading, setIsPriceLoading] = useState(false); const [isAddressModalOpen, setIsAddressModalOpen] = useState(false); const [isWithdrawModalOpen, setIsWithdrawModalOpen] = useState(false); const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false); const [isResumeTransactionModalOpen, setIsResumeTransactionModalOpen] = useState(false); const direction = from.id === chain.id ? 'deposit' : 'withdraw'; const [balance, setBalance] = useState(''); const [resumeWithdrawalTxHash, setResumeWithdrawalTxHash] = useState(); // Deposit const { deposit, depositStatus, transactionHash: depositTransactionHash, resetDepositStatus, } = useDeposit(); const { withdraw, withdrawStatus, waitForWithdrawal, proveAndFinalizeWithdrawal, finalizedWithdrawalTxHash, resetWithdrawStatus, } = useWithdraw({ config, chain, bridgeParams, }); // Update recipient when wallet connects // Defaults to current wallet address useEffect(() => { setBridgeParams((prev) => ({ ...prev, recipient: address, })); }, [address]); // Retrieves the ETH or ERC20 balance of the user // Based on the currently selected token const fetchBalance = useCallback(async () => { if (!address) { return; } const tokenAddress = direction === 'deposit' ? bridgeParams.token.address : bridgeParams.token.remoteToken; let newBalance: string; if ( !tokenAddress || /* v8 ignore next 1 */ (direction === 'withdraw' && bridgeParams.token.isCustomGasToken) ) { const ethBalance = await getBalance(wagmiConfig, { address, chainId: from.id, }); newBalance = formatUnits(ethBalance.value, ethBalance.decimals); } else { const erc20Balance = await readContract(wagmiConfig, { abi: erc20Abi, functionName: 'balanceOf', args: [address], address: tokenAddress as Address, chainId: from.id, }); newBalance = formatUnits(erc20Balance, bridgeParams.token.decimals); } setBalance(newBalance); }, [address, direction, bridgeParams.token, from.id, wagmiConfig]); // Fetch balance when bridge params change useEffect(() => { fetchBalance(); }, [fetchBalance]); // Fetch balance when withdraw is successful useEffect(() => { if ( withdrawStatus === 'claimSuccess' || depositStatus === 'depositSuccess' ) { fetchBalance(); } }, [withdrawStatus, depositStatus, fetchBalance]); const handleToggle = useCallback(() => { const tmp = from; setFrom(to); setTo(tmp); // Reset statuses when direction changes resetDepositStatus(); resetWithdrawStatus(); }, [from, to, resetDepositStatus, resetWithdrawStatus]); const handleAmountChange = useCallback( async ({ amount, token, remoteToken, }: { amount: string; token: Token; remoteToken?: Token; }) => { setIsPriceLoading(true); setBridgeParams((prev) => ({ ...prev, amount, token, remoteToken, })); const amountUSD = await handleFetchPrice(amount, token); setBridgeParams((prev) => ({ ...prev, amountUSD, })); setIsPriceLoading(false); }, [handleFetchPrice], ); const handleAddressSelect = useCallback((address: Address) => { setBridgeParams((prev) => ({ ...prev, recipient: address, })); }, []); const handleResumeTransaction = useCallback((txHash: Hex) => { setResumeWithdrawalTxHash(txHash); setIsResumeTransactionModalOpen(false); }, []); const handleOpenExplorer = useCallback(() => { const blockExplorerUrl = getChainExplorer(chain.id); const txHash = depositStatus === 'depositSuccess' ? depositTransactionHash : finalizedWithdrawalTxHash; window.open(`${blockExplorerUrl}/tx/${txHash}`, '_blank'); }, [ chain.id, depositStatus, depositTransactionHash, finalizedWithdrawalTxHash, ]); const handleDeposit = useCallback(async () => { await deposit({ config, from, bridgeParams, }); }, [deposit, config, from, bridgeParams]); const handleWithdraw = useCallback(async () => { await withdraw(); }, [withdraw]); const handleResetState = useCallback(() => { setIsSuccessModalOpen(false); setIsWithdrawModalOpen(false); setIsResumeTransactionModalOpen(false); setResumeWithdrawalTxHash(undefined); }, []); // Open withdraw modal when withdraw is successful, or when transaction is resumed useEffect(() => { if (withdrawStatus === 'withdrawSuccess' || resumeWithdrawalTxHash) { setIsWithdrawModalOpen(true); } }, [withdrawStatus, resumeWithdrawalTxHash]); // Reset withdraw status when withdraw modal is closed useEffect(() => { if (!isWithdrawModalOpen) { resetWithdrawStatus(); } }, [isWithdrawModalOpen, resetWithdrawStatus]); // Open success modal when deposit is successful useEffect(() => { if (depositStatus === 'depositSuccess') { setIsSuccessModalOpen(true); } }, [depositStatus]); // Open success modal when withdraw is successful useEffect(() => { if (withdrawStatus === 'claimSuccess') { setIsSuccessModalOpen(true); } }, [withdrawStatus]); const value = useValue({ // Internal config, from, to, bridgeableTokens, bridgeParams, isPriceLoading, // Bridge UI balance, handleToggle, handleAmountChange, // Address modal isAddressModalOpen, setIsAddressModalOpen, handleAddressSelect, // Success modal isSuccessModalOpen, setIsSuccessModalOpen, handleOpenExplorer, handleResetState, // Resume transaction modal isResumeTransactionModalOpen, setIsResumeTransactionModalOpen, resumeWithdrawalTxHash, setResumeWithdrawalTxHash, handleResumeTransaction, // Deposits and Withdrawals handleDeposit, depositStatus, depositTransactionHash, direction, handleWithdraw, withdrawStatus, waitForWithdrawal, proveAndFinalizeWithdrawal, finalizedWithdrawalTxHash, isWithdrawModalOpen, setIsWithdrawModalOpen, resetDepositStatus, resetWithdrawStatus, }); return ( {children} ); }; export const useAppchainBridgeContext = () => { const context = useContext(AppchainBridgeContext); if (context === undefined) { throw new Error('useAppchainBridge must be used within a BridgeProvider'); } return context; };