Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Multichain] fix: Zksync network selector [SW-152] #4185

Merged
merged 4 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 27 additions & 27 deletions src/components/common/NetworkSelector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,25 +67,32 @@ export const getNetworkLink = (router: NextRouter, networkShortName: string, isW
}

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
Comment on lines -80 to -82
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why we needed to pass chains here just to filter for the current chain again since we iterate over prodNets and testNets we have access to the chain anyway.

const isDisabled = !chain.available

return (
<MenuItem value={chainId} sx={{ '&:hover': { backgroundColor: 'inherit' } }} onClick={() => onSelect(chain)}>
<MenuItem
value={chain.chainId}
sx={{ '&:hover': { backgroundColor: 'inherit' } }}
onClick={() => onSelect(chain)}
disabled={isDisabled}
>
<Box className={css.item}>
<ChainIndicator responsive={isSelected} chainId={chain.chainId} inline />
<PlusIcon className={css.plusIcon} />
{isDisabled ? (
<Typography variant="caption" component="span" className={css.comingSoon}>
Not available
</Typography>
) : (
<PlusIcon className={css.plusIcon} />
)}
</Box>
</MenuItem>
)
Expand Down Expand Up @@ -143,6 +150,8 @@ const UndeployedNetworks = ({
[availableNetworks],
)

const noAvailableNetworks = useMemo(() => availableNetworks.every((config) => !config.available), [availableNetworks])

const onSelect = (chain: ChainInfo) => {
setReplayOnChain(chain)
}
Expand All @@ -155,15 +164,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 (
<Box p="0px 16px">
<Box px={2} py={1}>
<Typography color="text.secondary" fontSize="14px" maxWidth={300}>
{errorMessage}
</Typography>
Expand Down Expand Up @@ -195,21 +205,11 @@ const UndeployedNetworks = ({
) : (
<>
{prodNets.map((chain) => (
<UndeployedNetworkMenuItem
chainConfigs={chains}
chainId={chain.chainId}
onSelect={onSelect}
key={chain.chainId}
/>
<UndeployedNetworkMenuItem chain={chain} onSelect={onSelect} key={chain.chainId} />
))}
{testNets.length > 0 && <TestnetDivider />}
{testNets.map((chain) => (
<UndeployedNetworkMenuItem
chainConfigs={chains}
chainId={chain.chainId}
onSelect={onSelect}
key={chain.chainId}
/>
<UndeployedNetworkMenuItem chain={chain} onSelect={onSelect} key={chain.chainId} />
))}
</>
)}
Expand Down
8 changes: 8 additions & 0 deletions src/components/common/NetworkSelector/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
.item {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-1);
width: 100%;
}
Expand All @@ -81,3 +82,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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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])
}

{
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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])
})
})
20 changes: 13 additions & 7 deletions src/features/multichain/hooks/useCompatibleNetworks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']

Expand All @@ -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) {
Expand All @@ -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),
}
})
}
Loading