Skip to content

Commit

Permalink
Merge branch 'epic-tx-flow' into recommended-nonce
Browse files Browse the repository at this point in the history
  • Loading branch information
katspaugh authored May 30, 2023
2 parents 76dd717 + e2c9972 commit a815695
Show file tree
Hide file tree
Showing 25 changed files with 947 additions and 375 deletions.
67 changes: 67 additions & 0 deletions src/components/TxFlow/ModalProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
createContext,
type Dispatch,
type ReactElement,
type ReactNode,
type SetStateAction,
useState,
type ComponentProps,
useEffect,
} from 'react'
import NewModalDialog from '@/components/common/NewModalDialog'
import { ReplaceTxMenu, NewTxMenu, RejectTx, TokenTransferFlow } from '@/components/TxFlow'
import { useRouter } from 'next/router'

export enum ModalType {
SendTokens = 'sendTokens',
RejectTx = 'rejectTx',
ReplaceTx = 'replaceTx',
NewTx = 'newTx',
}

const ModalTypes = {
[ModalType.SendTokens]: TokenTransferFlow,
[ModalType.RejectTx]: RejectTx,
[ModalType.ReplaceTx]: ReplaceTxMenu,
[ModalType.NewTx]: NewTxMenu,
}

type VisibleModalState<T extends ModalType> = {
type: T
props: ComponentProps<typeof ModalTypes[T]>
}

type ContextProps<T extends ModalType> = {
visibleModal: VisibleModalState<T> | undefined
setVisibleModal: Dispatch<SetStateAction<VisibleModalState<T> | undefined>>
}

export const ModalContext = createContext<ContextProps<ModalType>>({
visibleModal: undefined,
setVisibleModal: () => {},
})

export const ModalProvider = ({ children }: { children: ReactNode }): ReactElement => {
const [visibleModal, setVisibleModal] = useState<VisibleModalState<ModalType>>()
const router = useRouter()

const Component = visibleModal ? ModalTypes[visibleModal.type] : null
const props = visibleModal ? visibleModal.props : {}

// Close the modal if user navigates
useEffect(() => {
router.events.on('routeChangeComplete', () => {
setVisibleModal(undefined)
})
}, [router])

return (
<ModalContext.Provider value={{ visibleModal, setVisibleModal }}>
{children}
<NewModalDialog open={!!visibleModal}>
{/* @ts-ignore TODO: Fix this somehow */}
{visibleModal && <Component {...props} />}
</NewModalDialog>
</ModalContext.Provider>
)
}
39 changes: 39 additions & 0 deletions src/components/TxFlow/NewTx/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import TxButton, { SendNFTsButton, SendTokensButton } from '@/components/TxFlow/common/TxButton'
import { useTxBuilderApp } from '@/hooks/safe-apps/useTxBuilderApp'
import { Box, Typography } from '@mui/material'
import { ModalContext, ModalType } from '@/components/TxFlow/ModalProvider'
import { useContext } from 'react'

const BUTTONS_HEIGHT = '91px'

const NewTxMenu = () => {
const { setVisibleModal } = useContext(ModalContext)
const txBuilder = useTxBuilderApp()

return (
<Box display="flex" flexDirection="column" alignItems="center" gap={2} width={452} m="auto">
<Typography variant="h6" fontWeight={700}>
New transaction
</Typography>
<SendTokensButton
onClick={() => setVisibleModal({ type: ModalType.SendTokens, props: {} })}
sx={{ height: BUTTONS_HEIGHT }}
/>

<SendNFTsButton onClick={() => console.log('open send NFTs flow')} sx={{ height: BUTTONS_HEIGHT }} />

{txBuilder && txBuilder.app && (
<TxButton
startIcon={<img src={txBuilder.app.iconUrl} height={20} width="auto" alt={txBuilder.app.name} />}
variant="outlined"
onClick={() => console.log('open contract interaction flow')}
sx={{ height: BUTTONS_HEIGHT }}
>
Contract interaction
</TxButton>
)}
</Box>
)
}

export default NewTxMenu
38 changes: 38 additions & 0 deletions src/components/TxFlow/RejectTx/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { ReactElement } from 'react'
import { Typography } from '@mui/material'
import type { SafeTransaction } from '@safe-global/safe-core-sdk-types'

import useAsync from '@/hooks/useAsync'
import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm'
import { createRejectTx } from '@/services/tx/tx-sender'
import TxLayout from '@/components/TxFlow/common/TxLayout'

type RejectTxProps = {
txNonce: number
}

const RejectTx = ({ txNonce }: RejectTxProps): ReactElement => {
const [rejectTx, rejectError] = useAsync<SafeTransaction>(() => {
return createRejectTx(txNonce)
}, [txNonce])

return (
<TxLayout title="Reject transaction">
<SignOrExecuteForm safeTx={rejectTx} isRejection onSubmit={() => {}} error={rejectError}>
<Typography mb={2}>
To reject the transaction, a separate rejection transaction will be created to replace the original one.
</Typography>

<Typography mb={2}>
Transaction nonce: <b>{txNonce}</b>
</Typography>

<Typography mb={2}>
You will need to confirm the rejection transaction with your currently connected wallet.
</Typography>
</SignOrExecuteForm>
</TxLayout>
)
}

