From 20846b4d3793fce3cf4cc28c05e424e5736e6c73 Mon Sep 17 00:00:00 2001 From: Mikhail Fedosov Date: Wed, 29 Oct 2025 01:46:46 +0400 Subject: [PATCH] refactor: Extract network switching logic into reusable hook with TDD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract ~700 lines of duplicated network switching logic from ClaimList and AssistantsList components into centralized useNetworkSwitcher hook. Hook provides unified interface for network detection and switching across bridge, assistant, transfer, and claim contexts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/components/AssistantsList.js | 216 ++---------- src/components/ClaimList.js | 238 ++----------- .../__tests__/useNetworkSwitcher.test.js | 332 ++++++++++++++++++ src/hooks/useNetworkSwitcher.js | 153 ++++++++ 4 files changed, 545 insertions(+), 394 deletions(-) create mode 100644 src/hooks/__tests__/useNetworkSwitcher.test.js create mode 100644 src/hooks/useNetworkSwitcher.js 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, + }; +};