From 2dd64f197ee62152d23388392b91462018714ccc Mon Sep 17 00:00:00 2001 From: schmanu Date: Thu, 14 Nov 2024 14:12:39 +0700 Subject: [PATCH 1/2] feat: offer modal to synchronize Safe setups --- .../InconsistentSignerSetupWarning.tsx | 43 ++++-- .../ReviewSynchronizeSignersStep.tsx | 42 ++++++ .../SelectNetworkStep.tsx | 129 ++++++++++++++++++ .../SynchronizeSignersFlow/index.tsx | 33 +++++ .../src/features/multichain/utils/utils.ts | 2 +- 5 files changed, 237 insertions(+), 12 deletions(-) create mode 100644 apps/web/src/features/multichain/components/SynchronizeSignersFlow/ReviewSynchronizeSignersStep.tsx create mode 100644 apps/web/src/features/multichain/components/SynchronizeSignersFlow/SelectNetworkStep.tsx create mode 100644 apps/web/src/features/multichain/components/SynchronizeSignersFlow/index.tsx diff --git a/apps/web/src/features/multichain/components/SignerSetupWarning/InconsistentSignerSetupWarning.tsx b/apps/web/src/features/multichain/components/SignerSetupWarning/InconsistentSignerSetupWarning.tsx index 6043d68e5d..1b52ef5d3f 100644 --- a/apps/web/src/features/multichain/components/SignerSetupWarning/InconsistentSignerSetupWarning.tsx +++ b/apps/web/src/features/multichain/components/SignerSetupWarning/InconsistentSignerSetupWarning.tsx @@ -6,10 +6,12 @@ import { useAppSelector } from '@/store' import { selectCurrency, selectUndeployedSafes, useGetMultipleSafeOverviewsQuery } from '@/store/slices' import { useAllSafesGrouped } from '@/features/myAccounts/hooks/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() @@ -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() + } if (!isMultichainSafe || !deviatingChainIds.length) return return ( - - Signers are different on these networks of this account: - - - - To manage your account easier and to prevent lose of funds, we recommend keeping the same signers. - + + + + Signers are different on these networks of this account: + + + + To manage your account easier and to prevent lose of funds, we recommend keeping the same signers. + + + + ) } diff --git a/apps/web/src/features/multichain/components/SynchronizeSignersFlow/ReviewSynchronizeSignersStep.tsx b/apps/web/src/features/multichain/components/SynchronizeSignersFlow/ReviewSynchronizeSignersStep.tsx new file mode 100644 index 0000000000..8cd21bc8d5 --- /dev/null +++ b/apps/web/src/features/multichain/components/SynchronizeSignersFlow/ReviewSynchronizeSignersStep.tsx @@ -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 +} diff --git a/apps/web/src/features/multichain/components/SynchronizeSignersFlow/SelectNetworkStep.tsx b/apps/web/src/features/multichain/components/SynchronizeSignersFlow/SelectNetworkStep.tsx new file mode 100644 index 0000000000..ee56e31bcd --- /dev/null +++ b/apps/web/src/features/multichain/components/SynchronizeSignersFlow/SelectNetworkStep.tsx @@ -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 ( + `1px solid ${palette.border.light}`, + }} + onClick={onSelect} + > + + + + + + + + {safeName && ( + + {safeName} + + )} + {chain?.shortName}: + + {shortenAddress(safeAddress)} + + + + + + ) +} + +const SetupSelector = ({ setups, selectedChain }: { setups: SafeSetup[]; selectedChain: string | null }) => { + const { setValue } = useFormContext() + return ( + + {setups.map((setup) => ( + setValue('selectedChain', setup.chainId)} + /> + ))} + + ) +} + +export const SelectNetworkStep = ({ + onSubmit, + data, + deviatingSetups, +}: { + onSubmit: (data: SynchronizeSetupsData) => void + data: SynchronizeSetupsData + deviatingSetups: SafeSetup[] +}) => { + const formMethods = useForm({ + defaultValues: data, + mode: 'all', + }) + + const { handleSubmit, watch } = formMethods + + const onFormSubmit = handleSubmit((formData: SynchronizeSetupsData) => { + onSubmit(formData) + }) + + const selectedChain = watch('selectedChain') + + return ( + + +
+ + + This action copies the setup from another Safe account with the same address. + + Select Setup to copy + + + + + + + + +
+
+ ) +} diff --git a/apps/web/src/features/multichain/components/SynchronizeSignersFlow/index.tsx b/apps/web/src/features/multichain/components/SynchronizeSignersFlow/index.tsx new file mode 100644 index 0000000000..03f767e65d --- /dev/null +++ b/apps/web/src/features/multichain/components/SynchronizeSignersFlow/index.tsx @@ -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({ selectedChain: null }) + + const steps = [ + nextStep({ ...data, ...formData })} + data={data} + deviatingSetups={deviatingSetups} + />, + , + ] + return ( + + {steps} + + ) +} diff --git a/apps/web/src/features/multichain/utils/utils.ts b/apps/web/src/features/multichain/utils/utils.ts index be50ff10f4..a5db3cb51e 100644 --- a/apps/web/src/features/multichain/utils/utils.ts +++ b/apps/web/src/features/multichain/utils/utils.ts @@ -13,7 +13,7 @@ import { type SafeItem } from '@/features/myAccounts/hooks/useAllSafes' import { type MultiChainSafeItem } from '@/features/myAccounts/hooks/useAllSafesGrouped' import { LATEST_SAFE_VERSION } from '@/config/constants' -type SafeSetup = { +export type SafeSetup = { owners: string[] threshold: number chainId: string From b5e1f502069a4c86ae7b9ff1a41bd9613668c65e Mon Sep 17 00:00:00 2001 From: schmanu Date: Mon, 6 Jan 2025 13:04:18 +0100 Subject: [PATCH 2/2] fix(web): missing hook dependency (lint) --- apps/web/src/components/common/TokenIcon/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/common/TokenIcon/index.tsx b/apps/web/src/components/common/TokenIcon/index.tsx index 3df24157ed..232c08e245 100644 --- a/apps/web/src/components/common/TokenIcon/index.tsx +++ b/apps/web/src/components/common/TokenIcon/index.tsx @@ -19,7 +19,7 @@ const TokenIcon = ({ }): ReactElement => { const src = useMemo(() => { return logoUri?.replace(COINGECKO_THUMB, COINGECKO_SMALL) - }, []) + }, [logoUri]) return (