diff --git a/src/components/AssistantsList.js b/src/components/AssistantsList.js index 1f9347e..3a69150 100644 --- a/src/components/AssistantsList.js +++ b/src/components/AssistantsList.js @@ -1,6 +1,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import { useSettings } from '../contexts/SettingsContext'; import { useWeb3 } from '../contexts/Web3Context'; +import { useNetworkSwitcher } from '../hooks/useNetworkSwitcher'; import { motion } from 'framer-motion'; import { ethers } from 'ethers'; import toast from 'react-hot-toast'; @@ -10,11 +11,11 @@ import WithdrawManagementFee from './WithdrawManagementFee'; import WithdrawSuccessFee from './WithdrawSuccessFee'; import AssignNewManager from './AssignNewManager'; import { IPRECOMPILE_ERC20_ABI } from '../contracts/abi'; -import { switchNetwork } from '../utils/network-switcher'; const AssistantsList = () => { const { getAssistantContractsWithSettings, getAllNetworksWithSettings, get3DPassTokenDecimalsDisplayMultiplier } = useSettings(); const { account } = useWeb3(); + const { getRequiredNetworkForAssistant, checkAndSwitchNetwork } = useNetworkSwitcher(); const [assistants, setAssistants] = useState([]); const [loading, setLoading] = useState(true); const [balances, setBalances] = useState({}); @@ -885,218 +886,75 @@ const AssistantsList = () => { } }, []); - // Network switching functions - const getRequiredNetwork = useCallback((assistant) => { - const networksWithSettings = getAllNetworksWithSettings(); - - console.log('🔍 getRequiredNetwork called for assistant:', assistant.address); - console.log('🔍 Available networks:', Object.keys(networksWithSettings)); - - for (const networkKey in networksWithSettings) { - const networkConfig = networksWithSettings[networkKey]; - console.log('🔍 Checking network:', networkKey, { - hasBridges: !!networkConfig.bridges, - bridgeCount: networkConfig.bridges ? Object.keys(networkConfig.bridges).length : 0 - }); - - if (networkConfig && networkConfig.bridges) { - for (const bridgeKey in networkConfig.bridges) { - const bridge = networkConfig.bridges[bridgeKey]; - console.log('🔍 Checking bridge:', { - bridgeAddress: bridge.address, - assistantBridgeAddress: assistant.bridgeAddress, - networkName: networkConfig.name, - networkId: networkConfig.id, - matches: bridge.address === assistant.bridgeAddress - }); - - if (bridge.address === assistant.bridgeAddress) { - const result = { - ...networkConfig, - chainId: networkConfig.id, - bridgeAddress: bridge.address, - assistantType: assistant.type - }; - console.log('✅ Found required network:', result); - return result; - } - } - } - } - console.log('❌ No required network found for assistant:', assistant.address); - return null; - }, [getAllNetworksWithSettings]); - - const checkNetwork = useCallback(async () => { - try { - const currentChainId = await window.ethereum.request({ method: 'eth_chainId' }); - const currentChainIdNumber = parseInt(currentChainId, 16); - console.log('🔍 Current chain ID:', currentChainIdNumber); - return currentChainIdNumber; - } catch (error) { - console.error('Error checking network:', error); - return null; - } - }, []); - - const switchToRequiredNetwork = useCallback(async (requiredNetwork) => { - console.log('🔄 Switching to network:', requiredNetwork.name, 'Chain ID:', requiredNetwork.chainId); - - const success = await switchNetwork(requiredNetwork); - - if (success) { - console.log('✅ Network switched successfully'); - } else { - console.error('❌ Network switching failed'); - } - - return success; - }, []); - const handleDeposit = useCallback(async (assistant) => { console.log('🔘 Deposit button clicked for assistant:', assistant.address); - - // Check if we need to switch networks first - const requiredNetwork = getRequiredNetwork(assistant); - if (!requiredNetwork) { - toast.error('Could not determine required network for this assistant'); + + const requiredNetwork = getRequiredNetworkForAssistant(assistant); + const switchSuccess = await checkAndSwitchNetwork(requiredNetwork); + + if (!switchSuccess) { return; } - - const currentChainId = await checkNetwork(); - if (currentChainId !== requiredNetwork.chainId) { - console.log('🚨 NETWORK SWITCHING WILL BE TRIGGERED NOW!'); - console.log('🔄 Wrong network detected, switching automatically...'); - toast(`Switching to ${requiredNetwork.name} network...`); - const switchSuccess = await switchToRequiredNetwork(requiredNetwork); - console.log('🔍 Network switch result:', switchSuccess); - if (!switchSuccess) { - toast.error('Failed to switch to the required network'); - return; - } - // Wait a moment for the network to settle - await new Promise(resolve => setTimeout(resolve, 1000)); - } - + setSelectedAssistant(assistant); setShowDepositDialog(true); - }, [getRequiredNetwork, checkNetwork, switchToRequiredNetwork]); + }, [getRequiredNetworkForAssistant, checkAndSwitchNetwork]); const handleWithdraw = useCallback(async (assistant) => { console.log('🔘 Withdraw button clicked for assistant:', assistant.address); - - // Check if we need to switch networks first - const requiredNetwork = getRequiredNetwork(assistant); - if (!requiredNetwork) { - toast.error('Could not determine required network for this assistant'); + + const requiredNetwork = getRequiredNetworkForAssistant(assistant); + const switchSuccess = await checkAndSwitchNetwork(requiredNetwork); + + if (!switchSuccess) { return; } - - const currentChainId = await checkNetwork(); - if (currentChainId !== requiredNetwork.chainId) { - console.log('🚨 NETWORK SWITCHING WILL BE TRIGGERED NOW!'); - console.log('🔄 Wrong network detected, switching automatically...'); - toast(`Switching to ${requiredNetwork.name} network...`); - const switchSuccess = await switchToRequiredNetwork(requiredNetwork); - console.log('🔍 Network switch result:', switchSuccess); - if (!switchSuccess) { - toast.error('Failed to switch to the required network'); - return; - } - // Wait a moment for the network to settle - await new Promise(resolve => setTimeout(resolve, 1000)); - } - + setSelectedAssistant(assistant); setShowWithdrawDialog(true); - }, [getRequiredNetwork, checkNetwork, switchToRequiredNetwork]); + }, [getRequiredNetworkForAssistant, checkAndSwitchNetwork]); const handleWithdrawManagementFee = useCallback(async (assistant) => { console.log('🔘 Withdraw Management Fee button clicked for assistant:', assistant.address); - - // Check if we need to switch networks first - const requiredNetwork = getRequiredNetwork(assistant); - if (!requiredNetwork) { - toast.error('Could not determine required network for this assistant'); + + const requiredNetwork = getRequiredNetworkForAssistant(assistant); + const switchSuccess = await checkAndSwitchNetwork(requiredNetwork); + + if (!switchSuccess) { return; } - - const currentChainId = await checkNetwork(); - if (currentChainId !== requiredNetwork.chainId) { - console.log('🚨 NETWORK SWITCHING WILL BE TRIGGERED NOW!'); - console.log('🔄 Wrong network detected, switching automatically...'); - toast(`Switching to ${requiredNetwork.name} network...`); - const switchSuccess = await switchToRequiredNetwork(requiredNetwork); - console.log('🔍 Network switch result:', switchSuccess); - if (!switchSuccess) { - toast.error('Failed to switch to the required network'); - return; - } - // Wait a moment for the network to settle - await new Promise(resolve => setTimeout(resolve, 1000)); - } - + setSelectedAssistant(assistant); setShowWithdrawManagementFeeDialog(true); - }, [getRequiredNetwork, checkNetwork, switchToRequiredNetwork]); + }, [getRequiredNetworkForAssistant, checkAndSwitchNetwork]); const handleWithdrawSuccessFee = useCallback(async (assistant) => { console.log('🔘 Withdraw Success Fee button clicked for assistant:', assistant.address); - - // Check if we need to switch networks first - const requiredNetwork = getRequiredNetwork(assistant); - if (!requiredNetwork) { - toast.error('Could not determine required network for this assistant'); + + const requiredNetwork = getRequiredNetworkForAssistant(assistant); + const switchSuccess = await checkAndSwitchNetwork(requiredNetwork); + + if (!switchSuccess) { return; } - - const currentChainId = await checkNetwork(); - if (currentChainId !== requiredNetwork.chainId) { - console.log('🚨 NETWORK SWITCHING WILL BE TRIGGERED NOW!'); - console.log('🔄 Wrong network detected, switching automatically...'); - toast(`Switching to ${requiredNetwork.name} network...`); - const switchSuccess = await switchToRequiredNetwork(requiredNetwork); - console.log('🔍 Network switch result:', switchSuccess); - if (!switchSuccess) { - toast.error('Failed to switch to the required network'); - return; - } - // Wait a moment for the network to settle - await new Promise(resolve => setTimeout(resolve, 1000)); - } - + setSelectedAssistant(assistant); setShowWithdrawSuccessFeeDialog(true); - }, [getRequiredNetwork, checkNetwork, switchToRequiredNetwork]); + }, [getRequiredNetworkForAssistant, checkAndSwitchNetwork]); const handleAssignNewManager = useCallback(async (assistant) => { console.log('🔘 Assign New Manager button clicked for assistant:', assistant.address); - - // Check if we need to switch networks first - const requiredNetwork = getRequiredNetwork(assistant); - if (!requiredNetwork) { - toast.error('Could not determine required network for this assistant'); + + const requiredNetwork = getRequiredNetworkForAssistant(assistant); + const switchSuccess = await checkAndSwitchNetwork(requiredNetwork); + + if (!switchSuccess) { return; } - - const currentChainId = await checkNetwork(); - if (currentChainId !== requiredNetwork.chainId) { - console.log('🚨 NETWORK SWITCHING WILL BE TRIGGERED NOW!'); - console.log('🔄 Wrong network detected, switching automatically...'); - toast(`Switching to ${requiredNetwork.name} network...`); - const switchSuccess = await switchToRequiredNetwork(requiredNetwork); - console.log('🔍 Network switch result:', switchSuccess); - if (!switchSuccess) { - toast.error('Failed to switch to the required network'); - return; - } - // Wait a moment for the network to settle - await new Promise(resolve => setTimeout(resolve, 1000)); - } - + setSelectedAssistant(assistant); setShowAssignNewManagerDialog(true); - }, [getRequiredNetwork, checkNetwork, switchToRequiredNetwork]); + }, [getRequiredNetworkForAssistant, checkAndSwitchNetwork]); const handleCloseDialogs = useCallback(() => { setShowDepositDialog(false); diff --git a/src/components/ClaimList.js b/src/components/ClaimList.js index 032428f..38bad68 100644 --- a/src/components/ClaimList.js +++ b/src/components/ClaimList.js @@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import { ethers } from 'ethers'; import { useWeb3 } from '../contexts/Web3Context'; import { useSettings } from '../contexts/SettingsContext'; +import { useNetworkSwitcher } from '../hooks/useNetworkSwitcher'; import { NETWORKS } from '../config/networks'; import { fetchClaimsFromAllNetworks } from '../utils/fetch-claims'; import { fetchLastTransfers } from '../utils/fetch-last-transfers'; @@ -249,6 +250,11 @@ const getFieldMatchStatus = (claim, field) => { const ClaimList = () => { const { account, network, getNetworkWithSettings } = useWeb3(); const { getBridgeInstancesWithSettings, getHistorySearchDepth, getClaimSearchDepth, getTokenDecimalsDisplayMultiplier } = useSettings(); + const { + getRequiredNetworkForTransfer, + getRequiredNetworkForClaim, + checkAndSwitchNetwork + } = useNetworkSwitcher(); const [claims, setClaims] = useState([]); const [aggregatedData, setAggregatedData] = useState(null); const [loading, setLoading] = useState(false); @@ -290,79 +296,6 @@ const ClaimList = () => { }); // Cache stats removed from UI - cache still works internally for performance - // Network switching functions - const getRequiredNetwork = useCallback((transfer) => { - // For transfers, we need to determine which network the claim should be created on - // Import transfers (NewRepatriation) create claims on the foreign network (Ethereum) - // Export transfers (NewExpatriation) create claims on the home network (3DPass) - - console.log('🔍 getRequiredNetwork called with transfer:', { - eventType: transfer.eventType, - fromNetwork: transfer.fromNetwork, - toNetwork: transfer.toNetwork, - fullTransfer: transfer - }); - - console.log('🔍 Available networks:', Object.values(NETWORKS).map(n => ({ - name: n.name, - id: n.id, - symbol: n.symbol - }))); - - if (transfer.eventType === 'NewRepatriation') { - // Import transfer: claim should be created on foreign network (Ethereum) - const network = Object.values(NETWORKS).find(network => - network.name === transfer.toNetwork - ); - console.log('🔍 NewRepatriation - looking for network:', transfer.toNetwork, 'found:', network?.name); - return network; - } else if (transfer.eventType === 'NewExpatriation') { - // Export transfer: claim should be created on destination network (3DPass) - const network = Object.values(NETWORKS).find(network => - network.name === transfer.toNetwork - ); - console.log('🔍 NewExpatriation - looking for network:', transfer.toNetwork, 'found:', network?.name); - return network; - } - - console.log('🔍 No matching event type found'); - return null; - }, []); - - const getRequiredNetworkForClaim = useCallback((claim) => { - // For claims, we need to determine which network the claim exists on - // This is the network where the bridge contract is deployed - - console.log('🔍 getRequiredNetworkForClaim called with claim:', { - bridgeAddress: claim.bridgeAddress, - networkName: claim.networkName, - bridgeType: claim.bridgeType - }); - - // Find the network that contains this bridge address - const networksWithSettings = getNetworkWithSettings ? Object.values(NETWORKS) : []; - - for (const networkConfig of networksWithSettings) { - if (networkConfig && networkConfig.bridges) { - for (const bridgeKey in networkConfig.bridges) { - const bridge = networkConfig.bridges[bridgeKey]; - if (bridge.address === claim.bridgeAddress) { - const result = { - ...networkConfig, - chainId: networkConfig.id, - bridgeAddress: bridge.address - }; - console.log('✅ Found required network for claim:', result); - return result; - } - } - } - } - - console.log('❌ No required network found for claim:', claim.bridgeAddress); - return null; - }, [getNetworkWithSettings]); - // Helper function to get the correct ABI based on bridge type const getBridgeABI = useCallback((bridgeType) => { switch (bridgeType) { @@ -476,115 +409,19 @@ const ClaimList = () => { } }, [contractSettings, currentBlock]); - const checkNetwork = useCallback(async () => { - try { - const currentChainId = await window.ethereum.request({ method: 'eth_chainId' }); - const currentChainIdNumber = parseInt(currentChainId, 16); - console.log('🔍 Current chain ID:', currentChainIdNumber); - return currentChainIdNumber; - } catch (error) { - console.error('Error checking network:', error); - return null; - } - }, []); - - const switchToRequiredNetwork = useCallback(async (requiredNetwork) => { - try { - console.log('🔄 switchToRequiredNetwork called with:', requiredNetwork); - console.log('🔄 Switching to network:', requiredNetwork.name, 'Chain ID:', requiredNetwork.chainId || requiredNetwork.id); - - // Check if wallet is available - if (!window.ethereum) { - console.error('❌ No wallet detected'); - return false; - } - - // Use chainId if available, otherwise use id - const chainId = requiredNetwork.chainId || requiredNetwork.id; - if (!chainId) { - console.error('❌ No chain ID found in network configuration'); - return false; - } - - const chainIdHex = `0x${chainId.toString(16)}`; - console.log('🔄 Chain ID hex:', chainIdHex); - - try { - console.log('🔄 Attempting to switch to existing network...'); - const result = await window.ethereum.request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId: chainIdHex }], - }); - console.log('🔄 Switch request result:', result); - console.log('✅ Network switched successfully'); - return true; - } catch (switchError) { - console.log('⚠️ Network switch failed:', switchError); - console.log('⚠️ Error code:', switchError.code); - console.log('⚠️ Error message:', switchError.message); - console.log('⚠️ Network not added, attempting to add it...'); - - if (switchError.code === 4902) { - try { - console.log('🔄 Adding new network...'); - const addResult = await window.ethereum.request({ - method: 'wallet_addEthereumChain', - params: [{ - chainId: chainIdHex, - chainName: requiredNetwork.name, - nativeCurrency: requiredNetwork.nativeCurrency, - rpcUrls: [requiredNetwork.rpcUrl], - blockExplorerUrls: [requiredNetwork.explorer], - }], - }); - console.log('🔄 Add network result:', addResult); - console.log('✅ Network added and switched successfully'); - return true; - } catch (addError) { - console.error('❌ Failed to add network:', addError); - console.error('❌ Add error code:', addError.code); - console.error('❌ Add error message:', addError.message); - return false; - } - } else { - console.error('❌ Failed to switch network:', switchError); - return false; - } - } - } catch (error) { - console.error('❌ Network switching error:', error); - return false; - } - }, []); - const handleChallenge = useCallback(async (claim) => { console.log('🔘 Challenge button clicked for claim:', claim.actualClaimNum || claim.claimNum); - - // Check if we need to switch networks first + const requiredNetwork = getRequiredNetworkForClaim(claim); - if (!requiredNetwork) { - toast.error('Could not determine required network for this claim'); + const switchSuccess = await checkAndSwitchNetwork(requiredNetwork); + + if (!switchSuccess) { return; } - - const currentChainId = await checkNetwork(); - if (currentChainId !== requiredNetwork.chainId) { - console.log('🚨 NETWORK SWITCHING WILL BE TRIGGERED NOW!'); - console.log('🔄 Wrong network detected, switching automatically...'); - toast(`Switching to ${requiredNetwork.name} network...`); - const switchSuccess = await switchToRequiredNetwork(requiredNetwork); - console.log('🔍 Network switch result:', switchSuccess); - if (!switchSuccess) { - toast.error('Failed to switch to the required network'); - return; - } - // Wait a moment for the network to settle - await new Promise(resolve => setTimeout(resolve, 1000)); - } - + setSelectedClaim(claim); setShowChallengeModal(true); - }, [getRequiredNetworkForClaim, checkNetwork, switchToRequiredNetwork]); + }, [getRequiredNetworkForClaim, checkAndSwitchNetwork]); @@ -1104,32 +941,17 @@ const ClaimList = () => { const handleWithdraw = useCallback(async (claim) => { console.log('🔘 Withdraw button clicked for claim:', claim.actualClaimNum || claim.claimNum); - - // Check if we need to switch networks first + const requiredNetwork = getRequiredNetworkForClaim(claim); - if (!requiredNetwork) { - toast.error('Could not determine required network for this claim'); + const switchSuccess = await checkAndSwitchNetwork(requiredNetwork); + + if (!switchSuccess) { return; } - - const currentChainId = await checkNetwork(); - if (currentChainId !== requiredNetwork.chainId) { - console.log('🚨 NETWORK SWITCHING WILL BE TRIGGERED NOW!'); - console.log('🔄 Wrong network detected, switching automatically...'); - toast(`Switching to ${requiredNetwork.name} network...`); - const switchSuccess = await switchToRequiredNetwork(requiredNetwork); - console.log('🔍 Network switch result:', switchSuccess); - if (!switchSuccess) { - toast.error('Failed to switch to the required network'); - return; - } - // Wait a moment for the network to settle - await new Promise(resolve => setTimeout(resolve, 1000)); - } - + setSelectedClaim(prepareClaimForWithdraw(claim)); setShowWithdrawModal(true); - }, [getRequiredNetworkForClaim, checkNetwork, switchToRequiredNetwork, prepareClaimForWithdraw]); + }, [getRequiredNetworkForClaim, checkAndSwitchNetwork, prepareClaimForWithdraw]); // Load cached data from browser storage const loadCachedData = useCallback(async () => { @@ -3163,27 +2985,13 @@ const ClaimList = () => { } try { - // Determine the required network for this transfer - const requiredNetwork = getRequiredNetwork(claim); - console.log('🔍 Required network result:', requiredNetwork); - - if (!requiredNetwork) { - toast.error('Could not determine the required network for this transfer'); - return; - } - - // Switch to the required network before opening the dialog - console.log('🔄 Starting network switch to:', requiredNetwork.name); - toast(`Switching to ${requiredNetwork.name} network...`); - const switchResult = await switchToRequiredNetwork(requiredNetwork); - console.log('🔄 Network switch result:', switchResult); - - if (!switchResult) { - toast.error('Failed to switch to the required network'); + const requiredNetwork = getRequiredNetworkForTransfer(claim); + const switchSuccess = await checkAndSwitchNetwork(requiredNetwork); + + if (!switchSuccess) { return; } - - // Set the transfer data and open the NewClaim dialog + setSelectedTransfer(claim); setShowNewClaim(true); } catch (error) { diff --git a/src/hooks/__tests__/useNetworkSwitcher.test.js b/src/hooks/__tests__/useNetworkSwitcher.test.js new file mode 100644 index 0000000..e9e829f --- /dev/null +++ b/src/hooks/__tests__/useNetworkSwitcher.test.js @@ -0,0 +1,332 @@ +import { renderHook, act } from '@testing-library/react'; +import { useNetworkSwitcher } from '../useNetworkSwitcher'; +import { useSettings } from '../../contexts/SettingsContext'; +import { useWeb3 } from '../../contexts/Web3Context'; +import toast from 'react-hot-toast'; + +jest.mock('../../contexts/SettingsContext'); +jest.mock('../../contexts/Web3Context'); +jest.mock('react-hot-toast'); + +describe('useNetworkSwitcher', () => { + const mockGetAllNetworksWithSettings = jest.fn(); + const mockSwitchNetwork = jest.fn(); + const mockEthereumRequest = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + + useSettings.mockReturnValue({ + getAllNetworksWithSettings: mockGetAllNetworksWithSettings, + }); + + useWeb3.mockReturnValue({ + switchNetwork: mockSwitchNetwork, + }); + + global.window.ethereum = { + request: mockEthereumRequest, + }; + + mockGetAllNetworksWithSettings.mockReturnValue({ + ETHEREUM: { + name: 'Ethereum', + id: 1, + symbol: 'ETH', + bridges: { + bridge1: { address: '0xBridge1' }, + bridge2: { address: '0xBridge2' }, + }, + }, + THREEDPASS: { + name: '3DPass', + id: 132, + symbol: 'P3D', + bridges: { + bridge3: { address: '0xBridge3' }, + }, + }, + }); + }); + + afterEach(() => { + delete global.window.ethereum; + }); + + describe('getRequiredNetworkForBridge', () => { + it('should return null if bridge address not found', () => { + const { result } = renderHook(() => useNetworkSwitcher()); + + const network = result.current.getRequiredNetworkForBridge('0xNonExistent'); + + expect(network).toBeNull(); + }); + + it('should find network by bridge address', () => { + const { result } = renderHook(() => useNetworkSwitcher()); + + const network = result.current.getRequiredNetworkForBridge('0xBridge1'); + + expect(network).toEqual({ + name: 'Ethereum', + id: 1, + symbol: 'ETH', + chainId: 1, + bridgeAddress: '0xBridge1', + bridges: { + bridge1: { address: '0xBridge1' }, + bridge2: { address: '0xBridge2' }, + }, + }); + }); + + it('should find network by bridge address across multiple networks', () => { + const { result } = renderHook(() => useNetworkSwitcher()); + + const network = result.current.getRequiredNetworkForBridge('0xBridge3'); + + expect(network).toEqual({ + name: '3DPass', + id: 132, + symbol: 'P3D', + chainId: 132, + bridgeAddress: '0xBridge3', + bridges: { + bridge3: { address: '0xBridge3' }, + }, + }); + }); + }); + + describe('getRequiredNetworkForAssistant', () => { + it('should return null if assistant bridge address not found', () => { + const { result } = renderHook(() => useNetworkSwitcher()); + + const assistant = { bridgeAddress: '0xNonExistent' }; + const network = result.current.getRequiredNetworkForAssistant(assistant); + + expect(network).toBeNull(); + }); + + it('should find network by assistant bridge address', () => { + const { result } = renderHook(() => useNetworkSwitcher()); + + const assistant = { address: '0xAssistant1', bridgeAddress: '0xBridge1', type: 'export' }; + const network = result.current.getRequiredNetworkForAssistant(assistant); + + expect(network).toEqual({ + name: 'Ethereum', + id: 1, + symbol: 'ETH', + chainId: 1, + bridgeAddress: '0xBridge1', + assistantType: 'export', + bridges: { + bridge1: { address: '0xBridge1' }, + bridge2: { address: '0xBridge2' }, + }, + }); + }); + }); + + describe('getRequiredNetworkForTransfer', () => { + it('should return network by toNetwork name for NewRepatriation', () => { + const { result } = renderHook(() => useNetworkSwitcher()); + + const transfer = { + eventType: 'NewRepatriation', + fromNetwork: '3DPass', + toNetwork: 'Ethereum', + }; + + const network = result.current.getRequiredNetworkForTransfer(transfer); + + expect(network.name).toBe('Ethereum'); + expect(network.id).toBe(1); + }); + + it('should return network by toNetwork name for NewExpatriation', () => { + const { result } = renderHook(() => useNetworkSwitcher()); + + const transfer = { + eventType: 'NewExpatriation', + fromNetwork: 'Ethereum', + toNetwork: '3DPass', + }; + + const network = result.current.getRequiredNetworkForTransfer(transfer); + + expect(network.name).toBe('3DPass'); + expect(network.id).toBe(132); + }); + + it('should return null for unknown event type', () => { + const { result } = renderHook(() => useNetworkSwitcher()); + + const transfer = { + eventType: 'UnknownEvent', + fromNetwork: 'Ethereum', + toNetwork: '3DPass', + }; + + const network = result.current.getRequiredNetworkForTransfer(transfer); + + expect(network).toBeNull(); + }); + }); + + describe('getRequiredNetworkForClaim', () => { + it('should find network by claim bridge address', () => { + const { result } = renderHook(() => useNetworkSwitcher()); + + const claim = { + bridgeAddress: '0xBridge2', + networkName: 'Ethereum', + }; + + const network = result.current.getRequiredNetworkForClaim(claim); + + expect(network).toEqual({ + name: 'Ethereum', + id: 1, + symbol: 'ETH', + chainId: 1, + bridgeAddress: '0xBridge2', + bridges: { + bridge1: { address: '0xBridge1' }, + bridge2: { address: '0xBridge2' }, + }, + }); + }); + + it('should return null if claim bridge address not found', () => { + const { result } = renderHook(() => useNetworkSwitcher()); + + const claim = { + bridgeAddress: '0xNonExistent', + networkName: 'Unknown', + }; + + const network = result.current.getRequiredNetworkForClaim(claim); + + expect(network).toBeNull(); + }); + }); + + describe('checkAndSwitchNetwork', () => { + it('should return true if already on correct network', async () => { + mockEthereumRequest.mockResolvedValue('0x1'); // Chain ID 1 in hex + + const { result } = renderHook(() => useNetworkSwitcher()); + + const requiredNetwork = { name: 'Ethereum', id: 1, chainId: 1 }; + + let switchResult; + await act(async () => { + switchResult = await result.current.checkAndSwitchNetwork(requiredNetwork); + }); + + expect(switchResult).toBe(true); + expect(mockSwitchNetwork).not.toHaveBeenCalled(); + expect(toast).not.toHaveBeenCalled(); + }); + + it('should switch network if on wrong network', async () => { + mockEthereumRequest.mockResolvedValue('0x84'); // Chain ID 132 in hex + mockSwitchNetwork.mockResolvedValue(true); + + const { result } = renderHook(() => useNetworkSwitcher()); + + const requiredNetwork = { name: 'Ethereum', id: 1, chainId: 1 }; + + let switchResult; + await act(async () => { + switchResult = await result.current.checkAndSwitchNetwork(requiredNetwork); + }); + + expect(mockEthereumRequest).toHaveBeenCalledWith({ method: 'eth_chainId' }); + expect(toast).toHaveBeenCalledWith('Switching to Ethereum network...'); + expect(mockSwitchNetwork).toHaveBeenCalledWith(1); + expect(switchResult).toBe(true); + }); + + it('should return false if network switch fails', async () => { + mockEthereumRequest.mockResolvedValue('0x84'); // Chain ID 132 in hex + mockSwitchNetwork.mockResolvedValue(false); + + const { result } = renderHook(() => useNetworkSwitcher()); + + const requiredNetwork = { name: 'Ethereum', id: 1, chainId: 1 }; + + let switchResult; + await act(async () => { + switchResult = await result.current.checkAndSwitchNetwork(requiredNetwork); + }); + + expect(mockSwitchNetwork).toHaveBeenCalledWith(1); + expect(toast.error).toHaveBeenCalledWith('Failed to switch to the required network'); + expect(switchResult).toBe(false); + }); + + it('should show custom error message when provided', async () => { + mockEthereumRequest.mockResolvedValue('0x84'); // Chain ID 132 in hex + mockSwitchNetwork.mockResolvedValue(false); + + const { result } = renderHook(() => useNetworkSwitcher()); + + const requiredNetwork = { name: 'Ethereum', id: 1, chainId: 1 }; + const options = { errorMessage: 'Custom error message' }; + + await act(async () => { + await result.current.checkAndSwitchNetwork(requiredNetwork, options); + }); + + expect(toast.error).toHaveBeenCalledWith('Custom error message'); + }); + + it('should wait for network to settle after successful switch', async () => { + mockEthereumRequest.mockResolvedValue('0x84'); // Chain ID 132 in hex + mockSwitchNetwork.mockResolvedValue(true); + + const { result } = renderHook(() => useNetworkSwitcher()); + + const requiredNetwork = { name: 'Ethereum', id: 1, chainId: 1 }; + const options = { waitTime: 100 }; + + let switchResult; + await act(async () => { + switchResult = await result.current.checkAndSwitchNetwork(requiredNetwork, options); + }); + + expect(switchResult).toBe(true); + }); + + it('should return false and show error toast if network detection fails', async () => { + mockEthereumRequest.mockRejectedValue(new Error('Network error')); + + const { result } = renderHook(() => useNetworkSwitcher()); + + const requiredNetwork = { name: 'Ethereum', id: 1, chainId: 1 }; + + let switchResult; + await act(async () => { + switchResult = await result.current.checkAndSwitchNetwork(requiredNetwork); + }); + + expect(switchResult).toBe(false); + expect(toast.error).toHaveBeenCalled(); + }); + + it('should return false if required network is null', async () => { + const { result } = renderHook(() => useNetworkSwitcher()); + + let switchResult; + await act(async () => { + switchResult = await result.current.checkAndSwitchNetwork(null); + }); + + expect(switchResult).toBe(false); + expect(toast.error).toHaveBeenCalledWith('Could not determine required network'); + }); + }); +}); diff --git a/src/hooks/useNetworkSwitcher.js b/src/hooks/useNetworkSwitcher.js new file mode 100644 index 0000000..b5a709c --- /dev/null +++ b/src/hooks/useNetworkSwitcher.js @@ -0,0 +1,153 @@ +import { useCallback } from 'react'; +import { useSettings } from '../contexts/SettingsContext'; +import { useWeb3 } from '../contexts/Web3Context'; +import toast from 'react-hot-toast'; + +export const useNetworkSwitcher = () => { + const { getAllNetworksWithSettings } = useSettings(); + const { switchNetwork } = useWeb3(); + + const getRequiredNetworkForBridge = useCallback((bridgeAddress) => { + const networks = getAllNetworksWithSettings(); + + for (const networkKey in networks) { + const networkConfig = networks[networkKey]; + + if (networkConfig && networkConfig.bridges) { + for (const bridgeKey in networkConfig.bridges) { + const bridge = networkConfig.bridges[bridgeKey]; + + if (bridge.address === bridgeAddress) { + return { + ...networkConfig, + chainId: networkConfig.id, + bridgeAddress: bridge.address, + }; + } + } + } + } + + return null; + }, [getAllNetworksWithSettings]); + + const getRequiredNetworkForAssistant = useCallback((assistant) => { + const networks = getAllNetworksWithSettings(); + + for (const networkKey in networks) { + const networkConfig = networks[networkKey]; + + if (networkConfig && networkConfig.bridges) { + for (const bridgeKey in networkConfig.bridges) { + const bridge = networkConfig.bridges[bridgeKey]; + + if (bridge.address === assistant.bridgeAddress) { + return { + ...networkConfig, + chainId: networkConfig.id, + bridgeAddress: bridge.address, + assistantType: assistant.type, + }; + } + } + } + } + + return null; + }, [getAllNetworksWithSettings]); + + const getRequiredNetworkForTransfer = useCallback((transfer) => { + const networks = getAllNetworksWithSettings(); + + if (transfer.eventType === 'NewRepatriation' || transfer.eventType === 'NewExpatriation') { + const network = Object.values(networks).find(network => + network.name === transfer.toNetwork + ); + return network || null; + } + + return null; + }, [getAllNetworksWithSettings]); + + const getRequiredNetworkForClaim = useCallback((claim) => { + const networks = getAllNetworksWithSettings(); + + for (const networkKey in networks) { + const networkConfig = networks[networkKey]; + + if (networkConfig && networkConfig.bridges) { + for (const bridgeKey in networkConfig.bridges) { + const bridge = networkConfig.bridges[bridgeKey]; + + if (bridge.address === claim.bridgeAddress) { + return { + ...networkConfig, + chainId: networkConfig.id, + bridgeAddress: bridge.address, + }; + } + } + } + } + + return null; + }, [getAllNetworksWithSettings]); + + const checkNetwork = useCallback(async () => { + try { + const currentChainId = await window.ethereum.request({ method: 'eth_chainId' }); + const currentChainIdNumber = parseInt(currentChainId, 16); + return currentChainIdNumber; + } catch (error) { + console.error('Error checking network:', error); + return null; + } + }, []); + + const checkAndSwitchNetwork = useCallback(async (requiredNetwork, options = {}) => { + const { + errorMessage = 'Failed to switch to the required network', + waitTime = 1000 + } = options; + + if (!requiredNetwork) { + toast.error('Could not determine required network'); + return false; + } + + try { + const currentChainId = await checkNetwork(); + + if (currentChainId === null) { + toast.error(errorMessage); + return false; + } + + if (currentChainId !== requiredNetwork.chainId && currentChainId !== requiredNetwork.id) { + toast(`Switching to ${requiredNetwork.name} network...`); + const switchSuccess = await switchNetwork(requiredNetwork.id || requiredNetwork.chainId); + + if (!switchSuccess) { + toast.error(errorMessage); + return false; + } + + await new Promise(resolve => setTimeout(resolve, waitTime)); + } + + return true; + } catch (error) { + console.error('Error in checkAndSwitchNetwork:', error); + toast.error(errorMessage); + return false; + } + }, [checkNetwork, switchNetwork]); + + return { + getRequiredNetworkForBridge, + getRequiredNetworkForAssistant, + getRequiredNetworkForTransfer, + getRequiredNetworkForClaim, + checkAndSwitchNetwork, + }; +};