(Feature) Proper rejections (#2096)

* remove `isCancelTransaction` utility function in favor of `txInfo.isCancellation` flag provided by client-gateway

* replace "cancel" concept in favor of "reject"

* add circle-cross-red icon to "On-chain rejection" transaction info

Adjust owner's list text color

* identify queued on-chain rejection

* apply styles to on-chain rejection type identifier

* update awaiting messages wording

* fix styles on styles to on-chain rejection

* replace local svg with SRC `Icon` component wherever is possible
This commit is contained in:
Fernando 2021-04-07 07:40:33 -03:00 committed by GitHub
parent e48891ba2b
commit c96e3192ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 134 additions and 106 deletions

View File

@ -161,7 +161,7 @@
"@gnosis.pm/safe-apps-sdk": "1.0.3",
"@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2",
"@gnosis.pm/safe-contracts": "1.1.1-dev.2",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#a68a67e",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#7ebc414",
"@gnosis.pm/util-contracts": "2.0.6",
"@ledgerhq/hw-transport-node-hid-singleton": "5.45.0",
"@material-ui/core": "^4.11.0",

View File

@ -1,5 +1,5 @@
import { Text } from '@gnosis.pm/safe-react-components'
import React from 'react'
import React, { ReactElement } from 'react'
import styled from 'styled-components'
const Wrapper = styled.div`
@ -12,11 +12,9 @@ const Icon = styled.img`
margin-right: 9px;
`
const CustomIconText = ({ iconUrl, text }: { iconUrl: string; text?: string }) => (
export const CustomIconText = ({ iconUrl, text }: { iconUrl: string; text?: string }): ReactElement => (
<Wrapper>
<Icon alt={text} src={iconUrl} />
{text && <Text size="xl">{text}</Text>}
</Wrapper>
)
export default CustomIconText

View File

@ -41,7 +41,7 @@ export const TransactionFailText = ({
if (isExecution) {
errorMessage =
threshold && threshold > 1
? `To save gas costs, cancel this transaction`
? `To save gas costs, reject this transaction`
: `To save gas costs, avoid executing the transaction.`
}

View File

@ -236,7 +236,7 @@ type MultiSigExecutionDetails = {
type DetailedExecutionInfo = ModuleExecutionDetails | MultiSigExecutionDetails
type ExpandedTxDetails = {
executedAt: number
executedAt: number | null
txStatus: TransactionStatus
txInfo: TransactionInfo
txData: TransactionData | null

View File

@ -4,7 +4,7 @@ import CircularProgress from '@material-ui/core/CircularProgress'
import React, { ReactElement, useContext, useRef } from 'react'
import styled from 'styled-components'
import CustomIconText from 'src/components/CustomIconText'
import { CustomIconText } from 'src/components/CustomIconText'
import {
isCustomTxInfo,
isMultiSendTxInfo,
@ -24,6 +24,7 @@ import { TokenTransferAmount } from './TokenTransferAmount'
import { TxsInfiniteScrollContext } from './TxsInfiniteScroll'
import { TxLocationContext } from './TxLocationProvider'
import { CalculatedVotes } from './TxQueueCollapsed'
import { isCancelTxDetails } from './utils'
const TxInfo = ({ info }: { info: AssetInfo }) => {
if (isTokenTransferAsset(info)) {
@ -116,6 +117,8 @@ export const TxCollapsed = ({
const { ref, lastItemId } = useContext(TxsInfiniteScrollContext)
const willBeReplaced = transaction?.txStatus === 'WILL_BE_REPLACED' ? ' will-be-replaced' : ''
const onChainRejection =
isCancelTxDetails(transaction.txInfo) && txLocation !== 'history' ? ' on-chain-rejection' : ''
const txCollapsedNonce = (
<div className={'tx-nonce' + willBeReplaced}>
@ -124,7 +127,7 @@ export const TxCollapsed = ({
)
const txCollapsedType = (
<div className={'tx-type' + willBeReplaced}>
<div className={'tx-type' + willBeReplaced + onChainRejection}>
<CustomIconText iconUrl={type.icon} text={type.text} />
</div>
)

View File

@ -46,7 +46,7 @@ export const TxCollapsedActions = ({ transaction }: TxCollapsedActionsProps): Re
</span>
</Tooltip>
{canCancel && (
<Tooltip title="Cancel" placement="top">
<Tooltip title="Reject" placement="top">
<span>
<IconButton size="small" type="button" onClick={handleCancelButtonClick} disabled={isPending}>
<Icon type="circleCross" color="error" size="sm" />

View File

@ -1,7 +1,6 @@
import { Icon, Link, Loader, Text } from '@gnosis.pm/safe-react-components'
import cn from 'classnames'
import React, { ReactElement, useContext } from 'react'
import { useSelector } from 'react-redux'
import styled from 'styled-components'
import {
@ -12,7 +11,6 @@ import {
MultiSigExecutionDetails,
Transaction,
} from 'src/logic/safe/store/models/types/gateway.d'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { TransactionActions } from './hooks/useTransactionActions'
import { useTransactionDetails } from './hooks/useTransactionDetails'
import { TxDetailsContainer, Centered, AlignItemsWithMargin } from './styled'
@ -30,34 +28,44 @@ const NormalBreakingText = styled(Text)`
`
const TxDataGroup = ({ txDetails }: { txDetails: ExpandedTxDetails }): ReactElement | null => {
const safeAddress = useSelector(safeParamAddressFromStateSelector)
if (isTransferTxInfo(txDetails.txInfo) || isSettingsChangeTxInfo(txDetails.txInfo)) {
return <TxInfo txInfo={txDetails.txInfo} />
}
if (isCancelTxDetails({ executedAt: txDetails.executedAt, txInfo: txDetails.txInfo, safeAddress })) {
if (isCancelTxDetails(txDetails.txInfo)) {
const txNonce = `${(txDetails.detailedExecutionInfo as MultiSigExecutionDetails).nonce ?? NOT_AVAILABLE}`
const isTxExecuted = txDetails.executedAt
// executed rejection transaction
let message = `This is an on-chain rejection that didn't send any funds.
This on-chain rejection replaced all transactions with nonce ${txNonce}.`
if (!isTxExecuted) {
// queued rejection transaction
message = `This is an on-chain rejection that doesn't send any funds.
Executing this on-chain rejection will replace all currently awaiting transactions with nonce ${txNonce}.`
}
return (
<>
<NormalBreakingText size="xl">
{`This is an empty cancelling transaction that doesn't send any funds.
Executing this transaction will replace all currently awaiting transactions with nonce ${
(txDetails.detailedExecutionInfo as MultiSigExecutionDetails).nonce ?? NOT_AVAILABLE
}.`}
</NormalBreakingText>
<Link
href="https://help.gnosis-safe.io/en/articles/4738501-why-do-i-need-to-pay-for-cancelling-a-transaction"
target="_blank"
rel="noreferrer"
title="Why do I need to pay for cancelling a transaction?"
>
<AlignItemsWithMargin>
<Text size="xl" as="span" color="primary">
Why do I need to pay for cancelling a transaction?
</Text>
<Icon size="sm" type="externalLink" color="primary" />
</AlignItemsWithMargin>
</Link>
<NormalBreakingText size="xl">{message}</NormalBreakingText>
{!isTxExecuted && (
<>
<br />
<Link
href="https://help.gnosis-safe.io/en/articles/4738501-why-do-i-need-to-pay-for-cancelling-a-transaction"
target="_blank"
rel="noreferrer"
title="Why do I need to pay for rejecting a transaction?"
>
<AlignItemsWithMargin>
<Text size="xl" as="span" color="primary">
Why do I need to pay for rejecting a transaction?
</Text>
<Icon size="sm" type="externalLink" color="primary" />
</AlignItemsWithMargin>
</Link>
</>
)}
</>
)
}
@ -116,7 +124,7 @@ export const TxDetails = ({ transaction, actions }: TxDetailsProps): ReactElemen
'will-be-replaced': transaction.txStatus === 'WILL_BE_REPLACED',
})}
>
<TxOwners detailedExecutionInfo={data.detailedExecutionInfo} />
<TxOwners txDetails={data} />
</div>
{!data.executedAt && txLocation !== 'history' && actions?.isUserAnOwner && (
<div className={cn('tx-details-actions', { 'will-be-replaced': transaction.txStatus === 'WILL_BE_REPLACED' })}>

View File

@ -41,7 +41,7 @@ export const TxExpandedActions = ({ transaction }: TxExpandedActionsProps): Reac
</Button>
{canCancel && (
<Button size="md" color="error" onClick={handleCancelButtonClick} className="error" disabled={isPending}>
Cancel
Reject
</Button>
)}
</>

View File

@ -1,4 +1,4 @@
import { Text } from '@gnosis.pm/safe-react-components'
import { Text, Icon } from '@gnosis.pm/safe-react-components'
import React, { ReactElement } from 'react'
import styled from 'styled-components'
@ -6,43 +6,55 @@ import Img from 'src/components/layout/Img'
import { ExpandedTxDetails, isModuleExecutionDetails } from 'src/logic/safe/store/models/types/gateway.d'
import TransactionListActive from './assets/transactions-list-active.svg'
import TransactionListInactive from './assets/transactions-list-inactive.svg'
import CheckCircleGreen from './assets/check-circle-green.svg'
import PlusCircleGreen from './assets/plus-circle-green.svg'
import { OwnerRow } from './OwnerRow'
import { OwnerList, OwnerListItem } from './styled'
type TxOwnersProps = {
detailedExecutionInfo: ExpandedTxDetails['detailedExecutionInfo']
}
import { isCancelTxDetails } from './utils'
const StyledImg = styled(Img)`
background-color: transparent;
border-radius: 50%;
`
export const TxOwners = ({ detailedExecutionInfo }: TxOwnersProps): ReactElement | null => {
export const TxOwners = ({ txDetails }: { txDetails: ExpandedTxDetails }): ReactElement | null => {
const { txInfo, detailedExecutionInfo } = txDetails
if (!detailedExecutionInfo || isModuleExecutionDetails(detailedExecutionInfo)) {
return null
}
const confirmationsNeeded = detailedExecutionInfo.confirmationsRequired - detailedExecutionInfo.confirmations.length
const CreationNode = isCancelTxDetails(txInfo) ? (
<OwnerListItem>
<span className="icon">
<Icon size="sm" type="circleCross" color="error" />
</span>
<div className="legend">
<Text color="error" size="xl" strong>
On-chain rejection created
</Text>
</div>
</OwnerListItem>
) : (
<OwnerListItem>
<span className="icon">
<Icon size="sm" type="add" color="primary" />
</span>
<div className="legend">
<Text color="primary" size="xl" strong>
Created
</Text>
</div>
</OwnerListItem>
)
return (
<OwnerList>
<OwnerListItem>
<span className="icon">
<StyledImg alt="" src={PlusCircleGreen} />
</span>
<div className="legend">
<Text color="primary" size="xl" strong>
Created
</Text>
</div>
</OwnerListItem>
{CreationNode}
{detailedExecutionInfo.confirmations.map(({ signer }) => (
<OwnerListItem key={signer}>
<span className="icon">
<StyledImg alt="" src={CheckCircleGreen} />
<Icon size="sm" type="circleCheck" color="primary" />
</span>
<div className="legend">
<Text color="primary" size="xl" strong>
@ -55,7 +67,11 @@ export const TxOwners = ({ detailedExecutionInfo }: TxOwnersProps): ReactElement
{confirmationsNeeded <= 0 ? (
<OwnerListItem>
<span className="icon">
<StyledImg alt="" src={detailedExecutionInfo.executor ? CheckCircleGreen : TransactionListActive} />
{detailedExecutionInfo.executor ? (
<Icon type="circleCheck" size="sm" color="primary" />
) : (
<StyledImg alt="" src={TransactionListActive} />
)}
</span>
<div className="legend">
<Text color="primary" size="xl" strong>

View File

@ -27,7 +27,7 @@ export const TxSummary = ({ txDetails }: { txDetails: ExpandedTxDetails }): Reac
</Text>
)}
</div>
{nonce && (
{nonce !== undefined && (
<div className="tx-nonce">
<Text size="xl" strong as="span">
Nonce:{' '}

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<g fill="none" fill-rule="evenodd">
<g>
<g>
<path d="M0 0H16V16H0z" transform="translate(-273 -201) translate(273 201)"/>
<path fill="#F02525" fill-rule="nonzero" d="M8 15c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm0-2c-2.761 0-5-2.239-5-5s2.239-5 5-5 5 2.239 5 5-2.239 5-5 5z" transform="translate(-273 -201) translate(273 201)"/>
<path fill="#F02525" d="M9.414 8l1.414 1.414c.391.39.391 1.024 0 1.414-.39.391-1.023.391-1.414 0L8 9.414l-1.414 1.414c-.39.391-1.024.391-1.414 0-.391-.39-.391-1.023 0-1.414L6.586 8 5.172 6.586c-.391-.39-.391-1.024 0-1.414.39-.391 1.023-.391 1.414 0L8 6.586l1.414-1.414c.39-.391 1.024-.391 1.414 0 .391.39.391 1.023 0 1.414L9.414 8z" transform="translate(-273 -201) translate(273 201)"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 936 B

View File

@ -7,7 +7,6 @@ import { getQueuedTransactionsByNonce } from 'src/logic/safe/store/selectors/gat
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
import { TxLocationContext } from 'src/routes/safe/components/Transactions/TxList/TxLocationProvider'
import { isCancelTransaction } from 'src/routes/safe/components/Transactions/TxList/utils'
import { grantedSelector } from 'src/routes/safe/container/selector'
import { AppReduxState } from 'src/store'
@ -60,14 +59,7 @@ export const useTransactionActions = (transaction: Transaction): TransactionActi
canConfirm,
canConfirmThenExecute: txLocation === 'queued.next' && canConfirm && oneToGo,
canExecute: txLocation === 'queued.next' && thresholdReached,
canCancel: !transactionsByNonce.some(
({ txInfo }) =>
isCustomTxInfo(txInfo) &&
isCancelTransaction({
txInfo,
safeAddress,
}),
),
canCancel: !transactionsByNonce.some(({ txInfo }) => isCustomTxInfo(txInfo) && txInfo.isCancellation),
isUserAnOwner,
oneToGo,
})

View File

@ -37,10 +37,10 @@ export const useTransactionStatus = (transaction: Transaction): TransactionStatu
switch (transaction.txStatus) {
case 'AWAITING_CONFIRMATIONS':
text = signaturePending(currentUser) ? 'Awaiting your confirmation' : 'Awaiting confirmations'
text = signaturePending(currentUser) ? 'Needs your confirmation' : 'Needs confirmations'
break
case 'AWAITING_EXECUTION':
text = 'Awaiting execution'
text = 'Needs execution'
break
case 'PENDING':
case 'PENDING_FAILED':

View File

@ -4,10 +4,10 @@ import { useSelector } from 'react-redux'
import { Transaction } from 'src/logic/safe/store/models/types/gateway.d'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import CustomTxIcon from 'src/routes/safe/components/Transactions/TxList/assets/custom.svg'
import CircleCrossRed from 'src/routes/safe/components/Transactions/TxList/assets/circle-cross-red.svg'
import IncomingTxIcon from 'src/routes/safe/components/Transactions/TxList/assets/incoming.svg'
import OutgoingTxIcon from 'src/routes/safe/components/Transactions/TxList/assets/outgoing.svg'
import SettingsTxIcon from 'src/routes/safe/components/Transactions/TxList/assets/settings.svg'
import { isCancelTransaction } from 'src/routes/safe/components/Transactions/TxList/utils'
export type TxTypeProps = {
icon: string
@ -40,20 +40,8 @@ export const useTransactionType = (tx: Transaction): TxTypeProps => {
break
}
// TODO: isCancel
// there are two 'cancelling' tx identification
// this one is the candidate to remain when the client gateway implements
// https://github.com/gnosis/safe-client-gateway/issues/255
if (typeof tx.txInfo.isCancellation === 'boolean' && tx.txInfo.isCancellation) {
setType({ icon: CustomTxIcon, text: 'Cancelling transaction' })
break
}
// TODO: isCancel
// remove the following condition when issue#255 is implemented
// also remove `isCancelTransaction` function
if (isCancelTransaction({ txInfo: tx.txInfo, safeAddress })) {
setType({ icon: CustomTxIcon, text: 'Cancelling transaction' })
if (tx.txInfo.isCancellation) {
setType({ icon: CircleCrossRed, text: 'On-chain rejection' })
break
}

View File

@ -163,6 +163,32 @@ const failedTransaction = css`
}
`
const onChainRejection = css`
&.on-chain-rejection {
background-color: ${({ theme }) => theme.colors.errorTooltip};
border-left: 4px solid ${({ theme }) => theme.colors.error};
border-radius: 4px;
padding-left: 7px;
height: 22px;
max-width: 165px;
> div {
height: 17px;
align-items: center;
padding-top: 3px;
}
p {
font-size: 11px;
line-height: 16px;
letter-spacing: 1px;
font-weight: bold;
text-transform: uppercase;
margin-left: -2px;
}
}
`
export const StyledTransaction = styled.div`
${willBeReplaced};
${failedTransaction};
@ -175,6 +201,10 @@ export const StyledTransaction = styled.div`
align-self: center;
}
.tx-type {
${onChainRejection};
}
.tx-votes {
justify-self: center;
}

View File

@ -2,7 +2,6 @@ import { BigNumber } from 'bignumber.js'
import { getNetworkInfo } from 'src/config'
import {
Custom,
isCustomTxInfo,
isTransferTxInfo,
Transaction,
@ -12,7 +11,6 @@ import {
import { formatAmount } from 'src/logic/tokens/utils/formatAmount'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { sameString } from 'src/utils/strings'
export const NOT_AVAILABLE = 'n/a'
@ -90,27 +88,11 @@ export const getTxTokenData = (txInfo: Transfer): txTokenData => {
}
}
// TODO: isCancel
// how can we be sure that it's a cancel tx without asking for tx-details?
// can the client-gateway service provide info about the tx, Like: `isCancelTransaction: boolean`?
// it will be solved as part of: https://github.com/gnosis/safe-client-gateway/issues/255
export const isCancelTransaction = ({ txInfo, safeAddress }: { txInfo: Custom; safeAddress: string }): boolean =>
sameAddress(txInfo.to, safeAddress) &&
sameString(txInfo.dataSize, '0') &&
sameString(txInfo.value, '0') &&
txInfo.methodName === null
type IsCancelTxDetailsProps = {
executedAt: number | null
txInfo: Transaction['txInfo']
safeAddress: string
}
export const isCancelTxDetails = ({ executedAt, txInfo, safeAddress }: IsCancelTxDetailsProps): boolean =>
!executedAt &&
export const isCancelTxDetails = (txInfo: Transaction['txInfo']): boolean =>
// custom transaction
isCustomTxInfo(txInfo) &&
// verify that it's a cancel tx based on it's info
isCancelTransaction({ safeAddress, txInfo })
// flag-based identification
txInfo.isCancellation
export const addressInList = (list: string[] = []) => (address: string): boolean =>
list.some((ownerAddress) => sameAddress(ownerAddress, address))

View File

@ -1596,9 +1596,9 @@
solc "0.5.14"
truffle "^5.1.21"
"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#a68a67e":
"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#7ebc414":
version "0.5.0"
resolved "https://github.com/gnosis/safe-react-components.git#a68a67e634d0be091856ebba9f6874eebb767cd7"
resolved "https://github.com/gnosis/safe-react-components.git#7ebc414ae975d60846704c5a8db5e61c30348d12"
dependencies:
classnames "^2.2.6"
react-media "^1.10.0"