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 (
@@ -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),
+ )
}