export default RejectTx
150 changes: 150 additions & 0 deletions src/components/TxFlow/ReplaceTx/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { Button, Container, Grid, Paper, Step, StepLabel, Stepper, SvgIcon, Tooltip, Typography } from '@mui/material'

import InfoIcon from '@/public/images/notifications/info.svg'
import RocketIcon from '@/public/images/transactions/rocket.svg'
import CheckIcon from '@mui/icons-material/Check'
import DeleteIcon from '@/public/images/common/delete.svg'
import { SendTokensButton } from '@/components/TxFlow/common/TxButton'
import { useQueuedTxByNonce } from '@/hooks/useTxQueue'
import { isCustomTxInfo } from '@/utils/transaction-guards'

import css from './styles.module.css'
import { useContext } from 'react'
import { ModalContext, ModalType } from '@/components/TxFlow/ModalProvider'

const wrapIcon = (icon: React.ReactNode) => <div className={css.circle}>{icon}</div>

const steps = [
{
label: 'Create new transaction with same nonce',
icon: <div className={css.redCircle} />,
},
{
label: 'Collect confirmations from owners',
icon: wrapIcon(<CheckIcon fontSize="small" color="border" />),
},
{
label: 'Execute replacement transaction',
icon: wrapIcon(<SvgIcon component={RocketIcon} inheritViewBox fontSize="small" color="border" />),
},
{
label: 'Initial transaction is replaced',
icon: wrapIcon(<SvgIcon component={DeleteIcon} inheritViewBox fontSize="small" color="border" />),
},
]

const btnWidth = {
width: {
xs: 240,
sm: '100%',
},
}

const ReplaceTxMenu = ({ txNonce }: { txNonce: number }) => {
const { setVisibleModal } = useContext(ModalContext)
const queuedTxsByNonce = useQueuedTxByNonce(txNonce)
const canCancel = !queuedTxsByNonce?.some(
(item) => isCustomTxInfo(item.transaction.txInfo) && item.transaction.txInfo.isCancellation,
)

return (
<Container>
<Grid container justifyContent="center">
<Grid item component={Paper} xs={8}>
<div className={css.container}>
<Typography variant="h5" mb={1} textAlign="center">
Need to replace or discard this transaction?
</Typography>
<Typography variant="body1" textAlign="center">
A signed transaction cannot be removed but it can be replaced with a new transaction with the same nonce.
</Typography>
<Stepper alternativeLabel className={css.stepper}>
{steps.map(({ label }) => (
<Step key={label}>
<StepLabel StepIconComponent={({ icon }) => steps[Number(icon) - 1].icon}>
<Typography variant="body1" fontWeight={700}>
{label}
</Typography>
</StepLabel>
</Step>
))}
</Stepper>
</div>
<div className={css.container}>
<Grid container alignItems="center" justifyContent="center" flexDirection="row">
<Grid item xs={12}>
<Typography variant="body2" textAlign="center" fontWeight={700} mb={3}>
Select how you would like to replace this transaction
</Typography>
</Grid>
<Grid item container justifyContent="center" alignItems="center" gap={1} xs={12} sm flexDirection="row">
<SendTokensButton
onClick={() => setVisibleModal({ type: ModalType.SendTokens, props: { txNonce } })}
sx={btnWidth}
/>
</Grid>
<Grid item>
<Typography variant="body2" className={css.or}>
or
</Typography>
</Grid>
<Grid
item
container
xs={12}
sm
justifyContent={{
xs: 'center',
sm: 'flex-start',
}}
alignItems="center"
textAlign="center"
flexDirection="row"
>
<Tooltip
arrow
placement="top"
title={canCancel ? '' : `Transaction with nonce ${txNonce} already has a reject transaction`}
>
<span style={{ width: '100%' }}>
<Button
onClick={() => setVisibleModal({ type: ModalType.RejectTx, props: { txNonce } })}
variant="outlined"
fullWidth
sx={{ mb: 1, ...btnWidth }}
disabled={!canCancel}
>
Reject transaction
</Button>
</span>
</Tooltip>

<div>
<Typography variant="caption" display="flex" alignItems="center">
How does it work?{' '}
<Tooltip
title={`An on-chain rejection doesn't send any funds. Executing an on-chain rejection will replace all currently awaiting transactions with nonce ${txNonce}.`}
arrow
>
<span>
<SvgIcon
component={InfoIcon}
inheritViewBox
fontSize="small"
color="border"
sx={{ verticalAlign: 'middle', ml: 0.5 }}
/>
</span>
</Tooltip>
</Typography>
</div>
</Grid>
</Grid>
</div>
</Grid>
</Grid>
</Container>
)
}

export default ReplaceTxMenu
Loading

0 comments on commit a815695

Please sign in to comment.