diff --git a/src/components/common/EnhancedTable/index.tsx b/src/components/common/EnhancedTable/index.tsx index 240dfc662f..33e6258a36 100644 --- a/src/components/common/EnhancedTable/index.tsx +++ b/src/components/common/EnhancedTable/index.tsx @@ -32,7 +32,7 @@ type EnhancedRow = { type EnhancedHeadCell = { id: string - label: string + label: ReactNode width?: string sticky?: boolean } diff --git a/src/components/dashboard/Recovery/index.tsx b/src/components/dashboard/Recovery/index.tsx index efc15d7640..aa10f0933e 100644 --- a/src/components/dashboard/Recovery/index.tsx +++ b/src/components/dashboard/Recovery/index.tsx @@ -7,16 +7,29 @@ import { WidgetBody, WidgetContainer } from '@/components/dashboard/styled' import { Chip } from '@/components/common/Chip' import { TxModalContext } from '@/components/tx-flow' import { UpsertRecoveryFlow } from '@/components/tx-flow/flows/UpsertRecovery' +import { useAppSelector } from '@/store' +import { selectRecovery } from '@/store/recoverySlice' +import { useRouter } from 'next/dist/client/router' +import { AppRoutes } from '@/config/routes' import css from './styles.module.css' export function Recovery(): ReactElement { + const router = useRouter() const { setTxFlow } = useContext(TxModalContext) + const recovery = useAppSelector(selectRecovery) - const onClick = () => { + const onEnable = () => { setTxFlow() } + const onEdit = () => { + router.push({ + pathname: AppRoutes.settings.recovery, + query: router.query, + }) + } + return ( @@ -39,9 +52,15 @@ export function Recovery(): ReactElement { Ensure that you never lose access to your funds by choosing a guardian to recover your account. - + {recovery.length === 0 ? ( + + ) : ( + + )} diff --git a/src/components/settings/Recovery/index.tsx b/src/components/settings/Recovery/index.tsx index 18cce31a70..d5e1c9ae97 100644 --- a/src/components/settings/Recovery/index.tsx +++ b/src/components/settings/Recovery/index.tsx @@ -1,18 +1,147 @@ -import { Alert, Box, Button, Grid, Paper, Typography } from '@mui/material' -import { useContext } from 'react' +import { Alert, Box, Button, Grid, IconButton, Paper, SvgIcon, Tooltip, Typography } from '@mui/material' +import { useContext, useMemo } from 'react' import type { ReactElement } from 'react' import { UpsertRecoveryFlow } from '@/components/tx-flow/flows/UpsertRecovery' import { TxModalContext } from '@/components/tx-flow' import { Chip } from '@/components/common/Chip' import ExternalLink from '@/components/common/ExternalLink' +import useIsSafeOwner from '@/hooks/useIsSafeOwner' import { useAppSelector } from '@/store' import { selectRecovery } from '@/store/recoverySlice' +import EthHashInfo from '@/components/common/EthHashInfo' +import DeleteIcon from '@/public/images/common/delete.svg' +import EditIcon from '@/public/images/common/edit.svg' +import EnhancedTable from '@/components/common/EnhancedTable' +import CheckWallet from '@/components/common/CheckWallet' +import InfoIcon from '@/public/images/notifications/info.svg' + +import tableCss from '@/components/common/EnhancedTable/styles.module.css' + +enum HeadCells { + Guardian = 'guardian', + TxCooldown = 'txCooldown', + TxExpiration = 'txExpiration', + Actions = 'actions', +} + +const headCells = [ + { id: HeadCells.Guardian, label: 'Guardian' }, + { + id: HeadCells.TxCooldown, + label: ( + <> + Recovery delay{' '} + + + + + + + ), + }, + { + id: HeadCells.TxExpiration, + label: ( + <> + Expiry{' '} + + + + + + + ), + }, + { id: HeadCells.Actions, label: '', sticky: true }, +] // TODO: Migrate section export function Recovery(): ReactElement { const { setTxFlow } = useContext(TxModalContext) const recovery = useAppSelector(selectRecovery) + const isOwner = useIsSafeOwner() + + const rows = useMemo(() => { + return recovery.flatMap(({ guardians, txCooldown, txExpiration }) => { + return guardians.map((guardian) => { + const DAY_IN_SECONDS = 60 * 60 * 24 + + const txCooldownDays = txCooldown.div(DAY_IN_SECONDS).toNumber() + const txExpirationDays = txExpiration.div(DAY_IN_SECONDS).toNumber() + + return { + cells: { + [HeadCells.Guardian]: { + rawValue: guardian, + content: , + }, + [HeadCells.TxCooldown]: { + rawValue: txCooldownDays, + content: ( + + {txCooldownDays} day{txCooldownDays > 1 ? 's' : ''} + + ), + }, + [HeadCells.TxExpiration]: { + rawValue: txExpirationDays, + content: ( + + {txExpirationDays === 0 ? 'never' : `${txExpirationDays} day${txExpirationDays > 1 ? 's' : ''}`} + + ), + }, + [HeadCells.Actions]: { + rawValue: '', + sticky: true, + content: ( +
+ {isOwner && ( + + {(isOk) => ( + <> + + + {/* TODO: Display flow */} + setTxFlow(undefined)} size="small" disabled={!isOk}> + + + + + + + + {/* TODO: Display flow */} + setTxFlow(undefined)} size="small" disabled={!isOk}> + + + + + + )} + + )} +
+ ), + }, + }, + } + }) + }) + }, [recovery, isOwner, setTxFlow]) return ( @@ -33,36 +162,21 @@ export function Recovery(): ReactElement { Enabling the Account recovery module will require a transactions.
- - Unhappy with the provided option? {/* TODO: Add link */} - - Give us feedback - - - - - {recovery ? ( - - ) : ( - - )} - + + ) : ( + + )} diff --git a/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowReview.tsx b/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowReview.tsx index c7695d2276..c2993f477e 100644 --- a/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowReview.tsx +++ b/src/components/tx-flow/flows/UpsertRecovery/UpsertRecoveryFlowReview.tsx @@ -5,7 +5,7 @@ import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm' import { Errors, logError } from '@/services/exceptions' import { createMultiSendCallOnlyTx, createTx } from '@/services/tx/tx-sender' import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider' -import { getEditRecoveryTransactions, getRecoverySetupTransactions } from '@/services/recovery/setup' +import { getRecoveryUpsertTransactions } from '@/services/recovery/setup' import { useWeb3 } from '@/hooks/wallets/web3' import useSafeInfo from '@/hooks/useSafeInfo' import { SvgIcon, Tooltip, Typography } from '@mui/material' @@ -14,43 +14,6 @@ import { TxDataRow } from '@/components/transactions/TxDetails/Summary/TxDataRow import InfoIcon from '@/public/images/notifications/info.svg' import EthHashInfo from '@/components/common/EthHashInfo' import type { UpsertRecoveryFlowProps } from '.' -import type { Web3Provider } from '@ethersproject/providers' - -const getSafeTx = async ({ - txCooldown, - txExpiration, - guardian, - provider, - moduleAddress, - chainId, - safeAddress, -}: UpsertRecoveryFlowProps & { - moduleAddress?: string - provider: Web3Provider - chainId: string - safeAddress: string -}) => { - if (moduleAddress) { - return getEditRecoveryTransactions({ - moduleAddress, - newTxCooldown: txCooldown, - newTxExpiration: txExpiration, - newGuardians: [guardian], - provider, - }) - } - - const { transactions } = getRecoverySetupTransactions({ - txCooldown, - txExpiration, - guardians: [guardian], - chainId, - safeAddress, - provider, - }) - - return transactions -} export function UpsertRecoveryFlowReview({ params, @@ -75,7 +38,7 @@ export function UpsertRecoveryFlowReview({ return } - getSafeTx({ + getRecoveryUpsertTransactions({ ...params, provider: web3, chainId: safe.chainId, diff --git a/src/services/recovery/__tests__/setup.test.ts b/src/services/recovery/__tests__/setup.test.ts index 38a32a3696..03fd408b43 100644 --- a/src/services/recovery/__tests__/setup.test.ts +++ b/src/services/recovery/__tests__/setup.test.ts @@ -5,7 +5,7 @@ import { OperationType } from '@safe-global/safe-core-sdk-types' import { SENTINEL_ADDRESS } from '@safe-global/safe-core-sdk/dist/src/utils/constants' import type { Web3Provider } from '@ethersproject/providers' -import { getEditRecoveryTransactions, getRecoverySetupTransactions } from '@/services/recovery/setup' +import { _getEditRecoveryTransactions, _getRecoverySetupTransactions } from '@/services/recovery/setup' jest.mock('@gnosis.pm/zodiac', () => ({ ...jest.requireActual('@gnosis.pm/zodiac'), @@ -45,7 +45,7 @@ describe('getRecoverySetupTransactions', () => { transaction: deployDelayModifierTx, }) - const result = getRecoverySetupTransactions({ + const result = _getRecoverySetupTransactions({ txCooldown, txExpiration, guardians, @@ -115,7 +115,7 @@ describe('getEditRecoveryTransactions', () => { }, } as any) - const transactions = await getEditRecoveryTransactions({ + const transactions = await _getEditRecoveryTransactions({ provider: {} as Web3Provider, newTxCooldown: txCooldown, newTxExpiration, @@ -155,7 +155,7 @@ describe('getEditRecoveryTransactions', () => { }, } as any) - const transactions = await getEditRecoveryTransactions({ + const transactions = await _getEditRecoveryTransactions({ provider: {} as Web3Provider, newTxCooldown, newTxExpiration: txExpiration, @@ -195,7 +195,7 @@ describe('getEditRecoveryTransactions', () => { }, } as any) - const transactions = await getEditRecoveryTransactions({ + const transactions = await _getEditRecoveryTransactions({ provider: {} as Web3Provider, newTxCooldown: txCooldown, newTxExpiration: txExpiration, @@ -241,7 +241,7 @@ describe('getEditRecoveryTransactions', () => { }, } as any) - const transactions = await getEditRecoveryTransactions({ + const transactions = await _getEditRecoveryTransactions({ provider: {} as Web3Provider, newTxCooldown: txCooldown, newTxExpiration: txExpiration, @@ -285,7 +285,7 @@ describe('getEditRecoveryTransactions', () => { }, } as any) - const transactions = await getEditRecoveryTransactions({ + const transactions = await _getEditRecoveryTransactions({ provider: {} as Web3Provider, newTxCooldown, newTxExpiration, @@ -344,7 +344,7 @@ describe('getEditRecoveryTransactions', () => { }, } as any) - const transactions = await getEditRecoveryTransactions({ + const transactions = await _getEditRecoveryTransactions({ provider: {} as Web3Provider, newTxCooldown: txCooldown, newTxExpiration: txExpiration, diff --git a/src/services/recovery/recovery-state.ts b/src/services/recovery/recovery-state.ts index 31406543fc..07961e3456 100644 --- a/src/services/recovery/recovery-state.ts +++ b/src/services/recovery/recovery-state.ts @@ -188,7 +188,7 @@ export const getRecoveryState = async ({ chainId: string version: SafeInfo['version'] }): Promise => { - const [[modules], txExpiration, txCooldown, txNonce, queueNonce] = await Promise.all([ + const [[guardians], txExpiration, txCooldown, txNonce, queueNonce] = await Promise.all([ delayModifier.getModulesPaginated(SENTINEL_ADDRESS, MAX_GUARDIAN_PAGE_SIZE), delayModifier.txExpiration(), delayModifier.txCooldown(), @@ -222,7 +222,7 @@ export const getRecoveryState = async ({ return { address: delayModifier.address, - guardians: modules, + guardians, txExpiration, txCooldown, txNonce, diff --git a/src/services/recovery/setup.ts b/src/services/recovery/setup.ts index 9308d146ad..27b35e5174 100644 --- a/src/services/recovery/setup.ts +++ b/src/services/recovery/setup.ts @@ -7,8 +7,9 @@ import type { MetaTransactionData } from '@safe-global/safe-core-sdk-types' import { sameAddress } from '@/utils/addresses' import { MAX_GUARDIAN_PAGE_SIZE } from './recovery-state' +import type { UpsertRecoveryFlowProps } from '@/components/tx-flow/flows/UpsertRecovery' -export function getRecoverySetupTransactions({ +export function _getRecoverySetupTransactions({ txCooldown, txExpiration, guardians, @@ -88,7 +89,7 @@ export function getRecoverySetupTransactions({ } } -export async function getEditRecoveryTransactions({ +export async function _getEditRecoveryTransactions({ newTxCooldown, newTxExpiration, newGuardians, @@ -165,3 +166,39 @@ export async function getEditRecoveryTransactions({ data, })) } + +export async function getRecoveryUpsertTransactions({ + txCooldown, + txExpiration, + guardian, + provider, + moduleAddress, + chainId, + safeAddress, +}: UpsertRecoveryFlowProps & { + moduleAddress?: string + provider: Web3Provider + chainId: string + safeAddress: string +}): Promise> { + if (moduleAddress) { + return _getEditRecoveryTransactions({ + moduleAddress, + newTxCooldown: txCooldown, + newTxExpiration: txExpiration, + newGuardians: [guardian], + provider, + }) + } + + const { transactions } = _getRecoverySetupTransactions({ + txCooldown, + txExpiration, + guardians: [guardian], + chainId, + safeAddress, + provider, + }) + + return transactions +}