diff --git a/src/components/common/NetworkSelector/index.tsx b/src/components/common/NetworkSelector/index.tsx index ffeb760a6b..ab9c22e4d2 100644 --- a/src/components/common/NetworkSelector/index.tsx +++ b/src/components/common/NetworkSelector/index.tsx @@ -68,25 +68,32 @@ export const getNetworkLink = (router: NextRouter, safeAddress: string, networkS } const UndeployedNetworkMenuItem = ({ - chainId, - chainConfigs, + chain, isSelected = false, onSelect, }: { - chainId: string - chainConfigs: ChainInfo[] + chain: ChainInfo & { available: boolean } isSelected?: boolean onSelect: (chain: ChainInfo) => void }) => { - const chain = useMemo(() => chainConfigs.find((chain) => chain.chainId === chainId), [chainConfigs, chainId]) - - if (!chain) return null + const isDisabled = !chain.available return ( - onSelect(chain)}> + onSelect(chain)} + disabled={isDisabled} + > - + {isDisabled ? ( + + Not available + + ) : ( + + )} ) @@ -144,6 +151,8 @@ const UndeployedNetworks = ({ [availableNetworks], ) + const noAvailableNetworks = useMemo(() => availableNetworks.every((config) => !config.available), [availableNetworks]) + const onSelect = (chain: ChainInfo) => { setReplayOnChain(chain) } @@ -156,15 +165,16 @@ const UndeployedNetworks = ({ ) } - const errorMessage = safeCreationDataError - ? 'Adding another network is not possible for this Safe.' - : isUnsupportedSafeCreationVersion - ? 'This account was created from an outdated mastercopy. Adding another network is not possible.' - : '' + const errorMessage = + safeCreationDataError || (safeCreationData && noAvailableNetworks) + ? 'Adding another network is not possible for this Safe.' + : isUnsupportedSafeCreationVersion + ? 'This account was created from an outdated mastercopy. Adding another network is not possible.' + : '' if (errorMessage) { return ( - + {errorMessage} @@ -196,21 +206,11 @@ const UndeployedNetworks = ({ ) : ( <> {prodNets.map((chain) => ( - + ))} {testNets.length > 0 && } {testNets.map((chain) => ( - + ))} )} diff --git a/src/components/common/NetworkSelector/styles.module.css b/src/components/common/NetworkSelector/styles.module.css index f7c31f9a7d..d4d38715da 100644 --- a/src/components/common/NetworkSelector/styles.module.css +++ b/src/components/common/NetworkSelector/styles.module.css @@ -75,6 +75,7 @@ .item { display: flex; align-items: center; + justify-content: space-between; gap: var(--space-1); width: 100%; } @@ -83,3 +84,10 @@ padding: var(--space-2) 0; margin: 2px; } + +.comingSoon { + background-color: var(--color-border-light); + border-radius: 4px; + color: var(--color-text-primary); + padding: 4px 8px; +} diff --git a/src/features/multichain/hooks/__tests__/useCompatibleNetworks.test.ts b/src/features/multichain/hooks/__tests__/useCompatibleNetworks.test.ts index efbf7081c1..4875d4cc14 100644 --- a/src/features/multichain/hooks/__tests__/useCompatibleNetworks.test.ts +++ b/src/features/multichain/hooks/__tests__/useCompatibleNetworks.test.ts @@ -26,7 +26,7 @@ const PROXY_FACTORY_111_DEPLOYMENTS = getProxyFactoryDeployments({ version: '1.1 const PROXY_FACTORY_130_DEPLOYMENTS = getProxyFactoryDeployments({ version: '1.3.0' })?.deployments const PROXY_FACTORY_141_DEPLOYMENTS = getProxyFactoryDeployments({ version: '1.4.1' })?.deployments -describe('useReplayableNetworks', () => { +describe('useCompatibleNetworks', () => { beforeAll(() => { jest.spyOn(useChains, 'default').mockReturnValue({ configs: [ @@ -77,7 +77,7 @@ describe('useReplayableNetworks', () => { expect(result.current).toHaveLength(0) }) - it('should return empty list for unknown masterCopies', () => { + it('should set available to false for unknown masterCopies', () => { const callData = { owners: [faker.finance.ethereumAddress()], threshold: 1, @@ -107,43 +107,10 @@ describe('useReplayableNetworks', () => { setupData, } const { result } = renderHook(() => useCompatibleNetworks(creationData)) - expect(result.current).toHaveLength(0) - }) - - it('should return empty list for unknown masterCopies', () => { - const callData = { - owners: [faker.finance.ethereumAddress()], - threshold: 1, - to: ZERO_ADDRESS, - data: EMPTY_DATA, - fallbackHandler: faker.finance.ethereumAddress(), - paymentToken: ZERO_ADDRESS, - payment: 0, - paymentReceiver: ECOSYSTEM_ID_ADDRESS, - } - - const setupData = safeInterface.encodeFunctionData('setup', [ - callData.owners, - callData.threshold, - callData.to, - callData.data, - callData.fallbackHandler, - callData.paymentToken, - callData.payment, - callData.paymentReceiver, - ]) - - const creationData: ReplayedSafeProps = { - factoryAddress: faker.finance.ethereumAddress(), - masterCopy: faker.finance.ethereumAddress(), - saltNonce: '0', - setupData, - } - const { result } = renderHook(() => useCompatibleNetworks(creationData)) - expect(result.current).toHaveLength(0) + expect(result.current.every((config) => config.available)).toEqual(false) }) - it('should return everything but zkSync for 1.4.1 Safes', () => { + it('should set everything to available except zkSync for 1.4.1 Safes', () => { const callData = { owners: [faker.finance.ethereumAddress()], threshold: 1, @@ -172,8 +139,9 @@ describe('useReplayableNetworks', () => { setupData, } const { result } = renderHook(() => useCompatibleNetworks(creationData)) - expect(result.current).toHaveLength(4) - expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '480']) + expect(result.current).toHaveLength(5) + expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480']) + expect(result.current.map((chain) => chain.available)).toEqual([true, true, true, false, true]) } { @@ -184,12 +152,13 @@ describe('useReplayableNetworks', () => { setupData, } const { result } = renderHook(() => useCompatibleNetworks(creationData)) - expect(result.current).toHaveLength(4) - expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '480']) + expect(result.current).toHaveLength(5) + expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480']) + expect(result.current.map((chain) => chain.available)).toEqual([true, true, true, false, true]) } }) - it('should return correct chains for 1.3.0 Safes', () => { + it('should mark already deployed chains as not available', () => { const callData = { owners: [faker.finance.ethereumAddress()], threshold: 1, @@ -221,8 +190,9 @@ describe('useReplayableNetworks', () => { setupData, } const { result } = renderHook(() => useCompatibleNetworks(creationData)) - expect(result.current).toHaveLength(4) - expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '480']) + expect(result.current).toHaveLength(5) + expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480']) + expect(result.current.map((chain) => chain.available)).toEqual([true, true, true, false, true]) } // 1.3.0, L2 and canonical @@ -234,8 +204,9 @@ describe('useReplayableNetworks', () => { setupData, } const { result } = renderHook(() => useCompatibleNetworks(creationData)) - expect(result.current).toHaveLength(4) - expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '480']) + expect(result.current).toHaveLength(5) + expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480']) + expect(result.current.map((chain) => chain.available)).toEqual([true, true, true, false, true]) } // 1.3.0, L1 and EIP155 is not available on Worldchain @@ -247,8 +218,9 @@ describe('useReplayableNetworks', () => { setupData, } const { result } = renderHook(() => useCompatibleNetworks(creationData)) - expect(result.current).toHaveLength(3) - expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100']) + expect(result.current).toHaveLength(5) + expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480']) + expect(result.current.map((chain) => chain.available)).toEqual([true, true, true, false, false]) } // 1.3.0, L2 and EIP155 @@ -260,12 +232,13 @@ describe('useReplayableNetworks', () => { setupData, } const { result } = renderHook(() => useCompatibleNetworks(creationData)) - expect(result.current).toHaveLength(3) - expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100']) + expect(result.current).toHaveLength(5) + expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480']) + expect(result.current.map((chain) => chain.available)).toEqual([true, true, true, false, false]) } }) - it('should return empty list for 1.1.1 Safes', () => { + it('should set everything to not available for 1.1.1 Safes', () => { const callData = { owners: [faker.finance.ethereumAddress()], threshold: 1, @@ -295,6 +268,8 @@ describe('useReplayableNetworks', () => { setupData, } const { result } = renderHook(() => useCompatibleNetworks(creationData)) - expect(result.current).toHaveLength(0) + expect(result.current).toHaveLength(5) + expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480']) + expect(result.current.map((chain) => chain.available)).toEqual([false, false, false, false, false]) }) }) diff --git a/src/features/multichain/hooks/useCompatibleNetworks.ts b/src/features/multichain/hooks/useCompatibleNetworks.ts index 087ca0a4bc..21b662774a 100644 --- a/src/features/multichain/hooks/useCompatibleNetworks.ts +++ b/src/features/multichain/hooks/useCompatibleNetworks.ts @@ -8,6 +8,7 @@ import { getSafeL2SingletonDeployments, getSafeSingletonDeployments, } from '@safe-global/safe-deployments' +import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' const SUPPORTED_VERSIONS: SafeVersion[] = ['1.4.1', '1.3.0'] @@ -25,7 +26,9 @@ const hasDeployment = (chainId: string, contractAddress: string, deployments: Si * Returns all chains where the creations's masterCopy and factory are deployed. * @param creation */ -export const useCompatibleNetworks = (creation: ReplayedSafeProps | undefined) => { +export const useCompatibleNetworks = ( + creation: ReplayedSafeProps | undefined, +): (ChainInfo & { available: boolean })[] => { const { configs } = useChains() if (!creation) { @@ -50,10 +53,13 @@ export const useCompatibleNetworks = (creation: ReplayedSafeProps | undefined) = getProxyFactoryDeployments({ version }), ).filter(Boolean) as SingletonDeploymentV2[] - return configs.filter( - (config) => - (hasDeployment(config.chainId, masterCopy, allL1SingletonDeployments) || - hasDeployment(config.chainId, masterCopy, allL2SingletonDeployments)) && - hasDeployment(config.chainId, factoryAddress, allProxyFactoryDeployments), - ) + return configs.map((config) => { + return { + ...config, + available: + (hasDeployment(config.chainId, masterCopy, allL1SingletonDeployments) || + hasDeployment(config.chainId, masterCopy, allL2SingletonDeployments)) && + hasDeployment(config.chainId, factoryAddress, allProxyFactoryDeployments), + } + }) }