Skip to content

Commit

Permalink
feat: enable recovery flow structure
Browse files Browse the repository at this point in the history
  • Loading branch information
iamacook committed Nov 8, 2023
1 parent 2fce8de commit 17e31e6
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 0 deletions.
38 changes: 38 additions & 0 deletions src/components/settings/Recovery/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Box, Button, Chip, Grid, Paper, Typography } from '@mui/material'
import { useContext } from 'react'
import type { ReactElement } from 'react'

import { EnableRecoveryFlow } from '@/components/tx-flow/flows/EnableRecovery'
import { TxModalContext } from '@/components/tx-flow'

export function Recovery(): ReactElement {
const { setTxFlow } = useContext(TxModalContext)

return (
<Paper sx={{ p: 4 }}>
<Grid container spacing={3}>
<Grid item lg={4} xs={12}>
<Box display="flex" alignItems="center" gap={1} mb={1}>
<Typography variant="h4" fontWeight="bold">
Account recovery
</Typography>

{/* TODO: Extract when widget is merged https://github.com/safe-global/safe-wallet-web/pull/2768 */}
<Chip label="New" color="primary" size="small" sx={{ borderRadius: '4px', fontSize: '12px' }} />
</Box>
</Grid>

<Grid item xs>
<Typography mb={3}>
Choose a trusted guardian to recover your Safe Account, in case you should ever lose access to your Account.
Enabling the Account recovery module will require a transactions.
</Typography>

<Button variant="contained" onClick={() => setTxFlow(<EnableRecoveryFlow />)}>
Set up recovery
</Button>
</Grid>
</Grid>
</Paper>
)
}
4 changes: 4 additions & 0 deletions src/components/sidebar/SidebarNavigation/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ export const settingsNavItems = [
label: 'Appearance',
href: AppRoutes.settings.appearance,
},
{
label: 'Recovery',
href: AppRoutes.settings.recovery,
},
{
label: 'Notifications',
href: AppRoutes.settings.notifications,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { ReactElement } from 'react'

import TxCard from '../../common/TxCard'

export function EnableRecoveryFlowIntro(): ReactElement {
return <TxCard>EnableRecoveryFlowIntro</TxCard>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useContext, useEffect, useMemo } from 'react'
import type { ReactElement } from 'react'

import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm'
import { Errors, logError } from '@/services/exceptions'
import { createMultiSendCallOnlyTx } from '@/services/tx/tx-sender'
import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider'
import { getRecoverySetup } from '@/services/recovery/setup'
import { useWeb3 } from '@/hooks/wallets/web3'
import useSafeInfo from '@/hooks/useSafeInfo'
import type { EnableRecoveryFlowProps } from '.'

export function EnableRecoveryFlowReview({ params }: { params: EnableRecoveryFlowProps }): ReactElement {
const web3 = useWeb3()
const { safe } = useSafeInfo()
const { setSafeTx, safeTxError, setSafeTxError } = useContext(SafeTxContext)

const recoverySetup = useMemo(() => {
if (!web3) {
return
}

return getRecoverySetup({
...params,
safe,
provider: web3,
})
}, [params, safe, web3])

useEffect(() => {
if (recoverySetup) {
createMultiSendCallOnlyTx(recoverySetup.transactions).then(setSafeTx).catch(setSafeTxError)
}
}, [recoverySetup, setSafeTx, setSafeTxError])

useEffect(() => {
if (safeTxError) {
logError(Errors._809, safeTxError.message)
}
}, [safeTxError])

return <SignOrExecuteForm onSubmit={() => null} />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { ReactElement } from 'react'

import TxCard from '../../common/TxCard'
import type { EnableRecoveryFlowProps } from '.'

export function EnableRecoveryFlowSettings({
params,
onSubmit,
}: {
params: EnableRecoveryFlowProps
onSubmit: (formData: EnableRecoveryFlowProps) => void
}): ReactElement {
return <TxCard>EnableRecoveryFlowSettings</TxCard>
}
58 changes: 58 additions & 0 deletions src/components/tx-flow/flows/EnableRecovery/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { ReactElement } from 'react'

import TxLayout from '@/components/tx-flow/common/TxLayout'
import CodeIcon from '@/public/images/apps/code-icon.svg'
import useTxStepper from '../../useTxStepper'
import { EnableRecoveryFlowReview } from './EnableRecoveryFlowReview'
import { EnableRecoveryFlowSettings } from './EnableRecoveryFlowSettings'
import { EnableRecoveryFlowIntro } from './EnableRecoveryFlowIntro'

export enum EnableRecoveryFlowFields {
guardians = 'guardians',
txCooldown = 'txCooldown',
txExpiration = 'txExpiration',
}

export type EnableRecoveryFlowProps = {
[EnableRecoveryFlowFields.guardians]: Array<string>
[EnableRecoveryFlowFields.txCooldown]: string
[EnableRecoveryFlowFields.txExpiration]: string
}

export function EnableRecoveryFlow(): ReactElement {
const { data, step, nextStep, prevStep } = useTxStepper<EnableRecoveryFlowProps>({
[EnableRecoveryFlowFields.guardians]: [],
[EnableRecoveryFlowFields.txCooldown]: '0',
[EnableRecoveryFlowFields.txExpiration]: '0',
})

const steps = [
<EnableRecoveryFlowIntro key={0} />,
<EnableRecoveryFlowSettings key={1} params={data} onSubmit={(formData) => nextStep({ ...data, ...formData })} />,
<EnableRecoveryFlowReview key={1} params={data} />,
]

const isIntro = step === 0
const isSettings = step === 1

const subtitle = isIntro
? 'How does recovery work?'
: isSettings
? 'Set up account settings'
: 'Set up account recovery'

const icon = isIntro ? undefined : CodeIcon

return (
<TxLayout
title="Account recovery"
subtitle={subtitle}
icon={icon}
step={step}
onBack={prevStep}
hideNonce={isIntro}
>
{steps}
</TxLayout>
)
}
1 change: 1 addition & 0 deletions src/config/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const AppRoutes = {
settings: {
spendingLimits: '/settings/spending-limits',
setup: '/settings/setup',
recovery: '/settings/recovery',
notifications: '/settings/notifications',
modules: '/settings/modules',
index: '/settings',
Expand Down
24 changes: 24 additions & 0 deletions src/pages/settings/recovery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Head from 'next/head'
import type { NextPage } from 'next'

import SettingsHeader from '@/components/settings/SettingsHeader'
import { Recovery } from '@/components/settings/Recovery'

// TODO: Condense to other setting section once confirmed
const RecoveryPage: NextPage = () => {
return (
<>
<Head>
<title>{'Safe{Wallet} – Settings – Recovery'}</title>
</Head>

<SettingsHeader />

<main>
<Recovery />
</main>
</>
)
}

export default RecoveryPage
85 changes: 85 additions & 0 deletions src/services/recovery/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { getModuleInstance, KnownContracts, deployAndSetUpModule } from '@gnosis.pm/zodiac'
import { Interface } from 'ethers/lib/utils'
import type { Web3Provider } from '@ethersproject/providers'
import type { SafeInfo } from '@safe-global/safe-gateway-typescript-sdk'
import type { MetaTransactionData } from '@safe-global/safe-core-sdk-types'

export function getRecoverySetup({
txCooldown,
txExpiration,
guardians,
safe,
provider,
}: {
txCooldown: string
txExpiration: string
guardians: Array<string>
safe: SafeInfo
provider: Web3Provider
}): {
expectedModuleAddress: string
transactions: Array<MetaTransactionData>
} {
const safeAddress = safe.address.value

const setupArgs: Parameters<typeof deployAndSetUpModule>[1] = {
types: ['address', 'address', 'address', 'uint256', 'uint256'],
values: [
safeAddress, // address _owner
safeAddress, // address _avatar
safeAddress, // address _target
txCooldown, // uint256 _cooldown
txExpiration, // uint256 _expiration
],
}

const saltNonce: Parameters<typeof deployAndSetUpModule>[4] = Date.now().toString()

const { transaction, expectedModuleAddress } = deployAndSetUpModule(
KnownContracts.DELAY,
setupArgs,
provider,
Number(safe.chainId),
saltNonce,
)

const transactions: Array<MetaTransactionData> = []

// Deploy Delay Modifier
const deployDeplayModifier: MetaTransactionData = {
...transaction,
value: transaction.value.toString(),
}

transactions.push(deployDeplayModifier)

const safeAbi = ['function enableModule(address module)']
const safeInterface = new Interface(safeAbi)

// Enable Delay Modifier on Safe
const enableDelayModifier: MetaTransactionData = {
to: safeAddress,
value: '0',
data: safeInterface.encodeFunctionData('enableModule', [expectedModuleAddress]),
}

transactions.push(enableDelayModifier)

const delayModifierContract = getModuleInstance(KnownContracts.DELAY, expectedModuleAddress, provider)

// Add guardians to Delay Modifier
const enableDelayModifierModules: Array<MetaTransactionData> = guardians.map((guardian) => {
return {
to: expectedModuleAddress,
data: delayModifierContract.interface.encodeFunctionData('enableModule', [guardian]),
value: '0',
}
})

transactions.push(...enableDelayModifierModules)

return {
expectedModuleAddress,
transactions,
}
}

0 comments on commit 17e31e6

Please sign in to comment.