Skip to content

Commit

Permalink
Feat: batch any tx
Browse files Browse the repository at this point in the history
  • Loading branch information
katspaugh committed Jul 7, 2023
1 parent 9baef84 commit 069a360
Show file tree
Hide file tree
Showing 13 changed files with 295 additions and 14 deletions.
3 changes: 3 additions & 0 deletions public/images/common/batch.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions src/components/common/BatchIndicator/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ButtonBase, SvgIcon } from '@mui/material'
import BatchIcon from '@/public/images/common/batch.svg'

const BatchIndicator = ({ onClick }: { onClick?: () => void }) => {
return (
<ButtonBase onClick={onClick}>
<SvgIcon component={BatchIcon} inheritViewBox fontSize="small" />
</ButtonBase>
)
}

export default BatchIndicator
19 changes: 19 additions & 0 deletions src/components/common/BatchSidebar/BatchTxItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Box, SvgIcon } from '@mui/material'
import type { MetaTransactionData } from '@safe-global/safe-core-sdk-types'
import CustomTxIcon from '@/public/images/transactions/custom.svg'
import DateTime from '../DateTime'
import { EthHashInfo } from '@safe-global/safe-react-components'

const BatchTxItem = ({ txData, timestamp }: { txData: MetaTransactionData; timestamp: number }) => {
return (
<Box display="flex" gap={2} mb={1} p={2} sx={{ border: '1px solid', borderColor: 'border.light', borderRadius: 2 }}>
<SvgIcon component={CustomTxIcon} viewBox="0 0 16 16" width={24} height={24} />
<Box flex={1}>
<EthHashInfo address={txData.to} showAvatar={false} />
</Box>
<DateTime value={timestamp} />
</Box>
)
}

export default BatchTxItem
62 changes: 62 additions & 0 deletions src/components/common/BatchSidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { SyntheticEvent } from 'react'
import { useCallback, useContext } from 'react'
import { Button, Divider, Drawer, Typography } from '@mui/material'
import { useDraftBatch } from '@/hooks/useDraftBatch'
import css from './styles.module.css'
import NewTxMenu from '@/components/tx-flow/flows/NewTx'
import { TxModalContext } from '@/components/tx-flow'
import BatchTxItem from './BatchTxItem'
import ConfirmBatchFlow from '@/components/tx-flow/flows/ConfirmBatch'

const BatchSidebar = ({ isOpen, onToggle }: { isOpen: boolean; onToggle: (open: boolean) => void }) => {
const { setTxFlow } = useContext(TxModalContext)
const batchTxs = useDraftBatch()
const closeSidebar = useCallback(() => onToggle(false), [onToggle])

const onAddClick = useCallback(
(e: SyntheticEvent) => {
e.preventDefault()
closeSidebar()
setTxFlow(<NewTxMenu />, undefined, false)
},
[closeSidebar, setTxFlow],
)

const onConfirmClick = useCallback(
async (e: SyntheticEvent) => {
e.preventDefault()
if (!batchTxs.length) return
closeSidebar()
setTxFlow(<ConfirmBatchFlow calls={batchTxs.map((item) => item.txData)} />)
},
[setTxFlow, batchTxs, closeSidebar],
)

return (
<Drawer variant="temporary" anchor="right" open={isOpen} onClose={closeSidebar}>
<aside className={css.aside}>
<Typography variant="h4" fontWeight={700} mb={1}>
Batched transactions
</Typography>

<Divider />

<div className={css.txs}>
{batchTxs.length
? batchTxs.map((item, index) => <BatchTxItem key={index} {...item} />)
: 'No transactions added yet'}
</div>

<Button onClick={onAddClick}>+ Add new transaction</Button>

<Divider />

<Button variant="contained" onClick={onConfirmClick}>
Confirm batch
</Button>
</aside>
</Drawer>
)
}

