Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add batch execute button and tx highlighting #352

Merged
merged 11 commits into from
Aug 16, 2022
24 changes: 24 additions & 0 deletions src/components/common/CustomTooltip/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { styled } from '@mui/material/styles'
import Tooltip, { tooltipClasses, TooltipProps } from '@mui/material/Tooltip'

const CustomTooltip = styled(({ className, ...props }: TooltipProps) => (
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only extracted this so it can be reused

<Tooltip {...props} classes={{ popper: className }} />
))(({ theme }) => ({
[`& .${tooltipClasses.tooltip}`]: {
color: theme.palette.common.black,
backgroundColor: theme.palette.common.white,
borderRadius: '8px',
boxShadow: '1px 2px 10px rgba(40, 54, 61, 0.18)',
fontSize: '14px',
padding: '16px',
lineHeight: 'normal',
},
[`& .${tooltipClasses.arrow}`]: {
'&::before': {
backgroundColor: theme.palette.common.white,
boxShadow: '1px 2px 10px rgba(40, 54, 61, 0.18)',
},
},
}))

export default CustomTooltip
2 changes: 1 addition & 1 deletion src/components/common/PaginatedTxns/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const PaginatedTxns = ({ useTxns }: { useTxns: typeof useTxHistory | typeof useT
) : null

return (
<Box mb={3}>
<Box mb={3} position="relative">
{loading ? (
<CircularProgress size={40} sx={{ marginTop: 2 }} />
) : error ? (
Expand Down
2 changes: 1 addition & 1 deletion src/components/create-safe/status/useSafeCreation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe('useSafeCreation', () => {
owners: [],
saltNonce: 123,
chainId: '4',
safeAddress: '0x10',
address: '0x10',
txHash: '0x123',
},
jest.fn,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createContext, ReactElement, ReactNode, useState } from 'react'

export const BatchExecuteHoverContext = createContext<{
activeHover?: string[]
setActiveHover: (activeHover?: string[]) => void
}>({
activeHover: undefined,
setActiveHover: () => {},
})

// Used for highlighting transactions that will be included when executing them as a batch
export const BatchExecuteHoverProvider = ({ children }: { children: ReactNode }): ReactElement => {
const [activeHover, setActiveHover] = useState<string[]>()

return (
<BatchExecuteHoverContext.Provider value={{ activeHover, setActiveHover }}>
{children}
</BatchExecuteHoverContext.Provider>
)
}
95 changes: 95 additions & 0 deletions src/components/transactions/BatchExecuteButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useCallback, useContext, useMemo } from 'react'
import { Button } from '@mui/material'
import css from './styles.module.css'
import { BatchExecuteHoverContext } from '@/components/transactions/BatchExecuteButton/BatchExecuteHoverProvider'
import { Transaction, TransactionListItem } from '@gnosis.pm/safe-react-gateway-sdk'
import useSafeInfo from '@/hooks/useSafeInfo'
import { isMultisigExecutionInfo, isTransactionListItem } from '@/utils/transaction-guards'
import { useAppSelector } from '@/store'
import { selectPendingTxs } from '@/store/pendingTxsSlice'
import CustomTooltip from '@/components/common/CustomTooltip'

const BATCH_LIMIT = 10
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it was 10 txs before. But I'm not 100% sure where the 10 is coming from. Is it about the block gas limit?
If yes: Some apps like tx builder / csv airdrop can by itself create huge txs. 10 of those would not fit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I remember we chose 10 arbitrarily. By default we get 20 per page now I think so we could increase it to that

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually now infinite scroll makes more sense. 🤔


const getBatchableTransactions = (items: Transaction[][], nonce: number) => {
const batchableTransactions: Transaction[] = []
let currentNonce = nonce

items.forEach((txs) => {
const sorted = txs.slice().sort((a, b) => b.transaction.timestamp - a.transaction.timestamp)
sorted.forEach((tx) => {
if (
batchableTransactions.length < BATCH_LIMIT &&
isMultisigExecutionInfo(tx.transaction.executionInfo) &&
tx.transaction.executionInfo.nonce === currentNonce &&
tx.transaction.executionInfo.confirmationsSubmitted >= tx.transaction.executionInfo.confirmationsRequired
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know its just a minor bug but lets add a condition here that we stop adding txs to the batch if an update safe tx was added.
5afe/safe-react#4014

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed lets tackle this in a separate task as it involves decoding each tx which could blow up the scope

) {
batchableTransactions.push(tx)
currentNonce = tx.transaction.executionInfo.nonce + 1
}
iamacook marked this conversation as resolved.
Show resolved Hide resolved
})
})

return batchableTransactions
}

const BatchExecuteButton = ({ items }: { items: (TransactionListItem | Transaction[])[] }) => {
const pendingTxs = useAppSelector(selectPendingTxs)
const hoverContext = useContext(BatchExecuteHoverContext)
const { safe } = useSafeInfo()

const currentNonce = safe.nonce

const groupedTransactions = useMemo(
() =>
items
.map((item) => {
if (Array.isArray(item)) return item
if (isTransactionListItem(item)) return [item]
})
.filter((item) => item !== undefined) as Transaction[][],
[items],
)

const batchableTransactions = useMemo(
() => getBatchableTransactions(groupedTransactions, currentNonce),
[currentNonce, groupedTransactions],
)

const isBatchable = batchableTransactions.length > 1
const hasPendingTx = batchableTransactions.some((tx) => pendingTxs[tx.transaction.id])
const isDisabled = !isBatchable || hasPendingTx

const handleOnMouseEnter = useCallback(() => {
hoverContext.setActiveHover(batchableTransactions.map((tx) => tx.transaction.id))
}, [batchableTransactions, hoverContext])

const handleOnMouseLeave = useCallback(() => {
hoverContext.setActiveHover()
}, [hoverContext])

return (
<CustomTooltip
placement="top-start"
arrow
title={
isDisabled
? 'Batch execution is only available for transactions that have been fully signed and are strictly sequential in Safe Nonce.'
iamacook marked this conversation as resolved.
Show resolved Hide resolved
: 'All transactions highlighted in light green will be included in the batch execution.'
}
>
<Button
onMouseEnter={handleOnMouseEnter}
onMouseLeave={handleOnMouseLeave}
className={css.button}
variant="contained"
size="small"
disabled={isDisabled}
>
Execute Batch {isBatchable && ` (${batchableTransactions.length})`}
</Button>
</CustomTooltip>
)
}

export default BatchExecuteButton
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.button {
position: absolute;
right: 0;
top: -50px;
}
28 changes: 20 additions & 8 deletions src/components/transactions/TxList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
} from '@/utils/transaction-guards'
import GroupedTxListItems from '@/components/transactions/GroupedTxListItems'
import css from './styles.module.css'
import BatchExecuteButton from '@/components/transactions/BatchExecuteButton'
import { BatchExecuteHoverProvider } from '@/components/transactions/BatchExecuteButton/BatchExecuteHoverProvider'
import { useRouter } from 'next/router'
import { AppRoutes } from '@/config/routes'

type TxListProps = {
items: TransactionListPage['results']
Expand All @@ -24,6 +28,7 @@ export const TxListGrid = ({ children }: { children: (ReactElement | null)[] }):
}

const TxList = ({ items }: TxListProps): ReactElement => {
const router = useRouter()
// Ensure list always starts with a date label
const list = useMemo(() => {
const firstDateLabelIndex = items.findIndex(isDateLabel)
Expand Down Expand Up @@ -59,16 +64,23 @@ const TxList = ({ items }: TxListProps): ReactElement => {
}, [])
}, [list])

const isQueue = router.pathname === AppRoutes.safe.transactions.queue

return (
<TxListGrid>
{listWithGroupedItems.map((item, index) => {
if (Array.isArray(item)) {
return <GroupedTxListItems key={index} groupedListItems={item} />
}
<>
<BatchExecuteHoverProvider>
{isQueue && <BatchExecuteButton items={listWithGroupedItems} />}
<TxListGrid>
{listWithGroupedItems.map((item, index) => {
if (Array.isArray(item)) {
return <GroupedTxListItems key={index} groupedListItems={item} />
}

return <TxListItem key={index} item={item} />
})}
</TxListGrid>
return <TxListItem key={index} item={item} />
})}
</TxListGrid>
</BatchExecuteHoverProvider>
</>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import TxSummary from '@/components/transactions/TxSummary'
import TxDetails from '@/components/transactions/TxDetails'
import CreateTxInfo from '@/components/transactions/SafeCreationTx'
import { isCreationTxInfo } from '@/utils/transaction-guards'
import { useContext } from 'react'
import { BatchExecuteHoverContext } from '@/components/transactions/BatchExecuteButton/BatchExecuteHoverProvider'
import css from './styles.module.css'

interface ExpandableTransactionItemProps {
isGrouped?: boolean
Expand All @@ -13,6 +16,9 @@ interface ExpandableTransactionItemProps {
}

export const ExpandableTransactionItem = ({ isGrouped = false, item, txDetails }: ExpandableTransactionItemProps) => {
const hoverContext = useContext(BatchExecuteHoverContext)
const isActive = hoverContext.activeHover?.includes(item.transaction.id)
iamacook marked this conversation as resolved.
Show resolved Hide resolved

return (
<Accordion
disableGutters
Expand All @@ -22,6 +28,7 @@ export const ExpandableTransactionItem = ({ isGrouped = false, item, txDetails }
}}
elevation={0}
defaultExpanded={!!txDetails}
className={isActive ? css.active : undefined}
>
<AccordionSummary expandIcon={<ExpandMoreIcon />} sx={{ justifyContent: 'flex-start', overflowX: 'auto' }}>
<TxSummary item={item} isGrouped={isGrouped} />
Expand Down
7 changes: 7 additions & 0 deletions src/components/transactions/TxListItem/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.active {
border-color: var(--color-primary-light);
}

.active :global .MuiAccordionSummary-root {
background-color: var(--color-primary-background);
}
25 changes: 3 additions & 22 deletions src/components/transactions/Warning/index.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,12 @@
import { ReactElement } from 'react'
import { Alert, Link } from '@mui/material'
import Tooltip, { tooltipClasses, type TooltipProps } from '@mui/material/Tooltip'
import { styled } from '@mui/material/styles'
import { tooltipClasses } from '@mui/material/Tooltip'
import css from './styles.module.css'
import CustomTooltip from '@/components/common/CustomTooltip'

const UNEXPECTED_DELEGATE_ARTICLE =
'https://help.gnosis-safe.io/en/articles/6302452-why-do-i-see-an-unexpected-delegate-call-warning-in-my-transaction'

const CustomTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} />
))(({ theme }) => ({
[`& .${tooltipClasses.tooltip}`]: {
color: theme.palette.common.black,
backgroundColor: theme.palette.common.white,
borderRadius: '8px',
boxShadow: '1px 2px 10px rgba(40, 54, 61, 0.18)',
fontSize: '14px',
padding: '16px',
lineHeight: 'normal',
},
[`& .${tooltipClasses.arrow}`]: {
'&::before': {
backgroundColor: theme.palette.common.white,
boxShadow: '1px 2px 10px rgba(40, 54, 61, 0.18)',
},
},
}))

export const DelegateCallWarning = ({ showWarning }: { showWarning: boolean }): ReactElement => (
<CustomTooltip
sx={{
Expand Down Expand Up @@ -55,6 +35,7 @@ export const DelegateCallWarning = ({ showWarning }: { showWarning: boolean }):
sx={({ palette }) => ({
color: showWarning ? palette.warning.dark : palette.success.main,
backgroundColor: `${showWarning ? palette.warning.light : palette.success.background}`,
border: 0,
borderLeft: `3px solid ${showWarning ? palette.warning.dark : palette.success.main}`,

'&.MuiAlert-standardInfo .MuiAlert-icon': {
Expand Down