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

feat(Multichain): Multichain Safes with same address #4120

Draft
wants to merge 63 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
4069d34
feat: show multichain accounts in sidebar (#4090)
schmanu Aug 28, 2024
957f236
fix: counting of total Safes in the sidebar
schmanu Aug 28, 2024
a0116da
Feat: replay safe creation with same address (#4116)
schmanu Aug 29, 2024
3c50dd8
Merge branch 'dev' into epic/multichain-safes
jmealy Sep 10, 2024
6d44e2f
[Multichain] feat: automatically migrate incompatible Safes from L1 t…
schmanu Sep 11, 2024
bb82043
[Multichain] Feat: new network select (#4124)
schmanu Sep 11, 2024
eb6381f
Merge remote-tracking branch 'origin/dev' into epic/multichain-safes
schmanu Sep 12, 2024
ee2257e
[Multichain] feat: SetupToL2 during Safe creation [SW-95] (#4075)
schmanu Sep 12, 2024
0cf633e
[Multichain] Feat: show warning when changing signer setup in a multi…
jmealy Sep 12, 2024
60f08a5
Merge remote-tracking branch 'origin/dev' into epic/multichain-safes
schmanu Sep 13, 2024
09fc8a6
[Multichain] fix: Hide header network selector on non-safe routes [SW…
usame-algan Sep 16, 2024
80c8649
Merge remote-tracking branch 'origin/dev' into epic/multichain-safes
usame-algan Sep 16, 2024
b0d17ea
fix: allow activating Safes as non-owner (#4176)
schmanu Sep 17, 2024
4f5e9bf
[Multichain] Feat: disable adding new chains to safes created from ol…
jmealy Sep 17, 2024
6555c95
[Multichain] feat: redesign of multiaccounts and context menu for mul…
schmanu Sep 17, 2024
8680d6b
[Multichain] fix: Network selector links and visual style [SW-184] (#…
usame-algan Sep 17, 2024
1db148e
[Multichain] fix: Zksync network selector [SW-152] (#4185)
usame-algan Sep 17, 2024
938d524
fix: show unavailable networks when replaying Safes (#4197)
schmanu Sep 18, 2024
99b0cb1
fix: check for canonical deployments (#4193)
schmanu Sep 19, 2024
c81e54d
fix: show network when activating a safe (#4199)
jmealy Sep 19, 2024
bf6ea57
[Multichain] Fix: precise counterfactual safes (#4191)
schmanu Sep 19, 2024
91f0566
[Multichain] Feat: multichain feature toggle (#4209)
schmanu Sep 19, 2024
7c321fe
Fix: multichain disable tx creation for undeployed Safes (#4208)
schmanu Sep 19, 2024
27f2970
fix: dont add setupToL2 for older safe version (#4213)
schmanu Sep 19, 2024
d450f1c
Feat(Multichain): Show warning on dashboard when multichain setup is …
jmealy Sep 20, 2024
05197e0
fix(Multichain): memoize data for InconsistentSignerSetupWarning (#4232)
schmanu Sep 23, 2024
244dad2
Merge remote-tracking branch 'origin/dev' into epic/multichain-safes
schmanu Sep 23, 2024
126a8bb
[Multichain] fix: Disable submit button while replaying safe [SW-173]…
usame-algan Sep 23, 2024
2f8d774
Chore: update lint action (#4230)
katspaugh Sep 23, 2024
a4ee3d6
fix: do not disable currently selected network in the network selecto…
jmealy Sep 23, 2024
357fc85
fix(Multichain): add more unsupported Safe cases (#4233)
schmanu Sep 23, 2024
caf5b96
[Multichain] fix: Pass options for safe creation [SW-199] (#4234)
usame-algan Sep 23, 2024
8679c93
Fix(Multichain): replay modal loses focus when typing certain letters…
jmealy Sep 24, 2024
916318f
fix(Multichain): overview of non multichain Safe (#4235)
schmanu Sep 24, 2024
8e6b2e8
[Multichain] fix: Add analytics events [SW-165] (#4228)
usame-algan Sep 24, 2024
88a9549
fix: Minified react error because of DOM nesting (#4244)
usame-algan Sep 24, 2024
11754e1
fix: use wallet network as default for safe creation (#4241)
jmealy Sep 24, 2024
5c9be78
Fix(Multichain): Show owner setup warning also when replacing owners …
jmealy Sep 24, 2024
ffe9139
fix: Add loading spinner to add new network form when submitting (#4245)
usame-algan Sep 25, 2024
54f7c47
Feat(Multichain): simplify multichain naming options [SW-187] [SW-221…
jmealy Sep 25, 2024
1e971f7
Merge remote-tracking branch 'origin/dev' into epic/multichain-safes
usame-algan Sep 25, 2024
6880bf0
fix: Display current chain network logo in success screen as a fallba…
usame-algan Sep 25, 2024
2910cec
chore: Fix lint issues
usame-algan Sep 25, 2024
3ff63d4
Merge remote-tracking branch 'origin/epic/multichain-safes' into epic…
usame-algan Sep 25, 2024
a701240
fix: Redirect user to dashboard after safe creation (#4252)
usame-algan Sep 25, 2024
ea83b21
Feat: cached safe overviews (#4221)
schmanu Sep 25, 2024
d899d2b
Feat(Multichain): update text on safe creation review step [SW-225] (…
jmealy Sep 25, 2024
3361831
fix: Show notification when adding a new network to an account (#4250)
usame-algan Sep 26, 2024
2531ac1
fix: throw custom error for empty setupData (#4255)
schmanu Sep 26, 2024
5c7d23c
Feat(Multichain): use contract addresses from safe-deployments [SW-16…
jmealy Sep 30, 2024
d5e6219
Merge remote-tracking branch 'origin/dev' into epic/multichain-safes
schmanu Sep 30, 2024
c63ea91
[Multichain] fix: Adjust multichain design [SW-244] (#4284)
usame-algan Sep 30, 2024
2d832eb
Fix(Multichain): ignore empty safe address when loading safeOverviews…
schmanu Oct 1, 2024
f38021c
feat: block adding networks to safes with unknown setupModule calls (…
schmanu Oct 1, 2024
03c6810
fix: Adjust balance alignment in safe list (#4286)
usame-algan Oct 1, 2024
e8e0f67
[Multichain] fix: Check all fallbackHandler deployments [SW-236] (#4281)
usame-algan Oct 1, 2024
bfcbba1
fix: decoding of migration txs (#4291)
schmanu Oct 1, 2024
c8592a9
fix: detection of migration txs in SignOrExecuteForm (#4296)
schmanu Oct 2, 2024
a5b4870
Feat(Multichain): explain impossible network addition (#4283)
schmanu Oct 2, 2024
6cd64f8
Feat(Multichain): add feature flag checks when adding a new network […
jmealy Oct 2, 2024
d7ee967
Fix(Multichain): do not migrate to L2 if no lib contract is available…
schmanu Oct 2, 2024
4ff9d0f
Merge remote-tracking branch 'origin/dev' into epic/multichain-safes
schmanu Oct 2, 2024
c8f5406
fix: Display Activate now button in sidebar for counterfactual safes …
usame-algan Oct 2, 2024
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
7 changes: 7 additions & 0 deletions cypress/e2e/pages/create_wallet.pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const googleSignedinBtn = '[data-testid="signed-in-account-btn"]'
export const accountInfoHeader = '[data-testid="open-account-center"]'
export const reviewStepOwnerInfo = '[data-testid="review-step-owner-info"]'
const reviewStepNextBtn = '[data-testid="review-step-next-btn"]'
const creationModalLetsGoBtn = '[data-testid="cf-creation-lets-go-btn"]'
const safeCreationStatusInfo = '[data-testid="safe-status-info"]'
const startUsingSafeBtn = '[data-testid="start-using-safe-btn"]'
const sponsorIcon = '[data-testid="sponsor-icon"]'
Expand Down Expand Up @@ -123,6 +124,12 @@ export function clickOnReviewStepNextBtn() {
cy.get(reviewStepNextBtn).click()
cy.get(reviewStepNextBtn, { timeout: 60000 }).should('not.exist')
}

export function clickOnLetsGoBtn() {
cy.get(creationModalLetsGoBtn).click()
cy.get(creationModalLetsGoBtn, { timeout: 60000 }).should('not.exist')
}

export function verifyOwnerInfoIsPresent() {
return cy.get(reviewStepOwnerInfo).shoul('exist')
}
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/pages/load_safe.pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export function verifyDataInReviewSection(safeName, ownerName, threshold = null,
cy.findByText(ownerName).should('be.visible')
if (ownerAddress !== null) cy.get(safeDataForm).contains(ownerAddress).should('be.visible')
if (threshold !== null) cy.get(safeDataForm).contains(threshold).should('be.visible')
if (network !== null) cy.get(sidebar.chainLogo).eq(1).contains(network).should('be.visible')
if (network !== null) cy.get(sidebar.chainLogo).eq(0).contains(network).should('be.visible')
}

export function clickOnAddBtn() {
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/pages/owners.pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function verifyOwnerDeletionWindowDisplayed() {
}

function clickOnThresholdDropdown() {
cy.get(thresholdDropdown).eq(1).click()
cy.get(thresholdDropdown).eq(0).click()
}

export function getThresholdOptions() {
Expand Down
1 change: 1 addition & 0 deletions cypress/e2e/smoke/create_safe_cf.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe('[SMOKE] CF Safe creation tests', () => {
},
]
checkDataLayerEvents(safe_created)
createwallet.clickOnLetsGoBtn()
createwallet.verifyCFSafeCreated()
})
})
Expand Down
6 changes: 1 addition & 5 deletions cypress/e2e/smoke/import_export_data.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import * as file from '../pages/import_export.pages'
import * as main from '../pages/main.page'
import * as constants from '../../support/constants'
import * as ls from '../../support/localstorage_data.js'
import * as createwallet from '../pages/create_wallet.pages'
import * as sideBar from '../pages/sidebar.pages'
import { getSafes, CATEGORIES } from '../../support/safes/safesHandler.js'

Expand All @@ -16,10 +15,7 @@ describe('[SMOKE] Import Export Data tests', () => {

beforeEach(() => {
cy.clearLocalStorage()
cy.visit(constants.dataSettingsUrl).then(() => {
main.acceptCookies()
createwallet.selectNetwork(constants.networks.sepolia)
})
cy.visit(constants.dataSettingsUrl)
})

it('[SMOKE] Verify Safe can be accessed after test file upload', () => {
Expand Down
7 changes: 7 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
// Learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect'
import { TextEncoder, TextDecoder } from 'util'
import { Headers, Request, Response } from 'node-fetch'

jest.mock('@web3-onboard/coinbase', () => jest.fn())
jest.mock('@web3-onboard/injected-wallets', () => ({ ProviderLabel: { MetaMask: 'MetaMask' } }))
jest.mock('@web3-onboard/keystone/dist/index', () => jest.fn())
jest.mock('@web3-onboard/ledger/dist/index', () => jest.fn())
jest.mock('@web3-onboard/trezor', () => jest.fn())
jest.mock('@web3-onboard/walletconnect', () => jest.fn())
jest.mock('safe-client-gateway-sdk')

const mockOnboardState = {
chains: [],
Expand Down Expand Up @@ -68,3 +70,8 @@ Object.defineProperty(Uint8Array, Symbol.hasInstance, {
: Uint8Array[Symbol.hasInstance].call(this, potentialInstance)
},
})

// These are required for safe-client-gateway-sdk
globalThis.Request = Request
globalThis.Response = Response
globalThis.Headers = Headers
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"@safe-global/api-kit": "^2.4.6",
"@safe-global/protocol-kit": "^4.1.1",
"@safe-global/safe-apps-sdk": "^9.1.0",
"@safe-global/safe-deployments": "^1.37.8",
"@safe-global/safe-deployments": "^1.37.10",
"@safe-global/safe-gateway-typescript-sdk": "3.22.3-beta.15",
"@safe-global/safe-modules-deployments": "^2.2.1",
"@sentry/react": "^7.91.0",
Expand Down Expand Up @@ -91,6 +91,7 @@
"react-hook-form": "7.41.1",
"react-papaparse": "^4.0.2",
"react-redux": "^9.1.2",
"safe-client-gateway-sdk": "git+https://github.com/safe-global/safe-client-gateway-sdk.git#v1.53.0-next-7344903",
"semver": "^7.6.3",
"zodiac-roles-deployments": "^2.2.5"
},
Expand Down
8 changes: 8 additions & 0 deletions public/images/sidebar/multichain-account.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 10 additions & 5 deletions src/components/address-book/EntryDialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import ModalDialog from '@/components/common/ModalDialog'
import NameInput from '@/components/common/NameInput'
import useChainId from '@/hooks/useChainId'
import { useAppDispatch } from '@/store'
import { upsertAddressBookEntry } from '@/store/addressBookSlice'
import madProps from '@/utils/mad-props'
import { upsertAddressBookEntries } from '@/store/addressBookSlice'

export type AddressEntry = {
name: string
Expand All @@ -22,13 +22,13 @@ function EntryDialog({
address: '',
},
disableAddressInput = false,
chainId,
chainIds,
currentChainId,
}: {
handleClose: () => void
defaultValues?: AddressEntry
disableAddressInput?: boolean
chainId?: string
chainIds?: string[]
currentChainId: string
}): ReactElement {
const dispatch = useAppDispatch()
Expand All @@ -41,7 +41,7 @@ function EntryDialog({
const { handleSubmit, formState } = methods

const submitCallback = handleSubmit((data: AddressEntry) => {
dispatch(upsertAddressBookEntry({ ...data, chainId: chainId || currentChainId }))
dispatch(upsertAddressBookEntries({ ...data, chainIds: chainIds ?? [currentChainId] }))
handleClose()
})

Expand All @@ -51,7 +51,12 @@ function EntryDialog({
}

return (
<ModalDialog open onClose={handleClose} dialogTitle={defaultValues.name ? 'Edit entry' : 'Create entry'}>
<ModalDialog
open
onClose={handleClose}
dialogTitle={defaultValues.name ? 'Edit entry' : 'Create entry'}
hideChainIndicator={chainIds && chainIds.length > 1}
>
<FormProvider {...methods}>
<form onSubmit={onSubmit}>
<DialogContent>
Expand Down
4 changes: 2 additions & 2 deletions src/components/address-book/ImportDialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { ParseResult } from 'papaparse'
import { type ReactElement, useState, type MouseEvent, useMemo } from 'react'

import ModalDialog from '@/components/common/ModalDialog'
import { upsertAddressBookEntry } from '@/store/addressBookSlice'
import { upsertAddressBookEntries } from '@/store/addressBookSlice'
import { useAppDispatch } from '@/store'

import css from './styles.module.css'
Expand Down Expand Up @@ -60,7 +60,7 @@ const ImportDialog = ({ handleClose }: { handleClose: () => void }): ReactElemen

for (const entry of entries) {
const [address, name, chainId] = entry
dispatch(upsertAddressBookEntry({ address, name, chainId: chainId.trim() }))
dispatch(upsertAddressBookEntries({ address, name, chainIds: [chainId.trim()] }))
}

trackEvent({ ...ADDRESS_BOOK_EVENTS.IMPORT, label: entries.length })
Expand Down
20 changes: 17 additions & 3 deletions src/components/common/ChainIndicator/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ import { useAppSelector } from '@/store'
import { selectChainById, selectChains } from '@/store/chainsSlice'
import css from './styles.module.css'
import useChainId from '@/hooks/useChainId'
import { Skeleton } from '@mui/material'
import { Skeleton, Stack, Typography } from '@mui/material'
import isEmpty from 'lodash/isEmpty'
import FiatValue from '../FiatValue'

type ChainIndicatorProps = {
chainId?: string
inline?: boolean
className?: string
showUnknown?: boolean
showLogo?: boolean
onlyLogo?: boolean
responsive?: boolean
fiatValue?: string
}

const fallbackChainConfig = {
Expand All @@ -29,11 +32,13 @@ const fallbackChainConfig = {

const ChainIndicator = ({
chainId,
fiatValue,
className,
inline = false,
showUnknown = true,
showLogo = true,
responsive = false,
onlyLogo = false,
}: ChainIndicatorProps): ReactElement | null => {
const currentChainId = useChainId()
const id = chainId || currentChainId
Expand Down Expand Up @@ -63,6 +68,7 @@ const ChainIndicator = ({
[css.indicator]: !inline,
[css.withLogo]: showLogo,
[css.responsive]: responsive,
[css.onlyLogo]: onlyLogo,
})}
>
{showLogo && (
Expand All @@ -74,8 +80,16 @@ const ChainIndicator = ({
loading="lazy"
/>
)}

<span className={css.name}>{chainConfig.chainName}</span>
{!onlyLogo && (
<Stack>
<span className={css.name}>{chainConfig.chainName}</span>
{fiatValue && (
<Typography fontWeight={700} textAlign="left" fontSize="14px">
<FiatValue value={fiatValue} />
</Typography>
)}
</Stack>
)}
</span>
) : null
}
Expand Down
4 changes: 4 additions & 0 deletions src/components/common/ChainIndicator/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
justify-content: flex-start;
}

.onlyLogo {
min-width: 0;
}

@media (max-width: 899.95px) {
.indicator {
min-width: 35px;
Expand Down
86 changes: 80 additions & 6 deletions src/components/common/CheckWallet/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { render } from '@/tests/test-utils'
import { getByLabelText, render } from '@/tests/test-utils'
import CheckWallet from '.'
import useIsOnlySpendingLimitBeneficiary from '@/hooks/useIsOnlySpendingLimitBeneficiary'
import useIsSafeOwner from '@/hooks/useIsSafeOwner'
import useIsWrongChain from '@/hooks/useIsWrongChain'
import useWallet from '@/hooks/wallets/useWallet'
import { chainBuilder } from '@/tests/builders/chains'
import { useIsWalletDelegate } from '@/hooks/useDelegates'
import { faker } from '@faker-js/faker'
import { extendedSafeInfoBuilder } from '@/tests/builders/safe'
import useSafeInfo from '@/hooks/useSafeInfo'

// mock useWallet
jest.mock('@/hooks/wallets/useWallet', () => ({
Expand Down Expand Up @@ -38,6 +42,25 @@ jest.mock('@/hooks/useIsWrongChain', () => ({
default: jest.fn(() => false),
}))

jest.mock('@/hooks/useDelegates', () => ({
__esModule: true,
useIsWalletDelegate: jest.fn(() => false),
}))

jest.mock('@/hooks/useSafeInfo', () => ({
__esModule: true,
default: jest.fn(() => {
const safeAddress = faker.finance.ethereumAddress()
return {
safeAddress,
safe: extendedSafeInfoBuilder()
.with({ address: { value: safeAddress } })
.with({ deployed: true })
.build(),
}
}),
}))

const renderButton = () =>
render(<CheckWallet checkNetwork={false}>{(isOk) => <button disabled={!isOk}>Continue</button>}</CheckWallet>)

Expand All @@ -62,7 +85,7 @@ describe('CheckWallet', () => {
expect(container.querySelector('button')).toBeDisabled()

// Check the tooltip text
expect(container.querySelector('span[aria-label]')).toHaveAttribute('aria-label', 'Please connect your wallet')
getByLabelText(container, 'Please connect your wallet')
})

it('should disable the button when the wallet is connected to the right chain but is not an owner', () => {
Expand Down Expand Up @@ -98,10 +121,7 @@ describe('CheckWallet', () => {
const { container } = renderButton()

expect(container.querySelector('button')).toBeDisabled()
expect(container.querySelector('span[aria-label]')).toHaveAttribute(
'aria-label',
'Your connected wallet is not a signer of this Safe Account',
)
getByLabelText(container, 'Your connected wallet is not a signer of this Safe Account')

const { container: allowContainer } = render(
<CheckWallet allowSpendingLimit>{(isOk) => <button disabled={!isOk}>Continue</button>}</CheckWallet>,
Expand All @@ -110,6 +130,60 @@ describe('CheckWallet', () => {
expect(allowContainer.querySelector('button')).not.toBeDisabled()
})

it('should not disable the button for delegates', () => {
;(useIsSafeOwner as jest.MockedFunction<typeof useIsSafeOwner>).mockReturnValueOnce(false)
;(useIsWalletDelegate as jest.MockedFunction<typeof useIsWalletDelegate>).mockReturnValueOnce(true)

const { container } = renderButton()

expect(container.querySelector('button')).not.toBeDisabled()
})

it('should disable the button for counterfactual Safes', () => {
;(useIsSafeOwner as jest.MockedFunction<typeof useIsSafeOwner>).mockReturnValueOnce(true)

const safeAddress = faker.finance.ethereumAddress()
const mockSafeInfo = {
safeAddress,
safe: extendedSafeInfoBuilder()
.with({ address: { value: safeAddress } })
.with({ deployed: false })
.build(),
}

;(useSafeInfo as jest.MockedFunction<typeof useSafeInfo>).mockReturnValueOnce(
mockSafeInfo as unknown as ReturnType<typeof useSafeInfo>,
)

const { container } = renderButton()

expect(container.querySelector('button')).toBeDisabled()
getByLabelText(container, 'You need to activate the Safe before transacting')
})

it('should enable the button for counterfactual Safes if allowed', () => {
;(useIsSafeOwner as jest.MockedFunction<typeof useIsSafeOwner>).mockReturnValueOnce(true)

const safeAddress = faker.finance.ethereumAddress()
const mockSafeInfo = {
safeAddress,
safe: extendedSafeInfoBuilder()
.with({ address: { value: safeAddress } })
.with({ deployed: false })
.build(),
}

;(useSafeInfo as jest.MockedFunction<typeof useSafeInfo>).mockReturnValueOnce(
mockSafeInfo as unknown as ReturnType<typeof useSafeInfo>,
)

const { container } = render(
<CheckWallet allowUndeployedSafe>{(isOk) => <button disabled={!isOk}>Continue</button>}</CheckWallet>,
)

expect(container.querySelector('button')).toBeEnabled()
})

it('should allow non-owners if specified', () => {
;(useIsSafeOwner as jest.MockedFunction<typeof useIsSafeOwner>).mockReturnValueOnce(false)

Expand Down
Loading
Loading