Skip to content

Commit

Permalink
feat: recovery proposal flow
Browse files Browse the repository at this point in the history
  • Loading branch information
iamacook committed Nov 14, 2023
1 parent 4468fea commit b326d52
Show file tree
Hide file tree
Showing 14 changed files with 1,275 additions and 17 deletions.
19 changes: 16 additions & 3 deletions src/components/settings/Recovery/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ import type { ReactElement } from 'react'
import { EnableRecoveryFlow } from '@/components/tx-flow/flows/EnableRecovery'
import { TxModalContext } from '@/components/tx-flow'
import { useDarkMode } from '@/hooks/useDarkMode'
import { RecoverAccountFlow } from '@/components/tx-flow/flows/RecoverAccount'
import useWallet from '@/hooks/wallets/useWallet'
import { useAppSelector } from '@/store'
import { selectRecoveryByGuardian } from '@/store/recoverySlice'

export function Recovery(): ReactElement {
const { setTxFlow } = useContext(TxModalContext)
const isDarkMode = useDarkMode()
const wallet = useWallet()
const recovery = useAppSelector((state) => selectRecoveryByGuardian(state, wallet?.address ?? ''))

return (
<Paper sx={{ p: 4 }}>
Expand All @@ -35,9 +41,16 @@ export function Recovery(): ReactElement {
Enabling the Account recovery module will require a transactions.
</Typography>

<Button variant="contained" onClick={() => setTxFlow(<EnableRecoveryFlow />)}>
Set up recovery
</Button>
{recovery ? (
// TODO: Move to correct location when widget is ready
<Button variant="contained" onClick={() => setTxFlow(<RecoverAccountFlow />)}>
Propose recovery
</Button>
) : (
<Button variant="contained" onClick={() => setTxFlow(<EnableRecoveryFlow />)}>
Set up recovery
</Button>
)}
</Grid>
</Grid>
</Paper>
Expand Down
30 changes: 30 additions & 0 deletions src/components/tx-flow/common/NewOwnerList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Paper, Typography, SvgIcon } from '@mui/material'
import type { AddressEx } from '@safe-global/safe-gateway-typescript-sdk'
import type { ReactElement } from 'react'

import PlusIcon from '@/public/images/common/plus.svg'
import EthHashInfo from '@/components/common/EthHashInfo'

import css from './styles.module.css'

export function NewOwnerList({ newOwners }: { newOwners: Array<AddressEx> }): ReactElement {
return (
<Paper className={css.container}>
<Typography color="text.secondary" display="flex" alignItems="center">
<SvgIcon component={PlusIcon} inheritViewBox fontSize="small" sx={{ mr: 1 }} />
New owner{newOwners.length > 1 ? 's' : ''}
</Typography>
{newOwners.map((newOwner) => (
<EthHashInfo
key={newOwner.value}
address={newOwner.value}
name={newOwner.name}
shortAddress={false}
showCopyButton
hasExplorer
avatarSize={32}
/>
))}
</Paper>
)
}
7 changes: 7 additions & 0 deletions src/components/tx-flow/common/NewOwnerList/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.container {
display: flex;
flex-direction: column;
gap: var(--space-1);
padding: var(--space-2);
background-color: var(--color-success-background);
}
10 changes: 2 additions & 8 deletions src/components/tx-flow/flows/AddOwner/ReviewOwner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { upsertAddressBookEntry } from '@/store/addressBookSlice'
import { SafeTxContext } from '../../SafeTxProvider'
import type { AddOwnerFlowProps } from '.'
import type { ReplaceOwnerFlowProps } from '../ReplaceOwner'
import PlusIcon from '@/public/images/common/plus.svg'
import { NewOwnerList } from '../../common/NewOwnerList'
import MinusIcon from '@/public/images/common/minus.svg'
import EthHashInfo from '@/components/common/EthHashInfo'
import commonCss from '@/components/tx-flow/common/styles.module.css'
Expand Down Expand Up @@ -68,13 +68,7 @@ export const ReviewOwner = ({ params }: { params: AddOwnerFlowProps | ReplaceOwn
/>
</Paper>
)}
<Paper sx={{ backgroundColor: ({ palette }) => palette.success.background, p: 2 }}>
<Typography color="text.secondary" mb={2} display="flex" alignItems="center">
<SvgIcon component={PlusIcon} inheritViewBox fontSize="small" sx={{ mr: 1 }} />
New owner
</Typography>
<EthHashInfo name={newOwner.name} address={newOwner.address} shortAddress={false} showCopyButton hasExplorer />
</Paper>
<NewOwnerList newOwners={[{ name: newOwner.name, value: newOwner.address }]} />
<Divider className={commonCss.nestedDivider} />
<Box>
<Typography variant="body2">Any transaction requires the confirmation of:</Typography>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { CardActions, Button, Typography, Divider, Box } from '@mui/material'
import { useContext, useEffect, useState } from 'react'
import type { ReactElement } from 'react'

import useSafeInfo from '@/hooks/useSafeInfo'
import { getRecoveryProposalTransactions } from '@/services/recovery/transaction'
import DecodedTx from '@/components/tx/DecodedTx'
import ErrorMessage from '@/components/tx/ErrorMessage'
import { RedefineBalanceChanges } from '@/components/tx/security/redefine/RedefineBalanceChange'
import ConfirmationTitle, { ConfirmationTitleTypes } from '@/components/tx/SignOrExecuteForm/ConfirmationTitle'
import TxChecks from '@/components/tx/SignOrExecuteForm/TxChecks'
import { WrongChainWarning } from '@/components/tx/WrongChainWarning'
import useDecodeTx from '@/hooks/useDecodeTx'
import TxCard from '../../common/TxCard'
import { SafeTxContext } from '../../SafeTxProvider'
import CheckWallet from '@/components/common/CheckWallet'
import { createMultiSendCallOnlyTx, createTx, dispatchRecoveryProposal } from '@/services/tx/tx-sender'
import { RecoverAccountFlowFields } from '.'
import { NewOwnerList } from '../../common/NewOwnerList'
import { useAppSelector } from '@/store'
import { selectRecoveryByGuardian } from '@/store/recoverySlice'
import useWallet from '@/hooks/wallets/useWallet'
import useOnboard from '@/hooks/wallets/useOnboard'
import { TxModalContext } from '../..'
import { asError } from '@/services/exceptions/utils'
import { trackError, Errors } from '@/services/exceptions'
import type { RecoverAccountFlowProps } from '.'

import commonCss from '@/components/tx-flow/common/styles.module.css'

export function RecoverAccountFlowReview({ params }: { params: RecoverAccountFlowProps }): ReactElement | null {
// Form state
const [isSubmittable, setIsSubmittable] = useState<boolean>(true)
const [submitError, setSubmitError] = useState<Error | undefined>()

// Hooks
const { setTxFlow } = useContext(TxModalContext)
const { safeTx, safeTxError, setSafeTx, setSafeTxError } = useContext(SafeTxContext)
const [decodedData, decodedDataError, decodedDataLoading] = useDecodeTx(safeTx)
const { safe } = useSafeInfo()
const wallet = useWallet()
const onboard = useOnboard()
const recovery = useAppSelector((state) => selectRecoveryByGuardian(state, wallet?.address ?? ''))

// Proposal
const txCooldown = recovery?.txCooldown?.toNumber()
const newThreshold = Number(params[RecoverAccountFlowFields.threshold])
const newOwners = params[RecoverAccountFlowFields.owners]

useEffect(() => {
const transactions = getRecoveryProposalTransactions({
safe,
newThreshold,
newOwners,
})

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

promise.then(setSafeTx).catch(setSafeTxError)
}, [newThreshold, newOwners, safe, setSafeTx, setSafeTxError])

// On modal submit
const onSubmit = async () => {
if (!recovery || !onboard) {
return
}

setIsSubmittable(false)
setSubmitError(undefined)

try {
await dispatchRecoveryProposal({ onboard, safe, newThreshold, newOwners, delayModifierAddress: recovery.address })
} catch (_err) {
const err = asError(_err)
trackError(Errors._810, err)
setIsSubmittable(true)
setSubmitError(err)
return
}

setTxFlow(undefined)
}

const submitDisabled = !safeTx || !isSubmittable || !recovery

return (
<>
<TxCard>
<Typography mb={1}>
This transaction will reset the Account setup, changing the owners
{newThreshold !== safe.threshold ? ' and threshold' : ''}.
</Typography>

<NewOwnerList newOwners={newOwners} />

<Divider className={commonCss.nestedDivider} sx={{ mt: 'var(--space-2) !important' }} />

<Box my={1}>
<Typography variant="body2" color="text.secondary" gutterBottom>
After recovery, Safe Account transactions will require:
</Typography>
<Typography>
<b>{params.threshold}</b> out of <b>{params[RecoverAccountFlowFields.owners].length} owners.</b>
</Typography>
</Box>

<Divider className={commonCss.nestedDivider} />

<DecodedTx
tx={safeTx}
decodedData={decodedData}
decodedDataError={decodedDataError}
decodedDataLoading={decodedDataLoading}
/>

<RedefineBalanceChanges />
</TxCard>

<TxCard>
<TxChecks isRecovery />
</TxCard>

<TxCard>
<ConfirmationTitle variant={ConfirmationTitleTypes.execute} />

{safeTxError && (
<ErrorMessage error={safeTxError}>
This recovery will most likely fail. To save gas costs, avoid executing the transaction.
</ErrorMessage>
)}

{submitError && (
<ErrorMessage error={submitError}>Error submitting the transaction. Please try again.</ErrorMessage>
)}

<WrongChainWarning />

<ErrorMessage level="info">
{/* // TODO: Convert txCooldown to days, minutes, seconds when https://github.com/safe-global/safe-wallet-web/pull/2772 is merged */}
Recovery will be {txCooldown === 0 ? 'immediately possible' : `possible ${txCooldown}`} after this transaction
is executed.
</ErrorMessage>

<Divider className={commonCss.nestedDivider} />

<CardActions sx={{ mt: 'var(--space-1) !important' }}>
<CheckWallet allowNonOwner>
{(isOk) => (
<Button variant="contained" disabled={!isOk || submitDisabled} onClick={onSubmit}>
Execute
</Button>
)}
</CheckWallet>
</CardActions>
</TxCard>
</>
)
}
Loading

0 comments on commit b326d52

Please sign in to comment.