From 4f5e9bf631fcfc48dcbacbc7bce17f6d71144c06 Mon Sep 17 00:00:00 2001 From: James Mealy Date: Tue, 17 Sep 2024 16:47:39 +0200 Subject: [PATCH] [Multichain] Feat: disable adding new chains to safes created from old mastercopy versions [SW-167] (#4161) Co-authored-by: schmanu --- .../common/NetworkSelector/index.tsx | 33 +++++-- .../components/CreateSafeOnNewChain/index.tsx | 87 ++++++++++++------- ....test.ts => useCompatibleNetworks.test.ts} | 80 ++++------------- ...leNetworks.ts => useCompatibleNetworks.ts} | 21 ++--- 4 files changed, 109 insertions(+), 112 deletions(-) rename src/features/multichain/hooks/__tests__/{useReplayableNetworks.test.ts => useCompatibleNetworks.test.ts} (77%) rename src/features/multichain/hooks/{useReplayableNetworks.ts => useCompatibleNetworks.ts} (69%) diff --git a/src/components/common/NetworkSelector/index.tsx b/src/components/common/NetworkSelector/index.tsx index 65b69aa80f..493182b2f4 100644 --- a/src/components/common/NetworkSelector/index.tsx +++ b/src/components/common/NetworkSelector/index.tsx @@ -8,6 +8,7 @@ import type { SelectChangeEvent } from '@mui/material' import { Box, ButtonBase, + CircularProgress, Collapse, Divider, ListSubheader, @@ -32,7 +33,7 @@ import useSafeAddress from '@/hooks/useSafeAddress' import { sameAddress } from '@/utils/addresses' import uniq from 'lodash/uniq' import useSafeOverviews from '@/components/welcome/MyAccounts/useSafeOverviews' -import { useReplayableNetworks } from '@/features/multichain/hooks/useReplayableNetworks' +import { useCompatibleNetworks } from '@/features/multichain/hooks/useCompatibleNetworks' import { useSafeCreationData } from '@/features/multichain/hooks/useSafeCreationData' import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import PlusIcon from '@/public/images/common/plus.svg' @@ -127,9 +128,15 @@ const UndeployedNetworks = ({ [chains, deployedChains], ) const safeCreationResult = useSafeCreationData(safeAddress, deployedChainInfos) - const [safeCreationData, safeCreationDataError] = safeCreationResult + const [safeCreationData, safeCreationDataError, safeCreationLoading] = safeCreationResult - const availableNetworks = useReplayableNetworks(safeCreationData, deployedChains) + const allCompatibleChains = useCompatibleNetworks(safeCreationData) + const isUnsupportedSafeCreationVersion = Boolean(!allCompatibleChains?.length) + + const availableNetworks = useMemo( + () => allCompatibleChains?.filter((config) => !deployedChains.includes(config.chainId)) || [], + [allCompatibleChains, deployedChains], + ) const [testNets, prodNets] = useMemo( () => partition(availableNetworks, (config) => config.isTestnet), @@ -140,11 +147,25 @@ const UndeployedNetworks = ({ setReplayOnChain(chain) } - if (safeCreationDataError) { + if (safeCreationLoading) { + return ( + + + + ) + } + + 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.' + : '' + + if (errorMessage) { return ( - - Adding another network is not possible for this Safe + + {errorMessage} ) diff --git a/src/features/multichain/components/CreateSafeOnNewChain/index.tsx b/src/features/multichain/components/CreateSafeOnNewChain/index.tsx index e95e3c0c1e..30ffca4671 100644 --- a/src/features/multichain/components/CreateSafeOnNewChain/index.tsx +++ b/src/features/multichain/components/CreateSafeOnNewChain/index.tsx @@ -2,18 +2,19 @@ import NameInput from '@/components/common/NameInput' import NetworkInput from '@/components/common/NetworkInput' import ErrorMessage from '@/components/tx/ErrorMessage' import { + Box, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, + Divider, Stack, Typography, } from '@mui/material' import { FormProvider, useForm } from 'react-hook-form' import { useSafeCreationData } from '../../hooks/useSafeCreationData' -import { useReplayableNetworks } from '../../hooks/useReplayableNetworks' import { replayCounterfactualSafeDeployment } from '@/features/counterfactual/utils' import useChains from '@/hooks/useChains' @@ -22,10 +23,12 @@ import { selectRpc } from '@/store/settingsSlice' import { createWeb3ReadOnly } from '@/hooks/wallets/web3' import { predictAddressBasedOnReplayData } from '@/components/welcome/MyAccounts/utils/multiChainSafe' import { sameAddress } from '@/utils/addresses' +import ExternalLink from '@/components/common/ExternalLink' import { useRouter } from 'next/router' import ChainIndicator from '@/components/common/ChainIndicator' import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import { useMemo, useState } from 'react' +import { useCompatibleNetworks } from '../../hooks/useCompatibleNetworks' type CreateSafeOnNewChainForm = { name: string @@ -35,11 +38,12 @@ type CreateSafeOnNewChainForm = { type ReplaySafeDialogProps = { safeAddress: string safeCreationResult: ReturnType - replayableChains?: ReturnType + replayableChains?: ReturnType chain?: ChainInfo currentName: string | undefined open: boolean onClose: () => void + isUnsupportedSafeCreationVersion?: boolean } const ReplaySafeDialog = ({ @@ -50,6 +54,7 @@ const ReplaySafeDialog = ({ onClose, safeCreationResult, replayableChains, + isUnsupportedSafeCreationVersion, }: ReplaySafeDialogProps) => { const formMethods = useForm({ mode: 'all', @@ -58,8 +63,7 @@ const ReplaySafeDialog = ({ chainId: chain?.chainId, }, }) - - const { handleSubmit } = formMethods + const { handleSubmit, formState } = formMethods const router = useRouter() const customRpc = useAppSelector(selectRpc) @@ -102,17 +106,28 @@ const ReplaySafeDialog = ({ onClose() }) - const submitDisabled = !!safeCreationDataError || safeCreationDataLoading || !formMethods.formState.isValid + const submitDisabled = + isUnsupportedSafeCreationVersion || !!safeCreationDataError || safeCreationDataLoading || !formState.isValid return (
Add another network + {safeCreationDataError ? ( Could not determine the Safe creation parameters. + ) : safeCreationDataLoading ? ( + + + Loading Safe data + + ) : isUnsupportedSafeCreationVersion ? ( + + This account was created from an outdated mastercopy. Adding another network is not possible. + ) : ( @@ -122,21 +137,12 @@ const ReplaySafeDialog = ({ the Safe's version will not be reflected in the copy. - {safeCreationDataLoading ? ( - - - Loading Safe data - + + + {chain ? ( + ) : ( - <> - - - {chain ? ( - - ) : ( - - )} - + )} {creationError && ( @@ -148,13 +154,27 @@ const ReplaySafeDialog = ({ )} - - - + + + {isUnsupportedSafeCreationVersion ? ( + + + Read more + + + + ) : ( + <> + + + + )}
@@ -165,7 +185,10 @@ export const CreateSafeOnNewChain = ({ safeAddress, deployedChainIds, ...props -}: Omit & { +}: Omit< + ReplaySafeDialogProps, + 'safeCreationResult' | 'replayableChains' | 'chain' | 'isUnsupportedSafeCreationVersion' +> & { deployedChainIds: string[] }) => { const { configs } = useChains() @@ -175,18 +198,24 @@ export const CreateSafeOnNewChain = ({ ) const safeCreationResult = useSafeCreationData(safeAddress, deployedChains) - const replayableChains = useReplayableNetworks(safeCreationResult[0], deployedChainIds) + const allCompatibleChains = useCompatibleNetworks(safeCreationResult[0]) + const isUnsupportedSafeCreationVersion = Boolean(!allCompatibleChains?.length) + const replayableChains = useMemo( + () => allCompatibleChains?.filter((config) => !deployedChainIds.includes(config.chainId)) || [], + [allCompatibleChains, deployedChainIds], + ) return ( ) } export const CreateSafeOnSpecificChain = ({ ...props }: Omit) => { - return + return } diff --git a/src/features/multichain/hooks/__tests__/useReplayableNetworks.test.ts b/src/features/multichain/hooks/__tests__/useCompatibleNetworks.test.ts similarity index 77% rename from src/features/multichain/hooks/__tests__/useReplayableNetworks.test.ts rename to src/features/multichain/hooks/__tests__/useCompatibleNetworks.test.ts index f75acb3f4a..efbf7081c1 100644 --- a/src/features/multichain/hooks/__tests__/useReplayableNetworks.test.ts +++ b/src/features/multichain/hooks/__tests__/useCompatibleNetworks.test.ts @@ -1,5 +1,5 @@ import { renderHook } from '@/tests/test-utils' -import { useReplayableNetworks } from '../useReplayableNetworks' +import { useCompatibleNetworks } from '../useCompatibleNetworks' import { type ReplayedSafeProps } from '@/store/slices' import { faker } from '@faker-js/faker' import { Safe__factory } from '@/types/contracts' @@ -38,8 +38,9 @@ describe('useReplayableNetworks', () => { ], }) }) + it('should return empty list without any creation data', () => { - const { result } = renderHook(() => useReplayableNetworks(undefined, [])) + const { result } = renderHook(() => useCompatibleNetworks(undefined)) expect(result.current).toHaveLength(0) }) @@ -72,7 +73,7 @@ describe('useReplayableNetworks', () => { saltNonce: '0', setupData, } - const { result } = renderHook(() => useReplayableNetworks(creationData, [])) + const { result } = renderHook(() => useCompatibleNetworks(creationData)) expect(result.current).toHaveLength(0) }) @@ -105,7 +106,7 @@ describe('useReplayableNetworks', () => { saltNonce: '0', setupData, } - const { result } = renderHook(() => useReplayableNetworks(creationData, [])) + const { result } = renderHook(() => useCompatibleNetworks(creationData)) expect(result.current).toHaveLength(0) }) @@ -138,7 +139,7 @@ describe('useReplayableNetworks', () => { saltNonce: '0', setupData, } - const { result } = renderHook(() => useReplayableNetworks(creationData, [])) + const { result } = renderHook(() => useCompatibleNetworks(creationData)) expect(result.current).toHaveLength(0) }) @@ -153,7 +154,6 @@ describe('useReplayableNetworks', () => { payment: 0, paymentReceiver: ECOSYSTEM_ID_ADDRESS, } - const setupData = safeInterface.encodeFunctionData('setup', [ callData.owners, callData.threshold, @@ -164,7 +164,6 @@ describe('useReplayableNetworks', () => { callData.payment, callData.paymentReceiver, ]) - { const creationData: ReplayedSafeProps = { factoryAddress: PROXY_FACTORY_141_DEPLOYMENTS?.canonical?.address!, @@ -172,7 +171,7 @@ describe('useReplayableNetworks', () => { saltNonce: '0', setupData, } - const { result } = renderHook(() => useReplayableNetworks(creationData, [])) + const { result } = renderHook(() => useCompatibleNetworks(creationData)) expect(result.current).toHaveLength(4) expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '480']) } @@ -184,55 +183,7 @@ describe('useReplayableNetworks', () => { saltNonce: '0', setupData, } - const { result } = renderHook(() => useReplayableNetworks(creationData, [])) - expect(result.current).toHaveLength(4) - expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '480']) - } - }) - - it('should remove already deployed chains from result', () => { - 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: PROXY_FACTORY_141_DEPLOYMENTS?.canonical?.address!, - masterCopy: L1_141_MASTERCOPY_DEPLOYMENTS?.canonical?.address!, - saltNonce: '0', - setupData, - } - const { result } = renderHook(() => useReplayableNetworks(creationData, ['10', '100'])) - expect(result.current).toHaveLength(2) - expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '480']) - } - - { - const creationData: ReplayedSafeProps = { - factoryAddress: PROXY_FACTORY_141_DEPLOYMENTS?.canonical?.address!, - masterCopy: L2_141_MASTERCOPY_DEPLOYMENTS?.canonical?.address!, - saltNonce: '0', - setupData, - } - const { result } = renderHook(() => useReplayableNetworks(creationData, [])) + const { result } = renderHook(() => useCompatibleNetworks(creationData)) expect(result.current).toHaveLength(4) expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '480']) } @@ -269,7 +220,7 @@ describe('useReplayableNetworks', () => { saltNonce: '0', setupData, } - const { result } = renderHook(() => useReplayableNetworks(creationData, [])) + const { result } = renderHook(() => useCompatibleNetworks(creationData)) expect(result.current).toHaveLength(4) expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '480']) } @@ -282,7 +233,7 @@ describe('useReplayableNetworks', () => { saltNonce: '0', setupData, } - const { result } = renderHook(() => useReplayableNetworks(creationData, [])) + const { result } = renderHook(() => useCompatibleNetworks(creationData)) expect(result.current).toHaveLength(4) expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '480']) } @@ -295,7 +246,7 @@ describe('useReplayableNetworks', () => { saltNonce: '0', setupData, } - const { result } = renderHook(() => useReplayableNetworks(creationData, [])) + const { result } = renderHook(() => useCompatibleNetworks(creationData)) expect(result.current).toHaveLength(3) expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100']) } @@ -308,13 +259,13 @@ describe('useReplayableNetworks', () => { saltNonce: '0', setupData, } - const { result } = renderHook(() => useReplayableNetworks(creationData, [])) + const { result } = renderHook(() => useCompatibleNetworks(creationData)) expect(result.current).toHaveLength(3) expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100']) } }) - it('should return correct chains for 1.1.1 Safes', () => { + it('should return empty list for 1.1.1 Safes', () => { const callData = { owners: [faker.finance.ethereumAddress()], threshold: 1, @@ -343,8 +294,7 @@ describe('useReplayableNetworks', () => { saltNonce: '0', setupData, } - const { result } = renderHook(() => useReplayableNetworks(creationData, [])) - expect(result.current).toHaveLength(2) - expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '100']) + const { result } = renderHook(() => useCompatibleNetworks(creationData)) + expect(result.current).toHaveLength(0) }) }) diff --git a/src/features/multichain/hooks/useReplayableNetworks.ts b/src/features/multichain/hooks/useCompatibleNetworks.ts similarity index 69% rename from src/features/multichain/hooks/useReplayableNetworks.ts rename to src/features/multichain/hooks/useCompatibleNetworks.ts index d184f8a788..087ca0a4bc 100644 --- a/src/features/multichain/hooks/useReplayableNetworks.ts +++ b/src/features/multichain/hooks/useCompatibleNetworks.ts @@ -9,7 +9,7 @@ import { getSafeSingletonDeployments, } from '@safe-global/safe-deployments' -const SUPPORTED_VERSIONS: SafeVersion[] = ['1.4.1', '1.3.0', '1.1.1'] +const SUPPORTED_VERSIONS: SafeVersion[] = ['1.4.1', '1.3.0'] const hasDeployment = (chainId: string, contractAddress: string, deployments: SingletonDeploymentV2[]) => { return deployments.some((deployment) => { @@ -22,11 +22,10 @@ const hasDeployment = (chainId: string, contractAddress: string, deployments: Si } /** - * Returns all chains where the transaction can be replayed successfully. - * Therefore the creation's masterCopy and factory need to be deployed to that network. + * Returns all chains where the creations's masterCopy and factory are deployed. * @param creation */ -export const useReplayableNetworks = (creation: ReplayedSafeProps | undefined, deployedChainIds: string[]) => { +export const useCompatibleNetworks = (creation: ReplayedSafeProps | undefined) => { const { configs } = useChains() if (!creation) { @@ -51,12 +50,10 @@ export const useReplayableNetworks = (creation: ReplayedSafeProps | undefined, d getProxyFactoryDeployments({ version }), ).filter(Boolean) as SingletonDeploymentV2[] - return configs - .filter((config) => !deployedChainIds.includes(config.chainId)) - .filter( - (config) => - (hasDeployment(config.chainId, masterCopy, allL1SingletonDeployments) || - hasDeployment(config.chainId, masterCopy, allL2SingletonDeployments)) && - hasDeployment(config.chainId, factoryAddress, allProxyFactoryDeployments), - ) + return configs.filter( + (config) => + (hasDeployment(config.chainId, masterCopy, allL1SingletonDeployments) || + hasDeployment(config.chainId, masterCopy, allL2SingletonDeployments)) && + hasDeployment(config.chainId, factoryAddress, allProxyFactoryDeployments), + ) }