export default BatchSidebar
23 changes: 23 additions & 0 deletions src/components/common/BatchSidebar/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.aside {
margin-top: var(--header-height);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

.aside h4 {
width: 100%;
padding: var(--space-3) var(--space-3) 0;
}

.aside hr {
width: 100%;
margin: var(--space-3) 0;
}

.txs {
padding: 0 var(--space-3) var(--space-3);
min-width: 500px;
max-width: 600px;
}
14 changes: 13 additions & 1 deletion src/components/common/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import useChainId from '@/hooks/useChainId'
import SafeLogo from '@/public/images/logo.svg'
import Link from 'next/link'
import useSafeAddress from '@/hooks/useSafeAddress'
import BatchIndicator from '../BatchIndicator'

type HeaderProps = {
onMenuToggle?: Dispatch<SetStateAction<boolean>>
onBatchToggle?: Dispatch<SetStateAction<boolean>>
}

const Header = ({ onMenuToggle }: HeaderProps): ReactElement => {
const Header = ({ onMenuToggle, onBatchToggle }: HeaderProps): ReactElement => {
const chainId = useChainId()
const safeAddress = useSafeAddress()
const showSafeToken = safeAddress && !!getSafeTokenAddress(chainId)
Expand All @@ -36,6 +38,12 @@ const Header = ({ onMenuToggle }: HeaderProps): ReactElement => {
}
}

const handleBatchToggle = () => {
if (onBatchToggle) {
onBatchToggle((isOpen) => !isOpen)
}
}

return (
<Paper className={css.container}>
<div className={classnames(css.element, css.menuButton, !onMenuToggle ? css.hideSidebarMobile : null)}>
Expand All @@ -52,6 +60,10 @@ const Header = ({ onMenuToggle }: HeaderProps): ReactElement => {
</Link>
</div>

<div className={classnames(css.element, css.hideMobile)}>
<BatchIndicator onClick={handleBatchToggle} />
</div>

{showSafeToken && (
<div className={classnames(css.element, css.hideMobile)}>
<SafeTokenWidget />
Expand Down
6 changes: 5 additions & 1 deletion src/components/common/PageLayout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AppRoutes } from '@/config/routes'
import useDebounce from '@/hooks/useDebounce'
import { useRouter } from 'next/router'
import { TxModalContext } from '@/components/tx-flow'
import BatchSidebar from '../BatchSidebar'

const isNoSidebarRoute = (pathname: string): boolean => {
return [
Expand All @@ -30,6 +31,7 @@ const PageLayout = ({ pathname, children }: { pathname: string; children: ReactE
const router = useRouter()
const [noSidebar, setNoSidebar] = useState<boolean>(isNoSidebarRoute(pathname))
const [isSidebarOpen, setSidebarOpen] = useState<boolean>(true)
const [isBatchOpen, setBatchOpen] = useState<boolean>(false)
const hideSidebar = noSidebar || !isSidebarOpen
const { setFullWidth } = useContext(TxModalContext)
let isAnimated = useDebounce(!noSidebar, 300)
Expand All @@ -47,7 +49,7 @@ const PageLayout = ({ pathname, children }: { pathname: string; children: ReactE
return (
<>
<header className={css.header}>
<Header onMenuToggle={noSidebar ? undefined : setSidebarOpen} />
<Header onMenuToggle={noSidebar ? undefined : setSidebarOpen} onBatchToggle={setBatchOpen} />
</header>

{!noSidebar && <SideDrawer isOpen={isSidebarOpen} onToggle={setSidebarOpen} />}
Expand All @@ -62,6 +64,8 @@ const PageLayout = ({ pathname, children }: { pathname: string; children: ReactE
<SafeLoadingError>{children}</SafeLoadingError>
</div>

<BatchSidebar isOpen={isBatchOpen} onToggle={setBatchOpen} />

<Footer />
</div>
</>
Expand Down
32 changes: 32 additions & 0 deletions src/components/tx-flow/flows/ConfirmBatch/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { type ReactElement, useContext, useEffect } from 'react'
import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm'
import { createMultiSendCallOnlyTx } from '@/services/tx/tx-sender'
import { SafeTxContext } from '../../SafeTxProvider'
import type { MetaTransactionData } from '@safe-global/safe-core-sdk-types'
import TxLayout from '../../common/TxLayout'
import BatchIcon from '@/public/images/common/batch.svg'

const ConfirmBatch = ({ calls }: { calls: MetaTransactionData[] }): ReactElement => {
const { setSafeTx, setSafeTxError } = useContext(SafeTxContext)

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

return <SignOrExecuteForm onSubmit={() => {}} />
}

const ConfirmBatchFlow = ({ calls }: { calls: MetaTransactionData[] }) => {
return (
<TxLayout
title="Confirm batch"
subtitle={`This batch contains ${calls.length} transactions`}
icon={BatchIcon}
step={0}
>
<ConfirmBatch calls={calls} />
</TxLayout>
)
}

export default ConfirmBatchFlow
42 changes: 32 additions & 10 deletions src/components/tx/SignOrExecuteForm/SignForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type ReactElement, type SyntheticEvent, useContext, useState } from 'react'
import { Button, CardActions, Divider } from '@mui/material'
import { Box, Button, CardActions, Divider } from '@mui/material'

import ErrorMessage from '@/components/tx/ErrorMessage'
import { logError, Errors } from '@/services/exceptions'
Expand All @@ -13,6 +13,7 @@ import { asError } from '@/services/exceptions/utils'
import commonCss from '@/components/tx-flow/common/styles.module.css'
import { TxSecurityContext } from '../security/shared/TxSecurityContext'
import css from '@/components/tx/SignOrExecuteForm/styles.module.css'
import { useUpdateBatch } from '@/hooks/useDraftBatch'

const SignForm = ({
safeTx,
Expand All @@ -31,23 +32,26 @@ const SignForm = ({
const isOwner = useIsSafeOwner()
const { signTx } = useTxActions()
const { setTxFlow } = useContext(TxModalContext)
const updateBatch = useUpdateBatch()
const { needsRiskConfirmation, isRiskConfirmed, setIsRiskIgnored } = useContext(TxSecurityContext)
const isCreation = safeTx?.signatures.size === 0

// On modal submit
const handleSubmit = async (e: SyntheticEvent) => {
const handleSubmit = async (e: SyntheticEvent, isBatch = false) => {
e.preventDefault()

if (needsRiskConfirmation && !isRiskConfirmed) {
setIsRiskIgnored(true)
return
}

if (!safeTx) return

setIsSubmittable(false)
setSubmitError(undefined)

try {
await signTx(safeTx, txId, origin)
setTxFlow(undefined)
await (isBatch ? updateBatch(safeTx) : signTx(safeTx, txId, origin))
} catch (_err) {
const err = asError(_err)
logError(Errors._804, err)
Expand All @@ -56,9 +60,14 @@ const SignForm = ({
return
}

setTxFlow(undefined)
onSubmit()
}

const onBatchClick = (e: SyntheticEvent) => {
handleSubmit(e, true)
}

const cannotPropose = !isOwner
const submitDisabled = !safeTx || !isSubmittable || disableSubmit || cannotPropose

Expand All @@ -80,14 +89,27 @@ const SignForm = ({
<Divider className={commonCss.nestedDivider} sx={{ pt: 3 }} />

<CardActions>
{/* Submit button */}
<CheckWallet>
{(isOk) => (
<Button variant="contained" type="submit" disabled={!isOk || submitDisabled}>
Submit
<Box display="flex" gap={2}>
{/* Batch button */}
{isCreation && (
<Button variant="outlined" onClick={onBatchClick} disabled={submitDisabled}>
+ Add to batch
</Button>
)}
</CheckWallet>

<Box display="flex" flexDirection="column" justifyContent="center" color="border.main">
or
</Box>

{/* Submit button */}
<CheckWallet>
{(isOk) => (
<Button variant="contained" type="submit" disabled={!isOk || submitDisabled}>
Sign
</Button>
)}
</CheckWallet>
</Box>
</CardActions>
</form>
)
Expand Down
4 changes: 2 additions & 2 deletions src/components/tx/SignOrExecuteForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ export type SignOrExecuteProps = {
const SignOrExecuteForm = (props: SignOrExecuteProps): ReactElement => {
const { transactionExecution } = useAppSelector(selectSettings)
const [shouldExecute, setShouldExecute] = useState<boolean>(transactionExecution)
const isCreation = !props.txId
const isNewExecutableTx = useImmediatelyExecutable() && isCreation
const { safeTx, safeTxError } = useContext(SafeTxContext)
const isCreation = safeTx?.signatures.size === 0
const isNewExecutableTx = useImmediatelyExecutable() && isCreation
const isCorrectNonce = useValidateNonce(safeTx)

// If checkbox is checked and the transaction is executable, execute it, otherwise sign it
Expand Down
36 changes: 36 additions & 0 deletions src/hooks/useDraftBatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useCallback } from 'react'
import type { MetaTransactionData, SafeTransaction } from '@safe-global/safe-core-sdk-types'
import { useAppDispatch, useAppSelector } from '@/store'
import useChainId from './useChainId'
import useSafeAddress from './useSafeAddress'
import type { DraftBatchItem } from '@/store/batchSlice'
import { selectBatchBySafe, addTx } from '@/store/batchSlice'

const getMetaData = (safeTx: SafeTransaction): MetaTransactionData => {
return {
to: safeTx.data.to,
value: safeTx.data.value,
data: safeTx.data.data,
operation: safeTx.data.operation,
}
}

export const useUpdateBatch = () => {
const chainId = useChainId()
const safeAddress = useSafeAddress()
const dispatch = useAppDispatch()

return useCallback(
(safeTx: SafeTransaction) => {
dispatch(addTx({ chainId, safeAddress, txData: getMetaData(safeTx), timestamp: Date.now() }))
},
[dispatch, chainId, safeAddress],
)
}

export const useDraftBatch = (): DraftBatchItem[] => {
const chainId = useChainId()
const safeAddress = useSafeAddress()
const batch = useAppSelector((state) => selectBatchBySafe(state, chainId, safeAddress))
return batch
}
Loading

0 comments on commit 069a360

Please sign in to comment.