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 3 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
45 changes: 22 additions & 23 deletions src/components/common/NetworkSelector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,25 +66,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 @@ -136,13 +143,15 @@ const UndeployedNetworks = ({
[availableNetworks],
)

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

const onSelect = (chain: ChainInfo) => {
setReplayOnChain(chain)
}

if (safeCreationDataError) {
if (safeCreationDataError || (safeCreationData && noAvailableNetworks)) {
return (
<Box p="0px 16px">
<Box px={2} py={1}>
<Typography color="text.secondary" fontSize="14px">
Adding another network is not possible for this Safe
</Typography>
Expand Down Expand Up @@ -174,21 +183,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 @@ -76,7 +76,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 @@ -106,43 +106,10 @@ describe('useReplayableNetworks', () => {
setupData,
}
const { result } = renderHook(() => useReplayableNetworks(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(() => useReplayableNetworks(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 @@ -173,8 +140,9 @@ describe('useReplayableNetworks', () => {
setupData,
}
const { result } = renderHook(() => useReplayableNetworks(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 @@ -185,12 +153,13 @@ describe('useReplayableNetworks', () => {
setupData,
}
const { result } = renderHook(() => useReplayableNetworks(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 remove already deployed chains from result', () => {
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(() => useReplayableNetworks(creationData, ['10', '100']))
expect(result.current).toHaveLength(2)
expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '480'])
expect(result.current).toHaveLength(3)
expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '324', '480'])
expect(result.current.map((chain) => chain.available)).toEqual([true, false, true])
}

{
Expand All @@ -233,8 +203,9 @@ describe('useReplayableNetworks', () => {
setupData,
}
const { result } = renderHook(() => useReplayableNetworks(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 Down Expand Up @@ -270,8 +241,9 @@ describe('useReplayableNetworks', () => {
setupData,
}
const { result } = renderHook(() => useReplayableNetworks(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 @@ -283,8 +255,9 @@ describe('useReplayableNetworks', () => {
setupData,
}
const { result } = renderHook(() => useReplayableNetworks(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 @@ -296,8 +269,9 @@ describe('useReplayableNetworks', () => {
setupData,
}
const { result } = renderHook(() => useReplayableNetworks(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 @@ -309,8 +283,9 @@ describe('useReplayableNetworks', () => {
setupData,
}
const { result } = renderHook(() => useReplayableNetworks(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])
}
})

Expand Down Expand Up @@ -344,7 +319,8 @@ describe('useReplayableNetworks', () => {
setupData,
}
const { result } = renderHook(() => useReplayableNetworks(creationData, []))
expect(result.current).toHaveLength(2)
expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '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, false, true, false, false])
})
})
21 changes: 14 additions & 7 deletions src/features/multichain/hooks/useReplayableNetworks.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', '1.1.1']

Expand All @@ -26,7 +27,10 @@ const hasDeployment = (chainId: string, contractAddress: string, deployments: Si
* Therefore the creation's masterCopy and factory need to be deployed to that network.
* @param creation
*/
export const useReplayableNetworks = (creation: ReplayedSafeProps | undefined, deployedChainIds: string[]) => {
export const useReplayableNetworks = (
creation: ReplayedSafeProps | undefined,
deployedChainIds: string[],
): (ChainInfo & { available: boolean })[] => {
const { configs } = useChains()

if (!creation) {
Expand All @@ -53,10 +57,13 @@ export const useReplayableNetworks = (creation: ReplayedSafeProps | undefined, d

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),
)
.map((config) => {
return {
...config,
available:
(hasDeployment(config.chainId, masterCopy, allL1SingletonDeployments) ||
hasDeployment(config.chainId, masterCopy, allL2SingletonDeployments)) &&
hasDeployment(config.chainId, factoryAddress, allProxyFactoryDeployments),
}
})
}
Loading