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: offer modal to synchronize Safe setups #4521

Draft
wants to merge 2 commits into
base: dev
Choose a base branch
from
Draft
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
1 change: 0 additions & 1 deletion src/components/tx-flow/SafeTxProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ const SafeTxProvider = ({ children }: { children: ReactNode }): ReactElement =>

createTx({ ...safeTx.data, safeTxGas: String(finalSafeTxGas) }, finalNonce)
.then((tx) => {
console.log('SafeTxProvider: Updated tx with nonce and safeTxGas', tx)
setSafeTx(tx)
})
.catch(setSafeTxError)
Expand Down
2 changes: 0 additions & 2 deletions src/components/tx/SignOrExecuteForm/SignOrExecuteForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,6 @@ export const SignOrExecuteForm = ({
const isNewExecutableTx = useImmediatelyExecutable() && isCreation
const isCorrectNonce = useValidateNonce(safeTx)

console.log(props.txDetails)

// TODO: move it to the confirmation view
// const showTxDetails =
// !isAnyStakingTxInfo(txDetails.txInfo) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import { useAppSelector } from '@/store'
import { selectCurrency, selectUndeployedSafes, useGetMultipleSafeOverviewsQuery } from '@/store/slices'
import { useAllSafesGrouped } from '@/components/welcome/MyAccounts/useAllSafesGrouped'
import { sameAddress } from '@/utils/addresses'
import { useMemo } from 'react'
import { useContext, useMemo } from 'react'
import { getDeviatingSetups, getSafeSetups } from '@/features/multichain/utils/utils'
import { Box, Typography } from '@mui/material'
import { Box, Button, Stack, Typography } from '@mui/material'
import ChainIndicator from '@/components/common/ChainIndicator'
import { TxModalContext } from '@/components/tx-flow'
import { SynchronizeSetupsFlow } from '../SynchronizeSignersFlow'

const ChainIndicatorList = ({ chainIds }: { chainIds: string[] }) => {
const { configs } = useChains()
Expand Down Expand Up @@ -54,20 +56,39 @@ export const InconsistentSignerSetupWarning = () => {
() => getSafeSetups(multiChainGroupSafes, safeOverviews ?? [], undeployedSafes),
[multiChainGroupSafes, safeOverviews, undeployedSafes],
)
const deviatingSetups = getDeviatingSetups(safeSetups, currentChain?.chainId)
const deviatingChainIds = deviatingSetups.map((setup) => setup?.chainId)

const deviatingSetups = useMemo(
() => getDeviatingSetups(safeSetups, currentChain?.chainId),
[currentChain?.chainId, safeSetups],
)
const deviatingChainIds = useMemo(() => {
return deviatingSetups.map((setup) => setup?.chainId)
}, [deviatingSetups])

const { setTxFlow } = useContext(TxModalContext)

const onClick = () => {
setTxFlow(<SynchronizeSetupsFlow deviatingSetups={deviatingSetups} />)
}

if (!isMultichainSafe || !deviatingChainIds.length) return

return (
<ErrorMessage level="warning" title="Signers are not consistent">
<Typography display="inline" mr={1}>
Signers are different on these networks of this account:
</Typography>
<ChainIndicatorList chainIds={deviatingChainIds} />
<Typography display="inline">
To manage your account easier and to prevent lose of funds, we recommend keeping the same signers.
</Typography>
<Stack direction="row" alignItems="center">
<Box>
<Typography display="inline" mr={1}>
Signers are different on these networks of this account:
</Typography>
<ChainIndicatorList chainIds={deviatingChainIds} />
<Typography display="inline">
To manage your account easier and to prevent lose of funds, we recommend keeping the same signers.
</Typography>
</Box>
<Button sx={{ mt: 1 }} variant="contained" onClick={onClick}>
Synchronize
</Button>
</Stack>
</ErrorMessage>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider'
import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm'
import useSafeInfo from '@/hooks/useSafeInfo'
import { useContext, useEffect } from 'react'
import { type SynchronizeSetupsData } from '.'
import { type SafeSetup } from '../../utils/utils'
import { getRecoveryProposalTransactions } from '@/features/recovery/services/transaction'
import { createMultiSendCallOnlyTx, createTx } from '@/services/tx/tx-sender'

export const ReviewSynchronizeSignersStep = ({
data,
setups,
}: {
data: SynchronizeSetupsData
setups: SafeSetup[]
}) => {
const { setSafeTx, setSafeTxError } = useContext(SafeTxContext)

const { safe } = useSafeInfo()

useEffect(() => {
const selectedSetup = setups.find((setup) => setup.chainId === data.selectedChain)
if (!selectedSetup) {
// TODO: handle error
return
}

const transactions = getRecoveryProposalTransactions({
safe,
newThreshold: selectedSetup.threshold,
newOwners: selectedSetup.owners.map((owner) => ({
value: owner,
})),
})

const promisedSafeTx = transactions.length > 1 ? createMultiSendCallOnlyTx(transactions) : createTx(transactions[0])

promisedSafeTx.then(setSafeTx).catch(setSafeTxError)
}, [safe, setSafeTx, setSafeTxError, setups, data.selectedChain])

return <SignOrExecuteForm />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import TxCard from '@/components/tx-flow/common/TxCard'
import { type SynchronizeSetupsData } from '.'
import { FormProvider, useForm, useFormContext } from 'react-hook-form'
import { Box, Button, ButtonBase, CardActions, Divider, Radio, Stack, Typography } from '@mui/material'
import { type SafeSetup } from '../../utils/utils'
import SafeIcon from '@/components/common/SafeIcon'
import useSafeAddress from '@/hooks/useSafeAddress'
import useAllAddressBooks from '@/hooks/useAllAddressBooks'
import { useChain } from '@/hooks/useChains'
import ChainIndicator from '@/components/common/ChainIndicator'
import { shortenAddress } from '@/utils/formatters'
import commonCss from '@/components/tx-flow/common/styles.module.css'

const SingleSafeSetup = ({
setup,
selected,
onSelect,
}: {
setup: SafeSetup
selected: boolean
onSelect: () => void
}) => {
const safeAddress = useSafeAddress()
const addressBooks = useAllAddressBooks()
const safeName = addressBooks[setup.chainId]?.[safeAddress]
const chain = useChain(setup.chainId)
return (
<ButtonBase
sx={{
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
flexDirection: 'row',
gap: '1',
border: ({ palette }) => `1px solid ${palette.border.light}`,
}}
onClick={onSelect}
>
<Radio checked={selected} />

<Box pr={2.5}>
<SafeIcon
address={safeAddress}
owners={setup.owners.length}
threshold={setup.threshold}
chainId={setup.chainId}
size={32}
/>
</Box>

<Typography variant="body2" component="div" mr={2}>
{safeName && (
<Typography variant="subtitle2" component="p" fontWeight="bold">
{safeName}
</Typography>
)}
{chain?.shortName}:
<Typography color="var(--color-primary-light)" fontSize="inherit" component="span">
{shortenAddress(safeAddress)}
</Typography>
</Typography>

<ChainIndicator chainId={setup.chainId} />
</ButtonBase>
)
}

const SetupSelector = ({ setups, selectedChain }: { setups: SafeSetup[]; selectedChain: string | null }) => {
const { setValue } = useFormContext<SynchronizeSetupsData>()
return (
<Stack spacing={1}>
{setups.map((setup) => (
<SingleSafeSetup
key={setup.chainId}
setup={setup}
selected={selectedChain === setup.chainId}
onSelect={() => setValue('selectedChain', setup.chainId)}
/>
))}
</Stack>
)
}

export const SelectNetworkStep = ({
onSubmit,
data,
deviatingSetups,
}: {
onSubmit: (data: SynchronizeSetupsData) => void
data: SynchronizeSetupsData
deviatingSetups: SafeSetup[]
}) => {
const formMethods = useForm<SynchronizeSetupsData>({
defaultValues: data,
mode: 'all',
})

const { handleSubmit, watch } = formMethods

const onFormSubmit = handleSubmit((formData: SynchronizeSetupsData) => {
onSubmit(formData)
})

const selectedChain = watch('selectedChain')

return (
<TxCard>
<FormProvider {...formMethods}>
<form onSubmit={onFormSubmit}>
<Stack spacing={1} mb={2}>
<Typography variant="body2">
This action copies the setup from another Safe account with the same address.
</Typography>
<Typography variant="h5">Select Setup to copy</Typography>
<SetupSelector setups={deviatingSetups} selectedChain={selectedChain} />
</Stack>
<Divider className={commonCss.nestedDivider} />

<CardActions>
<Button variant="contained" type="submit" disabled={selectedChain === null}>
Next
</Button>
</CardActions>
</form>
</FormProvider>
</TxCard>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import TxLayout from '@/components/tx-flow/common/TxLayout'
import useTxStepper from '@/components/tx-flow/useTxStepper'
import { SelectNetworkStep } from './SelectNetworkStep'
import { type SafeSetup } from '../../utils/utils'
import { ReviewSynchronizeSignersStep } from './ReviewSynchronizeSignersStep'

export type SynchronizeSetupsData = {
selectedChain: string | null
}

export const SynchronizeSetupsFlow = ({ deviatingSetups }: { deviatingSetups: SafeSetup[] }) => {
const { data, step, nextStep, prevStep } = useTxStepper<SynchronizeSetupsData>({ selectedChain: null })

const steps = [
<SelectNetworkStep
key={0}
onSubmit={(formData) => nextStep({ ...data, ...formData })}
data={data}
deviatingSetups={deviatingSetups}
/>,
<ReviewSynchronizeSignersStep key={1} data={data} setups={deviatingSetups} />,
]
return (
<TxLayout
title="Synchronize setup"
subtitle="Apply the Safe setup of another network"
step={step}
onBack={prevStep}
>
{steps}
</TxLayout>
)
}
1 change: 0 additions & 1 deletion src/features/multichain/hooks/useSafeCreationData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,6 @@ export const useSafeCreationData = (safeAddress: string, chains: ChainInfo[]): A

try {
const creationData = await getCreationDataForChain(chain, undeployedSafe, safeAddress, customRpc)
console.log({ creationData })
return creationData
} catch (err) {
lastError = asError(err)
Expand Down
2 changes: 1 addition & 1 deletion src/features/multichain/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { FEATURES, hasFeature } from '@/utils/chains'
import { type SafeItem } from '@/components/welcome/MyAccounts/useAllSafes'
import { type MultiChainSafeItem } from '@/components/welcome/MyAccounts/useAllSafesGrouped'

type SafeSetup = {
export type SafeSetup = {
owners: string[]
threshold: number
chainId: string
Expand Down