-
- Factory:{' '}
-
- {txInfo.factory ? (
-
- ) : (
-
- {NOT_AVAILABLE}
-
- )}
+
+
+ {generateCreatorTxData('Creator', txInfo.creator, useKnownAddress(txInfo.creator))}
+ {generateCreatorTxData('Factory', txInfo.factory, useKnownAddress(txInfo.factory))}
+ {generateCreatorTxData('Mastercopy', txInfo.implementation, useKnownAddress(txInfo.implementation))}
-
-
- Mastercopy:{' '}
-
- {txInfo.implementation ? (
-
- ) : (
-
- {NOT_AVAILABLE}
-
- )}
+
+
+
-
)
}
diff --git a/src/routes/safe/components/Transactions/TxList/TxInfoDetails.tsx b/src/routes/safe/components/Transactions/TxList/TxInfoDetails.tsx
index be79bdadfe..52c8f25667 100644
--- a/src/routes/safe/components/Transactions/TxList/TxInfoDetails.tsx
+++ b/src/routes/safe/components/Transactions/TxList/TxInfoDetails.tsx
@@ -17,7 +17,7 @@ const SingleRow = styled.div`
`
type TxInfoDetailsProps = {
- title: string
+ title: string | ReactElement
address: string
name?: string | undefined
avatarUrl?: string | undefined
diff --git a/src/routes/safe/components/Transactions/TxList/TxInfoMultiSend.tsx b/src/routes/safe/components/Transactions/TxList/TxInfoMultiSend.tsx
index 68972d5fbe..2913122da6 100644
--- a/src/routes/safe/components/Transactions/TxList/TxInfoMultiSend.tsx
+++ b/src/routes/safe/components/Transactions/TxList/TxInfoMultiSend.tsx
@@ -3,6 +3,7 @@ import { MultiSend } from '@gnosis.pm/safe-react-gateway-sdk'
import PrefixedEthHashInfo from 'src/components/PrefixedEthHashInfo'
import { getExplorerInfo } from 'src/config'
import { InfoDetails } from './InfoDetails'
+import { TxDataRow } from './TxDataRow'
// Does not use AddressInfo as to not allow address book data display
// as we use backend data to verify the deligate call
@@ -10,6 +11,7 @@ const TxInfoMultiSend = ({ txInfo }: { txInfo: MultiSend }): ReactElement => {
const hash = txInfo?.to.value
const name = txInfo.to?.name || undefined
const customAvatar = txInfo.to?.logoUri || undefined
+ const value = txInfo?.value
return (
{
showCopyBtn
explorerUrl={getExplorerInfo(hash)}
/>
+
)
}
diff --git a/src/routes/safe/components/Transactions/TxList/TxInfoTransfer.tsx b/src/routes/safe/components/Transactions/TxList/TxInfoTransfer.tsx
index 6f9083312b..e93519f5de 100644
--- a/src/routes/safe/components/Transactions/TxList/TxInfoTransfer.tsx
+++ b/src/routes/safe/components/Transactions/TxList/TxInfoTransfer.tsx
@@ -1,36 +1,51 @@
-import { Transfer } from '@gnosis.pm/safe-react-gateway-sdk'
+import { TransactionStatus, Transfer, TransferDirection } from '@gnosis.pm/safe-react-gateway-sdk'
import { ReactElement, useEffect, useState } from 'react'
+import { Text } from '@gnosis.pm/safe-react-components'
-import { useAssetInfo } from './hooks/useAssetInfo'
+import { AssetInfo, TokenTransferAsset, useAssetInfo } from './hooks/useAssetInfo'
import { TxInfoDetails } from './TxInfoDetails'
+import { isTxQueued } from 'src/logic/safe/store/models/types/gateway.d'
+
+export const isTransferAssetInfo = (value?: AssetInfo): value is TokenTransferAsset => {
+ return value?.type === 'Transfer'
+}
+
+const makeTitle = (txDirection: string, amountWithSymbol: string, txStatus: TransactionStatus) => (
+
+ {txDirection === TransferDirection.INCOMING ? 'Received' : isTxQueued(txStatus) ? 'Send' : 'Sent'}{' '}
+
+ {amountWithSymbol}
+
+ {txDirection === TransferDirection.INCOMING ? ' from:' : ' to:'}
+
+)
type Details = {
- title: string
+ title: string | ReactElement
address: string
name: string | undefined // AddressEx returns null if unknown
}
-export const TxInfoTransfer = ({ txInfo }: { txInfo: Transfer }): ReactElement | null => {
+export const TxInfoTransfer = ({
+ txInfo,
+ txStatus,
+}: {
+ txInfo: Transfer
+ txStatus: TransactionStatus
+}): ReactElement | null => {
const assetInfo = useAssetInfo(txInfo)
const [details, setDetails] = useState
()
useEffect(() => {
- if (assetInfo && assetInfo.type === 'Transfer') {
- if (txInfo.direction.toUpperCase() === 'INCOMING') {
- setDetails({
- title: `Received ${assetInfo.amountWithSymbol} from:`,
- address: txInfo.sender.value,
- name: txInfo.sender.name || undefined,
- })
- } else {
- setDetails({
- title: `Send ${assetInfo.amountWithSymbol} to:`,
- address: txInfo.recipient.value,
- name: txInfo.recipient.name || undefined,
- })
- }
+ if (isTransferAssetInfo(assetInfo)) {
+ const txDirection = txInfo.direction.toUpperCase()
+ setDetails({
+ title: makeTitle(txDirection, assetInfo.amountWithSymbol, txStatus),
+ address: txDirection === TransferDirection.INCOMING ? txInfo.sender.value : txInfo.recipient.value,
+ name: (txDirection === TransferDirection.INCOMING ? txInfo.sender.name : txInfo.recipient.name) || undefined,
+ })
}
- }, [assetInfo, txInfo.direction, txInfo.recipient, txInfo.sender])
+ }, [assetInfo, txInfo.direction, txInfo.recipient, txInfo.sender, txStatus])
return details ? : null
}
diff --git a/src/routes/safe/components/Transactions/TxList/TxOwners.tsx b/src/routes/safe/components/Transactions/TxList/TxOwners.tsx
index 51a0e9053b..c4aef6d016 100644
--- a/src/routes/safe/components/Transactions/TxList/TxOwners.tsx
+++ b/src/routes/safe/components/Transactions/TxList/TxOwners.tsx
@@ -1,20 +1,136 @@
-import { Text, Icon } from '@gnosis.pm/safe-react-components'
-import { ReactElement } from 'react'
-import styled from 'styled-components'
-
-import Img from 'src/components/layout/Img'
-import { ExpandedTxDetails, isModuleExecutionInfo } 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 { AddressInfo } from './AddressInfo'
-import { OwnerList, OwnerListItem } from './styled'
-import { isCancelTxDetails } from './utils'
-
-const StyledImg = styled(Img)`
- background-color: transparent;
- border-radius: 50%;
+import { ReactElement, useState, CSSProperties } from 'react'
+import styled, { AnyStyledComponent } from 'styled-components'
+import Step from '@material-ui/core/Step'
+import StepConnector from '@material-ui/core/StepConnector'
+import StepContent from '@material-ui/core/StepContent'
+import StepLabel from '@material-ui/core/StepLabel'
+import Stepper from '@material-ui/core/Stepper'
+import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'
+import AddCircleIcon from '@material-ui/icons/AddCircle'
+import RadioButtonUncheckedOutlinedIcon from '@material-ui/icons/RadioButtonUncheckedOutlined'
+import CheckCircleIcon from '@material-ui/icons/CheckCircle'
+import CancelIcon from '@material-ui/icons/Cancel'
+import { AddressEx, DetailedExecutionInfo } from '@gnosis.pm/safe-react-gateway-sdk'
+import { useSelector } from 'react-redux'
+
+import { ExpandedTxDetails, isMultiSigExecutionDetails } from 'src/logic/safe/store/models/types/gateway.d'
+import { AddressInfo } from 'src/routes/safe/components/Transactions/TxList/AddressInfo'
+import { isCancelTxDetails } from 'src/routes/safe/components/Transactions/TxList/utils'
+import { black300, gray500, primary400, red400, orange500 } from 'src/theme/variables'
+import { currentSafe } from 'src/logic/safe/store/selectors'
+import { currentChainId } from 'src/logic/config/store/selectors'
+import { addressBookName } from 'src/logic/addressBook/store/selectors'
+import { userAccountSelector } from 'src/logic/wallets/store/selectors'
+
+// Icons
+
+// All icons from MUI share the same type
+const getStyledIcon = (icon: typeof AddCircleIcon): AnyStyledComponent => {
+ return styled(icon)`
+ height: 20px;
+ width: 20px;
+ margin-left: 2px;
+ `
+}
+
+const TxCreationIcon = getStyledIcon(AddCircleIcon)
+const TxRejectionIcon = getStyledIcon(CancelIcon)
+const CheckIcon = getStyledIcon(CheckCircleIcon)
+
+const CircleIcon = styled(getStyledIcon(RadioButtonUncheckedOutlinedIcon))`
+ stroke: currentColor;
+ stroke-width: 1px;
+`
+const DotIcon = styled(FiberManualRecordIcon)`
+ height: 14px;
+ width: 14px;
+ margin-left: 5px;
+`
+
+// Stepper
+const StyledStepper = styled(Stepper)`
+ padding: 0;
+`
+
+const StyledStepConnector = styled(StepConnector)`
+ padding: 3px 0;
+
+ .MuiStepConnector-line {
+ margin-left: -1px;
+ border-color: ${gray500};
+ border-left-width: 2px;
+ min-height: 14px;
+ }
+`
+
+type StepState = 'confirmed' | 'active' | 'disabled' | 'error'
+const getStepColor = (state: StepState): string => {
+ switch (state) {
+ case 'confirmed':
+ return primary400
+ case 'active':
+ return orange500
+ case 'disabled':
+ return black300
+ case 'error':
+ return red400
+ }
+}
+
+type StyledStepProps = {
+ $bold?: boolean
+ state: StepState
+}
+const StyledStep = styled(Step)`
+ .MuiStepLabel-label {
+ font-weight: ${({ $bold = false }) => ($bold ? 'bold' : 'normal')};
+ font-size: 16px;
+ color: ${({ state }) => getStepColor(state)};
+ }
+
+ .MuiStepLabel-iconContainer {
+ color: ${({ state }) => getStepColor(state)};
+ align-items: center;
+ }
+`
+
+const StyledStepContent = styled(StepContent)`
+ color: ${black300};
`
+// Simple memoized styles
+const pointerStyle: CSSProperties = {
+ cursor: 'pointer',
+}
+
+const confirmationsStyle: CSSProperties = {
+ color: black300,
+ fontWeight: 'normal',
+}
+
+const shouldHideConfirmations = (detailedExecutionInfo: DetailedExecutionInfo | null): boolean => {
+ if (!detailedExecutionInfo || !isMultiSigExecutionDetails(detailedExecutionInfo)) {
+ return true
+ }
+
+ const confirmationsNeeded = detailedExecutionInfo.confirmationsRequired - detailedExecutionInfo.confirmations.length
+ const isConfirmed = confirmationsNeeded <= 0
+
+ // Threshold reached or more than 3 confirmations
+ return isConfirmed || detailedExecutionInfo.confirmations.length > 3
+}
+
+const getConfirmationStep = (
+ { value, name, logoUri }: AddressEx,
+ key: string | undefined = undefined,
+): ReactElement => (
+
+ }>
+
+
+
+)
+
export const TxOwners = ({
txDetails,
isPending,
@@ -24,92 +140,79 @@ export const TxOwners = ({
}): ReactElement | null => {
const { txInfo, detailedExecutionInfo } = txDetails
- if (!detailedExecutionInfo || isModuleExecutionInfo(detailedExecutionInfo)) {
+ const [hideConfirmations, setHideConfirmations] = useState(shouldHideConfirmations(detailedExecutionInfo))
+
+ const { threshold } = useSelector(currentSafe)
+ const account = useSelector(userAccountSelector)
+ const chainId = useSelector(currentChainId)
+ const name = useSelector((state) => addressBookName(state, { address: account, chainId }))
+
+ const toggleHide = () => {
+ setHideConfirmations((prev) => !prev)
+ }
+
+ if (!detailedExecutionInfo || !isMultiSigExecutionDetails(detailedExecutionInfo)) {
return null
}
const confirmationsNeeded = detailedExecutionInfo.confirmationsRequired - detailedExecutionInfo.confirmations.length
- const CreationNode = isCancelTxDetails(txInfo) ? (
-
-
-
-
-
-
- On-chain rejection created
-
-
-
- ) : (
-
-
-
-
-
-
- Created
-
-
-
- )
+ const isImmediateExecution = isPending && threshold === 1
+ const isConfirmed = confirmationsNeeded <= 0 || isImmediateExecution
+ const isExecuted = !!detailedExecutionInfo.executor
+ const numberOfConfirmations = isImmediateExecution ? 1 : detailedExecutionInfo.confirmations.length
return (
-
- {CreationNode}
- {detailedExecutionInfo.confirmations.map(({ signer }) => (
-
-
-
-
-
-
- ))}
- {isPending || confirmationsNeeded <= 0 ? (
-
-
- {detailedExecutionInfo.executor ? (
-
- ) : (
-
- )}
+ }>
+ {isCancelTxDetails(txInfo) ? (
+
+ }>On-chain rejection created
+
+ ) : (
+
+ }>Created
+
+ )}
+
+ : }>
+ Confirmations{' '}
+
+ ({`${numberOfConfirmations} of ${detailedExecutionInfo.confirmationsRequired}`})
-
-
- {detailedExecutionInfo.executor ? 'Executed' : isPending ? 'Executing' : 'Execute'}
-
- {detailedExecutionInfo.executor && (
+
+
+ {!hideConfirmations &&
+ (isImmediateExecution
+ ? getConfirmationStep({ value: account, name, logoUri: null })
+ : detailedExecutionInfo.confirmations.map(({ signer }) => getConfirmationStep(signer, signer.value)))}
+ {detailedExecutionInfo.confirmations.length > 0 && (
+
+ } onClick={toggleHide}>
+ {hideConfirmations ? 'Show all' : 'Hide all'}
+
+
+ )}
+
+ : }>
+ {isExecuted ? 'Executed' : isPending ? 'Executing' : 'Execution'}
+
+ {
+ // isExecuted
+ detailedExecutionInfo.executor ? (
+
- )}
-
-
- ) : (
-
-
-
-
-
-
- Execute ({confirmationsNeeded} more {confirmationsNeeded === 1 ? 'confirmation' : 'confirmations'} needed)
-
-
-
- )}
-
+
+ ) : (
+ !isConfirmed &&
+ !isPending && Can be executed once the threshold is reached
+ )
+ }
+
+
)
}
diff --git a/src/routes/safe/components/Transactions/TxList/TxQueueRow.tsx b/src/routes/safe/components/Transactions/TxList/TxQueueRow.tsx
index 59d46858e2..16a6a21d30 100644
--- a/src/routes/safe/components/Transactions/TxList/TxQueueRow.tsx
+++ b/src/routes/safe/components/Transactions/TxList/TxQueueRow.tsx
@@ -1,11 +1,18 @@
import { AccordionDetails } from '@gnosis.pm/safe-react-components'
import { ReactElement, useContext, useEffect, useState } from 'react'
-import { LocalTransactionStatus, Transaction } from 'src/logic/safe/store/models/types/gateway.d'
+import {
+ isMultisigExecutionInfo,
+ LocalTransactionStatus,
+ Transaction,
+} from 'src/logic/safe/store/models/types/gateway.d'
import { NoPaddingAccordion, StyledAccordionSummary } from './styled'
import { TxDetails } from './TxDetails'
import { TxHoverContext } from './TxHoverProvider'
import { TxQueueCollapsed } from './TxQueueCollapsed'
+import { useSelector } from 'react-redux'
+import { AppReduxState } from 'src/store'
+import { isTxPending, pendingTxByChain } from 'src/logic/safe/store/selectors/pendingTransactions'
type TxQueueRowProps = {
isGrouped?: boolean
@@ -15,15 +22,21 @@ type TxQueueRowProps = {
export const TxQueueRow = ({ isGrouped = false, transaction }: TxQueueRowProps): ReactElement => {
const { activeHover } = useContext(TxHoverContext)
const [tx, setTx] = useState(transaction)
+ const willBeReplaced = tx.txStatus === LocalTransactionStatus.WILL_BE_REPLACED ? ' will-be-replaced' : ''
+ const isPending = useSelector((state: AppReduxState) => isTxPending(state, transaction.id))
+ const pendingTx = useSelector(pendingTxByChain)
+ const pendingTxNonce = isMultisigExecutionInfo(pendingTx?.executionInfo) ? pendingTx?.executionInfo.nonce : undefined
+ const nonce = isMultisigExecutionInfo(transaction.executionInfo) ? transaction.executionInfo.nonce : undefined
+ const isReplacementTxPending = !isPending && nonce && pendingTxNonce && nonce === pendingTxNonce
useEffect(() => {
- if (activeHover && activeHover !== transaction.id) {
+ if ((activeHover && activeHover !== transaction.id) || isReplacementTxPending) {
setTx((currTx) => ({ ...currTx, txStatus: LocalTransactionStatus.WILL_BE_REPLACED }))
return
}
setTx(transaction)
- }, [activeHover, transaction])
+ }, [activeHover, transaction, isReplacementTxPending])
return (
diff --git a/src/routes/safe/components/Transactions/TxList/TxSingularDetails.tsx b/src/routes/safe/components/Transactions/TxList/TxSingularDetails.tsx
index f30e0b117b..366b2e19e1 100644
--- a/src/routes/safe/components/Transactions/TxList/TxSingularDetails.tsx
+++ b/src/routes/safe/components/Transactions/TxList/TxSingularDetails.tsx
@@ -1,5 +1,5 @@
import { ReactElement, useEffect, useState } from 'react'
-import { useParams } from 'react-router-dom'
+import { useHistory, useParams } from 'react-router-dom'
import { Loader } from '@gnosis.pm/safe-react-components'
import { shallowEqual, useDispatch, useSelector } from 'react-redux'
import { TransactionDetails } from '@gnosis.pm/safe-react-gateway-sdk'
@@ -12,7 +12,6 @@ import {
SafeRouteSlugs,
SAFE_ROUTES,
TRANSACTION_ID_SLUG,
- history,
} from 'src/routes/routes'
import { Centered } from './styled'
import { getTransactionWithLocationByAttribute } from 'src/logic/safe/store/selectors/gatewayTransactions'
@@ -30,12 +29,15 @@ import { Transaction } from 'src/logic/safe/store/models/types/gateway.d'
import { currentChainId } from 'src/logic/config/store/selectors'
import { QueueTxList } from './QueueTxList'
import { HistoryTxList } from './HistoryTxList'
+import FetchError from '../../FetchError'
const TxSingularDetails = (): ReactElement => {
const { [TRANSACTION_ID_SLUG]: safeTxHash = '' } = useParams()
const [fetchedTx, setFetchedTx] = useState()
const [liveTx, setLiveTx] = useState<{ txLocation: TxLocation; transaction: Transaction }>()
+ const [error, setError] = useState()
const dispatch = useDispatch()
+ const history = useHistory()
const chainId = useSelector(currentChainId)
// We must use the tx from the store as the queue actions alter the tx
@@ -76,6 +78,7 @@ const TxSingularDetails = (): ReactElement => {
txDetails = await fetchSafeTransaction(safeTxHash)
} catch (e) {
logError(Errors._614, e.message)
+ setError(e)
return
}
@@ -89,7 +92,7 @@ const TxSingularDetails = (): ReactElement => {
return () => {
isCurrent = false
}
- }, [safeTxHash, setFetchedTx, setLiveTx])
+ }, [history, safeTxHash, setFetchedTx, setLiveTx])
// Add the tx to the store
useEffect(() => {
@@ -112,6 +115,17 @@ const TxSingularDetails = (): ReactElement => {
dispatch(isTxQueued(listItemTx.txStatus) ? addQueuedTransactions(payload) : addHistoryTransactions(payload))
}, [fetchedTx, chainId, dispatch])
+ if (!liveTx && error) {
+ const safeParams = extractPrefixedSafeAddress()
+ return (
+
+ )
+ }
+
if (!liveTx) {
return (
diff --git a/src/routes/safe/components/Transactions/TxList/TxSummary.tsx b/src/routes/safe/components/Transactions/TxList/TxSummary.tsx
index f5af44f53c..bad3e61469 100644
--- a/src/routes/safe/components/Transactions/TxList/TxSummary.tsx
+++ b/src/routes/safe/components/Transactions/TxList/TxSummary.tsx
@@ -1,28 +1,60 @@
-import { Text } from '@gnosis.pm/safe-react-components'
+import { ReactElement, useState } from 'react'
import { Operation } from '@gnosis.pm/safe-react-gateway-sdk'
-import { ReactElement } from 'react'
+import { ButtonLink } from '@gnosis.pm/safe-react-components'
+import styled from 'styled-components'
-import { getExplorerInfo } from 'src/config'
import { formatDateTime } from 'src/utils/date'
import {
ExpandedTxDetails,
isMultiSendTxInfo,
isMultiSigExecutionDetails,
} from 'src/logic/safe/store/models/types/gateway.d'
-import { InlineEthHashInfo } from './styled'
import { NOT_AVAILABLE } from './utils'
import TxShareButton from './TxShareButton'
import TxInfoMultiSend from './TxInfoMultiSend'
import DelegateCallWarning from './DelegateCallWarning'
+import { TxDataRow } from 'src/routes/safe/components/Transactions/TxList/TxDataRow'
+import { sm } from 'src/theme/variables'
+
+const StyledButtonLink = styled(ButtonLink)`
+ margin-top: ${sm};
+ padding-left: 0;
+
+ & > p {
+ margin-left: 0;
+ }
+`
+
+const CollapsibleSection = styled.div<{ show: boolean }>`
+ max-height: ${({ show }) => (show ? '500px' : '0px')}; // We need to set a fixed height for the animation to work
+ overflow: hidden;
+ transition: ${({ show }) => (show ? 'max-height 0.4s ease-in-out' : 'max-height 0.2s cubic-bezier(0, 1, 0, 1)')};
+`
type Props = { txDetails: ExpandedTxDetails }
export const TxSummary = ({ txDetails }: Props): ReactElement => {
const { txHash, detailedExecutionInfo, executedAt, txData, txInfo } = txDetails
- const explorerUrl = txHash ? getExplorerInfo(txHash) : undefined
- const nonce = isMultiSigExecutionDetails(detailedExecutionInfo) ? detailedExecutionInfo.nonce : undefined
- const created = isMultiSigExecutionDetails(detailedExecutionInfo) ? detailedExecutionInfo.submittedAt : undefined
- const safeTxHash = isMultiSigExecutionDetails(detailedExecutionInfo) ? detailedExecutionInfo.safeTxHash : undefined
+ const [expanded, setExpanded] = useState(false)
+
+ const toggleExpanded = () => {
+ setExpanded((val) => !val)
+ }
+
+ let created, confirmations, safeTxHash, baseGas, gasPrice, gasToken, refundReceiver, safeTxGas
+ if (isMultiSigExecutionDetails(detailedExecutionInfo)) {
+ // prettier-ignore
+ ({
+ submittedAt: created,
+ confirmations,
+ safeTxHash,
+ baseGas,
+ gasPrice,
+ gasToken,
+ safeTxGas,
+ } = detailedExecutionInfo)
+ refundReceiver = detailedExecutionInfo.refundReceiver?.value
+ }
return (
<>
@@ -31,60 +63,53 @@ export const TxSummary = ({ txDetails }: Props): ReactElement => {
)}
-
-
- Transaction hash:{' '}
-
- {txHash ? (
-
- ) : (
-
- {NOT_AVAILABLE}
-
- )}
-
- {safeTxHash !== undefined && (
-
-
- SafeTxHash:{' '}
-
-
-
- )}
- {nonce !== undefined && (
-
-
- Nonce:{' '}
-
-
- {nonce}
-
-
- )}
- {created && (
-
-
- Created:{' '}
-
-
- {formatDateTime(created)}
-
-
- )}
-
-
- Executed:{' '}
-
-
- {executedAt ? formatDateTime(executedAt) : NOT_AVAILABLE}
-
-
{txData?.operation === Operation.DELEGATE && (
)}
- {isMultiSendTxInfo(txInfo) &&
}
+ {isMultiSendTxInfo(txInfo) && (
+ <>
+
+
+ >
+ )}
+
+
+
+
+
+
+ {/* Advanced TxData */}
+ {txData && (
+ <>
+
+ Advanced Details
+
+
+ {txData?.operation !== undefined && (
+
+ )}
+
+
+
+
+
+ {confirmations?.map(({ signature }, index) => (
+
+ ))}
+
+
+ >
+ )}
>
)
}
diff --git a/src/routes/safe/components/Transactions/TxList/assets/transactions-list-active.svg b/src/routes/safe/components/Transactions/TxList/assets/transactions-list-active.svg
deleted file mode 100644
index e2e3c6f470..0000000000
--- a/src/routes/safe/components/Transactions/TxList/assets/transactions-list-active.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
diff --git a/src/routes/safe/components/Transactions/TxList/assets/transactions-list-inactive.svg b/src/routes/safe/components/Transactions/TxList/assets/transactions-list-inactive.svg
deleted file mode 100644
index 3e429f5512..0000000000
--- a/src/routes/safe/components/Transactions/TxList/assets/transactions-list-inactive.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
diff --git a/src/routes/safe/components/Transactions/TxList/hooks/useKnownAddress.ts b/src/routes/safe/components/Transactions/TxList/hooks/useKnownAddress.ts
index 4e1e4fc091..1e4402203d 100644
--- a/src/routes/safe/components/Transactions/TxList/hooks/useKnownAddress.ts
+++ b/src/routes/safe/components/Transactions/TxList/hooks/useKnownAddress.ts
@@ -10,7 +10,9 @@ const DEFAULT_PROPS: AddressEx = {
name: null,
logoUri: null,
}
-export const useKnownAddress = (props: AddressEx | null = DEFAULT_PROPS): AddressEx & { isInAddressBook: boolean } => {
+
+export type KnownAddressType = AddressEx & { isInAddressBook: boolean }
+export const useKnownAddress = (props: AddressEx | null = DEFAULT_PROPS): KnownAddressType => {
const recipientName = useSelector((state) => addressBookEntryName(state, { address: props?.value || '' }))
// Undefined known address
diff --git a/src/routes/safe/components/Transactions/TxList/hooks/useTransactionActions.ts b/src/routes/safe/components/Transactions/TxList/hooks/useTransactionActions.ts
index e5de6a9c18..05405d05f6 100644
--- a/src/routes/safe/components/Transactions/TxList/hooks/useTransactionActions.ts
+++ b/src/routes/safe/components/Transactions/TxList/hooks/useTransactionActions.ts
@@ -11,11 +11,6 @@ import { grantedSelector } from 'src/routes/safe/container/selector'
import { AppReduxState } from 'src/store'
import { TxLocationContext } from '../TxLocationProvider'
-export const isThresholdReached = (executionInfo: MultisigExecutionInfo): boolean => {
- const { confirmationsSubmitted, confirmationsRequired } = executionInfo
- return confirmationsSubmitted >= confirmationsRequired
-}
-
export type TransactionActions = {
canConfirm: boolean
canConfirmThenExecute: boolean
diff --git a/src/routes/safe/components/Transactions/TxList/hooks/useTransactionType.ts b/src/routes/safe/components/Transactions/TxList/hooks/useTransactionType.ts
index d279a6a9c2..c26a9e4592 100644
--- a/src/routes/safe/components/Transactions/TxList/hooks/useTransactionType.ts
+++ b/src/routes/safe/components/Transactions/TxList/hooks/useTransactionType.ts
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
-import { Transaction } from 'src/logic/safe/store/models/types/gateway.d'
+import { isTxQueued, Transaction } from 'src/logic/safe/store/models/types/gateway.d'
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'
@@ -32,7 +32,7 @@ export const useTransactionType = (tx: Transaction): TxTypeProps => {
setType({
icon: isSendTx ? OutgoingTxIcon : IncomingTxIcon,
- text: isSendTx ? 'Send' : 'Receive',
+ text: isSendTx ? (isTxQueued(tx.txStatus) ? 'Send' : 'Sent') : 'Received',
})
break
}
diff --git a/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx b/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx
index 315f93d8a3..ec7e4d65e4 100644
--- a/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx
+++ b/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx
@@ -6,13 +6,12 @@ import {
Operation,
TokenType,
} from '@gnosis.pm/safe-react-gateway-sdk'
-import { useMemo, useRef, useState } from 'react'
+import { useMemo, useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useStyles } from './style'
-import Modal, { ButtonStatus, Modal as GenericModal } from 'src/components/Modal'
-import { ReviewInfoText } from 'src/components/ReviewInfoText'
+import Modal from 'src/components/Modal'
import Block from 'src/components/layout/Block'
import Bold from 'src/components/layout/Bold'
import Hairline from 'src/components/layout/Hairline'
@@ -20,40 +19,31 @@ import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
import { processTransaction } from 'src/logic/safe/store/actions/processTransaction'
-import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
-import { useEstimationStatus } from 'src/logic/hooks/useEstimationStatus'
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
-import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
-import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
-import { isThresholdReached } from 'src/routes/safe/components/Transactions/TxList/hooks/useTransactionActions'
import { ModalHeader } from 'src/routes/safe/components/Balances/SendModal/screens/ModalHeader'
import { Overwrite } from 'src/types/helpers'
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
import { makeConfirmation } from 'src/logic/safe/store/models/confirmation'
-import { NOTIFICATIONS } from 'src/logic/notifications'
-import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
-import { ExpandedTxDetails, isMultiSigExecutionDetails, Transaction } from 'src/logic/safe/store/models/types/gateway.d'
+import {
+ ExpandedTxDetails,
+ isMultiSigExecutionDetails,
+ isMultisigExecutionInfo,
+ Transaction,
+} from 'src/logic/safe/store/models/types/gateway.d'
import { extractSafeAddress } from 'src/routes/routes'
-import ExecuteCheckbox from 'src/components/ExecuteCheckbox'
+import { TxModalWrapper } from '../../helpers/TxModalWrapper'
+import { grantedSelector } from 'src/routes/safe/container/selector'
-export const APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID = 'approve-tx-modal-submit-btn'
export const REJECT_TX_MODAL_SUBMIT_BTN_TEST_ID = 'reject-tx-modal-submit-btn'
-const getModalTitleAndDescription = (
- thresholdReached: boolean,
- isCancelTx: boolean,
-): { title: string; description: string } => {
+const getModalTitleAndDescription = (thresholdReached: boolean): { title: string; description: string } => {
const modalInfo = {
title: 'Execute transaction rejection',
description: 'This action will execute this transaction.',
}
- if (isCancelTx) {
- return modalInfo
- }
-
if (thresholdReached) {
modalInfo.title = 'Execute transaction'
modalInfo.description = 'This action will execute this transaction.'
@@ -198,212 +188,75 @@ const useTxInfo = (transaction: Props['transaction']) => {
type Props = {
onClose: () => void
- isExecution?: boolean
- isCancelTx?: boolean
isOpen: boolean
transaction: Overwrite
- txParameters: TxParameters
}
-export const ApproveTxModal = ({
- onClose,
- isExecution = false,
- isCancelTx = false,
- isOpen,
- transaction,
-}: Props): React.ReactElement => {
+export const ApproveTxModal = ({ onClose, isOpen, transaction }: Props): React.ReactElement => {
const dispatch = useDispatch()
const userAddress = useSelector(userAccountSelector)
+ const isOwner = useSelector(grantedSelector)
const classes = useStyles()
const safeAddress = extractSafeAddress()
- const [shouldExecute, setShouldExecute] = useState(isExecution)
- const executionInfo = transaction.executionInfo as MultisigExecutionInfo
- const thresholdReached = !!(transaction.executionInfo && isThresholdReached(executionInfo))
- const _threshold = executionInfo?.confirmationsRequired ?? 0
- const _countingCurrentConfirmation = (executionInfo?.confirmationsSubmitted ?? 0) + 1
- const { description, title } = getModalTitleAndDescription(thresholdReached, isCancelTx)
- const oneConfirmationLeft = !thresholdReached && _countingCurrentConfirmation === _threshold
- const isTheTxReadyToBeExecuted = oneConfirmationLeft ? true : thresholdReached
- const [manualGasPrice, setManualGasPrice] = useState()
- const [manualMaxPrioFee, setManualMaxPrioFee] = useState()
- const [manualGasLimit, setManualGasLimit] = useState()
- const willExecute = isExecution && shouldExecute
-
- const {
- confirmations,
- data,
- baseGas,
- gasPrice,
- safeTxGas,
- gasToken,
- nonce,
- refundReceiver,
- safeTxHash,
- value,
- to,
- operation,
- origin,
- id,
- } = useTxInfo(transaction)
- const {
- gasLimit,
- gasPriceFormatted,
- gasCostFormatted,
- gasMaxPrioFeeFormatted,
- txEstimationExecutionStatus,
- isOffChainSignature,
- isCreation,
- } = useEstimateTransactionGas({
- txRecipient: to,
- txData: data,
- txConfirmations: confirmations,
- txAmount: value,
- preApprovingOwner: shouldExecute ? userAddress : undefined,
- safeTxGas,
- operation,
- manualGasPrice,
- manualMaxPrioFee,
- manualGasLimit,
- isExecution: willExecute,
- })
- const [buttonStatus] = useEstimationStatus(txEstimationExecutionStatus)
-
- const approveTx = (txParameters: TxParameters) => {
- if (thresholdReached && confirmations.size < _threshold) {
- dispatch(enqueueSnackbar(NOTIFICATIONS.TX_FETCH_SIGNATURES_ERROR_MSG))
- } else {
- dispatch(
- processTransaction({
- safeAddress,
- tx: {
- id,
- baseGas,
- confirmations,
- data,
- gasPrice,
- gasToken,
- nonce,
- operation,
- origin,
- refundReceiver,
- safeTxGas,
- safeTxHash,
- to,
- value,
- },
- userAddress,
- notifiedTransaction: TX_NOTIFICATION_TYPES.CONFIRMATION_TX,
- approveAndExecute: isExecution && shouldExecute && isTheTxReadyToBeExecuted,
- ethParameters: txParameters,
- thresholdReached,
- }),
- )
- }
- onClose()
- }
-
- const getParametersStatus = () => {
- if (isExecution || shouldExecute) {
- return 'SAFE_DISABLED'
- }
-
- return 'DISABLED'
+ const txInfo = useTxInfo(transaction)
+
+ const { executionInfo } = transaction
+ const { confirmationsSubmitted = 0, confirmationsRequired = 0 } = isMultisigExecutionInfo(executionInfo)
+ ? executionInfo
+ : {}
+ const thresholdReached = confirmationsSubmitted >= confirmationsRequired
+ const { description, title } = getModalTitleAndDescription(thresholdReached)
+
+ let preApprovingOwner: string | undefined = undefined
+ if (!thresholdReached && isOwner && confirmationsSubmitted === confirmationsRequired - 1) {
+ preApprovingOwner = userAddress
}
- const closeEditModalCallback = (txParameters: TxParameters) => {
- const oldGasPrice = gasPriceFormatted
- const newGasPrice = txParameters.ethGasPrice
- const oldGasLimit = gasLimit
- const newGasLimit = txParameters.ethGasLimit
- const oldMaxPrioFee = gasMaxPrioFeeFormatted
- const newMaxPrioFee = txParameters.ethMaxPrioFee
-
- if (oldGasPrice !== newGasPrice) {
- setManualGasPrice(newGasPrice)
- }
-
- if (oldMaxPrioFee !== newMaxPrioFee) {
- setManualMaxPrioFee(newMaxPrioFee)
- }
-
- if (oldGasLimit !== newGasLimit) {
- setManualGasLimit(newGasLimit)
- }
+ const approveTx = (txParameters: TxParameters, delayExecution: boolean) => {
+ dispatch(
+ processTransaction({
+ safeAddress,
+ tx: txInfo,
+ preApprovingOwner,
+ notifiedTransaction: TX_NOTIFICATION_TYPES.CONFIRMATION_TX,
+ approveAndExecute: !delayExecution,
+ ethParameters: txParameters,
+ thresholdReached,
+ }),
+ )
+ onClose()
}
return (
-
- {(txParameters, toggleEditMode) => {
- return (
- <>
-
-
-
-
- {/* Tx info */}
-
-
- {description}
-
- Transaction nonce:
-
- {nonce}
-
-
- {oneConfirmationLeft && isExecution && !isCancelTx && }
-
- {/* Tx Parameters */}
- {(shouldExecute || !isOffChainSignature) && (
-
- )}
-
-
-
- {txEstimationExecutionStatus === EstimationStatus.LOADING ? null : (
-
- )}
-
- {/* Footer */}
-
- approveTx(txParameters),
- type: 'submit',
- status: buttonStatus,
- text: txEstimationExecutionStatus === EstimationStatus.LOADING ? 'Estimating' : undefined,
- testId: isCancelTx ? REJECT_TX_MODAL_SUBMIT_BTN_TEST_ID : APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID,
- }}
- />
-
- >
- )
- }}
-
+
+
+
+
+ {/* Tx info */}
+
+
+ {description}
+
+ Transaction nonce:
+
+ {txInfo.nonce}
+
+
+
+
)
}
diff --git a/src/routes/safe/components/Transactions/TxList/modals/RejectTxModal.tsx b/src/routes/safe/components/Transactions/TxList/modals/RejectTxModal.tsx
index 23aa607818..9f8daddbcc 100644
--- a/src/routes/safe/components/Transactions/TxList/modals/RejectTxModal.tsx
+++ b/src/routes/safe/components/Transactions/TxList/modals/RejectTxModal.tsx
@@ -1,58 +1,40 @@
-import { MultisigExecutionInfo } from '@gnosis.pm/safe-react-gateway-sdk'
-
import { useDispatch } from 'react-redux'
import { useStyles } from './style'
-import Modal, { ButtonStatus, Modal as GenericModal } from 'src/components/Modal'
-import { ReviewInfoText } from 'src/components/ReviewInfoText'
+import Modal from 'src/components/Modal'
import Block from 'src/components/layout/Block'
import Bold from 'src/components/layout/Bold'
import Hairline from 'src/components/layout/Hairline'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
-import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
-import { Transaction } from 'src/logic/safe/store/models/types/gateway.d'
-import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
-import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
-import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
+import { ExpandedTxDetails, isMultisigExecutionInfo, Transaction } from 'src/logic/safe/store/models/types/gateway.d'
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
-import { ParametersStatus } from 'src/routes/safe/components/Transactions/helpers/utils'
import { ModalHeader } from 'src/routes/safe/components/Balances/SendModal/screens/ModalHeader'
import { extractSafeAddress } from 'src/routes/routes'
-import useCanTxExecute from 'src/logic/hooks/useCanTxExecute'
+import { Overwrite } from 'src/types/helpers'
+import { TxModalWrapper } from '../../helpers/TxModalWrapper'
+import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
type Props = {
isOpen: boolean
onClose: () => void
- gwTransaction: Transaction
+ transaction: Overwrite
}
-export const RejectTxModal = ({ isOpen, onClose, gwTransaction }: Props): React.ReactElement => {
+export const RejectTxModal = ({ isOpen, onClose, transaction }: Props): React.ReactElement => {
const dispatch = useDispatch()
const safeAddress = extractSafeAddress()
const classes = useStyles()
+ const executionInfo = isMultisigExecutionInfo(transaction.executionInfo) ? transaction.executionInfo : undefined
- const {
- gasCostFormatted,
- txEstimationExecutionStatus,
- isOffChainSignature,
- isCreation,
- gasLimit,
- gasPriceFormatted,
- } = useEstimateTransactionGas({
- txData: EMPTY_DATA,
- txRecipient: safeAddress,
- })
- const canTxExecute = useCanTxExecute()
-
- const origin = gwTransaction.safeAppInfo
- ? JSON.stringify({ name: gwTransaction.safeAppInfo.name, url: gwTransaction.safeAppInfo.url })
+ const origin = transaction.safeAppInfo
+ ? JSON.stringify({ name: transaction.safeAppInfo.name, url: transaction.safeAppInfo.url })
: ''
- const nonce = (gwTransaction.executionInfo as MultisigExecutionInfo)?.nonce ?? 0
+ const nonce = isMultisigExecutionInfo(transaction.executionInfo) ? transaction.executionInfo.nonce : 0
- const sendReplacementTransaction = (txParameters: TxParameters) => {
+ const sendReplacementTransaction = (txParameters: TxParameters, delayExecution: boolean) => {
dispatch(
createTransaction({
safeAddress,
@@ -63,86 +45,39 @@ export const RejectTxModal = ({ isOpen, onClose, gwTransaction }: Props): React.
safeTxGas: txParameters.safeTxGas,
ethParameters: txParameters,
notifiedTransaction: TX_NOTIFICATION_TYPES.CANCELLATION_TX,
+ delayExecution,
}),
)
onClose()
}
- const getParametersStatus = (): ParametersStatus => {
- return 'CANCEL_TRANSACTION'
- }
-
- let confirmButtonStatus: ButtonStatus = ButtonStatus.READY
- let confirmButtonText = 'Reject transaction'
- if (txEstimationExecutionStatus === EstimationStatus.LOADING) {
- confirmButtonStatus = ButtonStatus.LOADING
- confirmButtonText = 'Estimating'
- }
-
return (
-
- {(txParameters, toggleEditMode) => {
- return (
- <>
-
-
-
-
-
- This action will reject this transaction. A separate transaction will be performed to submit the
- rejection.
-
-
- Transaction nonce:
-
- {nonce}
-
-
- {/* Tx Parameters */}
-
-
-
- {txEstimationExecutionStatus === EstimationStatus.LOADING ? null : (
-
- )}
-
- sendReplacementTransaction(txParameters),
- color: 'error',
- type: 'submit',
- status: confirmButtonStatus,
- text: confirmButtonText,
- }}
- />
-
- >
- )
- }}
-
+
+
+
+
+
+ This action will reject this transaction. A separate transaction will be performed to submit the
+ rejection.
+
+
+ Transaction nonce:
+
+ {nonce}
+
+
+
+
)
}
diff --git a/src/routes/safe/components/Transactions/TxList/styled.tsx b/src/routes/safe/components/Transactions/TxList/styled.tsx
index e5c93dc268..4b9abcf2a1 100644
--- a/src/routes/safe/components/Transactions/TxList/styled.tsx
+++ b/src/routes/safe/components/Transactions/TxList/styled.tsx
@@ -1,4 +1,7 @@
import { Text, Accordion, AccordionDetails, AccordionSummary, EthHashInfo } from '@gnosis.pm/safe-react-components'
+
+import PrefixedEthHashInfo from 'src/components/PrefixedEthHashInfo'
+import { lg, md, sm } from 'src/theme/variables'
import styled, { css } from 'styled-components'
import { isDeeplinkedTx } from './utils'
@@ -37,6 +40,10 @@ export const ActionAccordion = styled(Accordion)`
border-top: none;
}
+ &:last-child {
+ border-bottom: none;
+ }
+
&.Mui-expanded {
&:last-child {
border-bottom: none;
@@ -44,7 +51,7 @@ export const ActionAccordion = styled(Accordion)`
}
.MuiAccordionDetails-root {
- padding: 16px;
+ padding: ${lg};
}
}
`
@@ -89,12 +96,6 @@ export const StyledTransactions = styled.div`
&:last-child {
border-bottom: none;
}
-
- &:last-of-type {
- div {
- row-gap: 0px;
- }
- }
}
`
@@ -134,7 +135,7 @@ export const GroupedTransactionsCard = styled(StyledTransactions)`
}
`
const gridColumns = {
- nonce: '0.5fr',
+ nonce: '1fr',
type: '3fr',
info: '3fr',
time: '2.5fr',
@@ -144,10 +145,14 @@ const gridColumns = {
}
const willBeReplaced = css`
+ .will-be-replaced {
+ pointer-events: none;
+ filter: grayscale(1) opacity(0.8) !important;
+ }
.will-be-replaced * {
+ pointer-events: none;
color: gray !important;
text-decoration: line-through !important;
- filter: grayscale(1) opacity(0.8) !important;
}
`
@@ -159,32 +164,6 @@ 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};
@@ -197,10 +176,6 @@ export const StyledTransaction = styled.div`
align-self: center;
}
- .tx-type {
- ${onChainRejection};
- }
-
.tx-votes {
justify-self: center;
}
@@ -343,27 +318,67 @@ export const TxDetailsContainer = styled.div<{ ownerRows?: number }>`
${willBeReplaced};
background-color: ${({ theme }) => theme.colors.separator} !important;
- column-gap: 2px;
display: grid;
- grid-template-columns: 1fr 1fr;
- grid-auto-rows: minmax(min-content, max-content);
- grid-template-rows: [tx-summary] minmax(min-content, max-content) [tx-details] minmax(min-content, 1fr);
- row-gap: 2px;
+ gap: 2px;
+ grid-template-columns: 2fr 1fr;
width: 100%;
& > div {
- background-color: ${({ theme }) => theme.colors.white};
+ display: grid;
+ grid-auto-rows: minmax(min-content, max-content);
+ grid-template-rows: [tx-summary] minmax(min-content, max-content) [tx-details] minmax(min-content, 1fr);
line-break: anywhere;
overflow: hidden;
- padding: 20px 24px;
word-break: break-all;
+
+ & > div {
+ padding: 20px 24px;
+ background-color: ${({ theme }) => theme.colors.white};
+ }
}
.tx-summary {
+ background-color: ${({ theme }) => theme.colors.white};
+ // grows to the height of tx-owner column
+ flex-grow: 1;
+ position: relative;
+
+ &.no-data {
+ row-span: 2;
+ }
+ }
+
+ .tx-creation {
+ // to occupy the unexistant "owners" column
+ grid-column: span 2;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
}
.tx-share {
- float: right;
+ position: absolute;
+ top: 20px;
+ right: 24px;
+ }
+
+ .tx-data {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ height: 100%;
+
+ & > div:last-of-type {
+ flex: 1;
+ }
+
+ &.no-owners {
+ grid-column: span 2;
+ }
+
+ &.no-data {
+ gap: 0;
+ }
}
.tx-details {
@@ -378,20 +393,18 @@ export const TxDetailsContainer = styled.div<{ ownerRows?: number }>`
.tx-owners {
padding: 24px;
- grid-column-start: 2;
- grid-row-end: span ${({ ownerRows }) => ownerRows || 2};
- grid-row-start: 1;
+ grid-row-end: span 2;
}
.tx-details-actions {
- align-items: center;
+ align-items: flex-end;
+ padding-bottom: 24px;
display: flex;
- height: 60px;
+ gap: 8px;
justify-content: center;
button {
color: ${({ theme }) => theme.colors.white};
- margin: 0 8px;
&:hover {
color: ${({ theme }) => theme.colors.white};
@@ -416,64 +429,19 @@ export const TxDetailsContainer = styled.div<{ ownerRows?: number }>`
}
`
-export const OwnerList = styled.ul`
- list-style: none;
- margin: 0;
- padding-left: 6px;
-
- .legend {
- left: 15px;
- padding-bottom: 0.86em;
- position: relative;
- top: -3px;
-
- .owner-info {
- margin: 5px;
- }
-
- span::first-of-type {
- color: #008c73;
- font-weight: bold;
- }
- }
-
- ul {
- margin-top: 0;
- }
-
- .icon {
- left: -7px;
- position: absolute;
- width: 16px;
- z-index: 2;
- }
-`
-
-export const OwnerListItem = styled.li`
- display: flex;
- position: relative;
-
- &::before {
- border-left: 2px ${({ theme }) => theme.colors.icon} solid;
- border-radius: 1px;
- content: '';
- height: calc(100% - 16px);
- top: 16px;
- left: 0;
- position: absolute;
- z-index: 1;
- }
+export const InlineEthHashInfo = styled(EthHashInfo)`
+ display: inline-flex;
- &:last-child::before {
- border-left: none;
+ span {
+ font-weight: bold;
}
`
-export const InlineEthHashInfo = styled(EthHashInfo)`
+export const InlinePrefixedEthHashInfo = styled(PrefixedEthHashInfo)`
display: inline-flex;
span {
- font-weight: normal;
+ font-weight: bold;
}
`
@@ -540,3 +508,30 @@ export const NoTransactions = styled.div`
flex-direction: column;
margin-top: 60px;
`
+export const StyledGridRow = styled.div`
+ display: grid;
+ grid-template-columns: 1fr 2.5fr;
+ gap: ${md};
+ justify-content: flex-start;
+ max-width: 800px;
+
+ & > * {
+ flex-shrink: 0;
+ }
+`
+
+export const StyledTxInfoDetails = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${sm};
+ margin-bottom: ${md};
+
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+`
+
+export const StyledDetailsTitle = styled(Text)<{ uppercase?: boolean }>`
+ text-transform: ${({ uppercase }) => (uppercase ? 'uppercase' : null)};
+ letter-spacing: ${({ uppercase }) => (uppercase ? '1px' : null)};
+`
diff --git a/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx b/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx
index 641bba26ba..6d2f4c67db 100644
--- a/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx
+++ b/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx
@@ -2,7 +2,7 @@ import { ReactElement } from 'react'
import IconButton from '@material-ui/core/IconButton'
import Close from '@material-ui/icons/Close'
import { makeStyles } from '@material-ui/core/styles'
-import { Title, Text, Divider, Link, Icon } from '@gnosis.pm/safe-react-components'
+import { Text, Divider, Link, Icon } from '@gnosis.pm/safe-react-components'
import styled from 'styled-components'
import Field from 'src/components/forms/Field'
@@ -12,7 +12,7 @@ import Row from 'src/components/layout/Row'
import { styles } from './style'
import GnoForm from 'src/components/forms/GnoForm'
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
-import { minValue } from 'src/components/forms/validator'
+import { composeValidators, maxValue, minValue } from 'src/components/forms/validator'
import { Modal } from 'src/components/Modal'
import {
ParametersStatus,
@@ -24,9 +24,10 @@ import useSafeTxGas from 'src/routes/safe/components/Transactions/helpers/useSaf
import { isMaxFeeParam } from 'src/logic/safe/transactions/gas'
import { extractSafeAddress } from 'src/routes/routes'
import useGetRecommendedNonce from 'src/logic/hooks/useGetRecommendedNonce'
+import Paragraph from 'src/components/layout/Paragraph'
const StyledDivider = styled(Divider)`
- margin: 0px;
+ margin: 0;
`
const StyledDividerFooter = styled(Divider)`
margin: 16px -24px;
@@ -39,14 +40,9 @@ const SafeOptions = styled.div`
`
const EthereumOptions = styled.div`
- display: flex;
- /* justify-content: space-between; */
- flex-wrap: wrap;
- gap: 10px 20px;
-
- div {
- width: 216px !important;
- }
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 10px;
`
const StyledLink = styled(Link)`
margin: 16px 0 0 0;
@@ -79,7 +75,7 @@ const formValidation = (values: Record): Record
{/* Header */}
-
- Advanced options
-
+
+ Advanced parameters
+
@@ -198,7 +194,7 @@ export const EditTxParametersForm = ({
text="Gas limit"
type="number"
component={TextField}
- disabled={parametersStatus === 'CANCEL_TRANSACTION'}
+ disabled={!areEthereumParamsVisible(parametersStatus)}
/>
{((gasPriceText) => (
void) => any
- isOffChainSignature: boolean
isExecution: boolean
parametersStatus?: ParametersStatus
ethGasLimit?: TxParameters['ethGasLimit']
@@ -21,7 +17,6 @@ type Props = {
export const EditableTxParameters = ({
children,
- isOffChainSignature,
isExecution,
parametersStatus,
ethGasLimit,
@@ -33,10 +28,9 @@ export const EditableTxParameters = ({
}: Props): React.ReactElement => {
const [isEditMode, toggleEditMode] = useState(false)
const [useManualValues, setUseManualValues] = useState(false)
- const threshold = useSelector(currentSafeThreshold) || 1
- const defaultParameterStatus = isOffChainSignature && threshold > 1 ? 'ETH_HIDDEN' : 'ENABLED'
+ const defaultParameterStatus = isExecution ? 'ENABLED' : 'ETH_HIDDEN'
const txParameters = useTransactionParameters({
- parameterStatus: parametersStatus || defaultParameterStatus,
+ parametersStatus: parametersStatus || defaultParameterStatus,
initialEthGasLimit: ethGasLimit,
initialEthGasPrice: ethGasPrice,
initialEthMaxPrioFee: ethMaxPrioFee,
@@ -88,7 +82,7 @@ export const EditableTxParameters = ({
isExecution={isExecution}
txParameters={txParameters}
onClose={closeEditFormHandler}
- parametersStatus={parametersStatus ? parametersStatus : defaultParameterStatus}
+ parametersStatus={parametersStatus ?? defaultParameterStatus}
/>
) : (
children(txParameters, toggleStatus)
diff --git a/src/routes/safe/components/Transactions/helpers/TxEstimatedFeesDetail/index.tsx b/src/routes/safe/components/Transactions/helpers/TxEstimatedFeesDetail/index.tsx
new file mode 100644
index 0000000000..1208199f6e
--- /dev/null
+++ b/src/routes/safe/components/Transactions/helpers/TxEstimatedFeesDetail/index.tsx
@@ -0,0 +1,113 @@
+import { ReactElement, useState } from 'react'
+import { useSelector } from 'react-redux'
+import styled from 'styled-components'
+import { Text, ButtonLink, Accordion, AccordionSummary, AccordionDetails } from '@gnosis.pm/safe-react-components'
+
+import { currentSafeThreshold } from 'src/logic/safe/store/selectors'
+import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
+import { ParametersStatus, areEthereumParamsVisible } from '../utils'
+import Bold from 'src/components/layout/Bold'
+import { isMaxFeeParam } from 'src/logic/safe/transactions/gas'
+
+const TxParameterWrapper = styled.div`
+ display: flex;
+ justify-content: space-between;
+`
+
+const AccordionDetailsWrapper = styled.div`
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+`
+
+const StyledButtonLink = styled(ButtonLink)`
+ padding-left: 0;
+ margin: 8px 0 0 0;
+
+ > p {
+ margin-left: 0;
+ }
+`
+
+const StyledAccordionSummary = styled(AccordionSummary)`
+ & .MuiAccordionSummary-content {
+ justify-content: space-between;
+ }
+`
+
+type Props = {
+ txParameters: TxParameters
+ gasCost: string
+ onEdit: () => void
+ compact?: boolean
+ parametersStatus?: ParametersStatus
+ isTransactionCreation: boolean
+ isTransactionExecution: boolean
+ isOffChainSignature: boolean
+}
+
+export const TxEstimatedFeesDetail = ({
+ onEdit,
+ txParameters,
+ gasCost,
+ compact = true,
+ parametersStatus,
+ isTransactionCreation,
+ isTransactionExecution,
+ isOffChainSignature,
+}: Props): ReactElement | null => {
+ const [isAccordionExpanded, setIsAccordionExpanded] = useState(false)
+ const threshold = useSelector(currentSafeThreshold) || 1
+ const defaultParameterStatus = isOffChainSignature && threshold > 1 ? 'ETH_HIDDEN' : 'ENABLED'
+
+ if (!isTransactionExecution && !isTransactionCreation && isOffChainSignature) {
+ return null
+ }
+
+ const onChangeExpand = () => {
+ setIsAccordionExpanded(!isAccordionExpanded)
+ }
+
+ return (
+
+
+ Estimated fee price
+
+ {gasCost}
+
+
+
+
+ {areEthereumParamsVisible(parametersStatus || defaultParameterStatus) && (
+ <>
+
+ Nonce
+ {txParameters.ethNonce}
+
+
+
+ Gas limit
+ {txParameters.ethGasLimit}
+
+
+
+ {isMaxFeeParam() ? 'Max fee per gas' : 'Gas price'}
+ {txParameters.ethGasPrice}
+
+
+ {isMaxFeeParam() && (
+
+ Max priority fee
+ {txParameters.ethMaxPrioFee}
+
+ )}
+ >
+ )}
+
+ Edit
+
+
+
+
+ )
+}
diff --git a/src/routes/safe/components/Transactions/helpers/TxModalWrapper/index.tsx b/src/routes/safe/components/Transactions/helpers/TxModalWrapper/index.tsx
index bb66979550..bb1a819854 100644
--- a/src/routes/safe/components/Transactions/helpers/TxModalWrapper/index.tsx
+++ b/src/routes/safe/components/Transactions/helpers/TxModalWrapper/index.tsx
@@ -6,43 +6,76 @@ import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionPara
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
import { extractSafeAddress } from 'src/routes/routes'
import { ReviewInfoText } from 'src/components/ReviewInfoText'
+import { TxEstimatedFeesDetail } from 'src/routes/safe/components/Transactions/helpers/TxEstimatedFeesDetail'
import ExecuteCheckbox from 'src/components/ExecuteCheckbox'
import { useEstimationStatus } from 'src/logic/hooks/useEstimationStatus'
import { Errors, logError } from 'src/logic/exceptions/CodedException'
import { ButtonStatus, Modal } from 'src/components/Modal'
import { lg, md } from 'src/theme/variables'
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
-import { isSpendingLimit } from 'src/routes/safe/components/Transactions/helpers/utils'
+import { isSpendingLimit, ParametersStatus } from 'src/routes/safe/components/Transactions/helpers/utils'
import useCanTxExecute from 'src/logic/hooks/useCanTxExecute'
+import { useSelector } from 'react-redux'
+import { grantedSelector } from 'src/routes/safe/container/selector'
+import { List } from 'immutable'
+import { userAccountSelector } from 'src/logic/wallets/store/selectors'
+import { Confirmation } from 'src/logic/safe/store/models/types/confirmation'
+import { Operation } from '@gnosis.pm/safe-react-gateway-sdk'
+import { getNativeCurrency } from 'src/config'
type Props = {
children: ReactNode
- operation?: number
+ operation?: Operation
+ txNonce?: string
txData: string
txValue?: string
txTo?: string
txType?: string
+ txConfirmations?: List
+ txThreshold?: number
+ safeTxGas?: string
onSubmit: (txParams: TxParameters, delayExecution?: boolean) => void
+ onClose?: () => void
onBack?: (...rest: any) => void
submitText?: string
- isConfirmDisabled?: boolean
+ isSubmitDisabled?: boolean
+ isRejectTx?: boolean
}
const Container = styled.div`
padding: 0 ${lg} ${md};
`
+/**
+ * Determines which fields are displayed in the TxEditableParameters
+ */
+const getParametersStatus = (isCreation: boolean, doExecute: boolean, isRejectTx = false): ParametersStatus => {
+ return isCreation && !isRejectTx
+ ? doExecute
+ ? 'ENABLED'
+ : 'ETH_HIDDEN' // allow editing nonce when creating
+ : doExecute
+ ? 'SAFE_DISABLED'
+ : 'DISABLED' // when not creating, nonce cannot be edited
+}
+
export const TxModalWrapper = ({
children,
operation,
+ txNonce,
txData,
txValue = '0',
txTo,
txType,
+ txConfirmations,
+ txThreshold,
+ safeTxGas,
onSubmit,
onBack,
+ onClose,
submitText,
- isConfirmDisabled,
+ isSubmitDisabled,
+ isRejectTx,
}: Props): React.ReactElement => {
const [manualSafeTxGas, setManualSafeTxGas] = useState('0')
const [manualGasPrice, setManualGasPrice] = useState()
@@ -50,8 +83,15 @@ export const TxModalWrapper = ({
const [manualGasLimit, setManualGasLimit] = useState()
const [manualSafeNonce, setManualSafeNonce] = useState()
const [executionApproved, setExecutionApproved] = useState(true)
+ const isOwner = useSelector(grantedSelector)
+ const userAddress = useSelector(userAccountSelector)
const safeAddress = extractSafeAddress()
const isSpendingLimitTx = isSpendingLimit(txType)
+ const preApprovingOwner = isOwner ? userAddress : undefined
+ const confirmationsLen = Array.from(txConfirmations || []).length
+ const canTxExecute = useCanTxExecute(preApprovingOwner, confirmationsLen, txThreshold, txNonce)
+ const doExecute = executionApproved && canTxExecute
+ const nativeCurrency = getNativeCurrency()
const {
gasCostFormatted,
@@ -66,8 +106,10 @@ export const TxModalWrapper = ({
txData,
txRecipient: txTo || safeAddress,
txType,
+ txConfirmations,
txAmount: txValue,
- safeTxGas: manualSafeTxGas,
+ preApprovingOwner,
+ safeTxGas: safeTxGas || manualSafeTxGas,
manualGasPrice,
manualMaxPrioFee,
manualGasLimit,
@@ -76,11 +118,9 @@ export const TxModalWrapper = ({
})
const [submitStatus, setSubmitStatus] = useEstimationStatus(txEstimationExecutionStatus)
+ const showCheckbox = !isSpendingLimitTx && canTxExecute && (!txThreshold || txThreshold > confirmationsLen)
- const canTxExecute = useCanTxExecute(undefined, manualSafeNonce)
- const doExecute = executionApproved && canTxExecute
-
- const onClose = (txParameters: TxParameters) => {
+ const onEditClose = (txParameters: TxParameters) => {
const oldGasPrice = gasPriceFormatted
const newGasPrice = txParameters.ethGasPrice
const oldGasLimit = gasLimit
@@ -125,39 +165,54 @@ export const TxModalWrapper = ({
onSubmit(txParameters, !doExecute)
}
+ const parametersStatus = getParametersStatus(isCreation, doExecute, isRejectTx)
+
+ const gasCost = `${gasCostFormatted} ${nativeCurrency.symbol}`
+
return (
- {(txParameters: TxParameters, toggleEditMode: () => unknown) => (
+ {(txParameters: TxParameters, toggleEditMode: () => void) => (
<>
{children}
- {!isSpendingLimitTx && canTxExecute && }
+ {showCheckbox && }
+
+ {!isSpendingLimitTx && doExecute && (
+
+ )}
{/* Tx Parameters */}
{/* FIXME TxParameters should be updated to be used with spending limits */}
{!isSpendingLimitTx && (
)}
{!isSpendingLimitTx && (
+
onSubmitClick(txParameters),
status: submitStatus,
- disabled: isConfirmDisabled,
+ disabled: isSubmitDisabled,
+ color: isRejectTx ? 'error' : undefined,
text: txEstimationExecutionStatus === EstimationStatus.LOADING ? 'Estimating' : submitText,
testId: 'submit-tx-btn',
}}
diff --git a/src/routes/safe/components/Transactions/helpers/TxParametersDetail/index.tsx b/src/routes/safe/components/Transactions/helpers/TxParametersDetail/index.tsx
index e47e12821c..255e11fa3d 100644
--- a/src/routes/safe/components/Transactions/helpers/TxParametersDetail/index.tsx
+++ b/src/routes/safe/components/Transactions/helpers/TxParametersDetail/index.tsx
@@ -1,20 +1,42 @@
-import { ReactElement, useEffect, useState } from 'react'
+import { ReactElement, useEffect, useMemo, useState } from 'react'
import { useSelector } from 'react-redux'
import styled from 'styled-components'
-import { Text, ButtonLink, Accordion, AccordionSummary, AccordionDetails } from '@gnosis.pm/safe-react-components'
-
-import { currentSafe, currentSafeThreshold } from 'src/logic/safe/store/selectors'
-import { getLastTxNonce } from 'src/logic/safe/store/selectors/gatewayTransactions'
+import {
+ Text,
+ Accordion,
+ AccordionSummary,
+ AccordionDetails,
+ ButtonLink,
+ Divider,
+ EthHashInfo,
+ CopyToClipboardBtn,
+} from '@gnosis.pm/safe-react-components'
+import { Operation } from '@gnosis.pm/safe-react-gateway-sdk'
+import { ThemeColors } from '@gnosis.pm/safe-react-components/dist/theme'
+
+import { currentSafe } from 'src/logic/safe/store/selectors'
+import { getLastTxNonce, getTransactionsByNonce } from 'src/logic/safe/store/selectors/gatewayTransactions'
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
-import { ParametersStatus, areEthereumParamsVisible, areSafeParamsEnabled, ethereumTxParametersTitle } from '../utils'
+import { ParametersStatus, areSafeParamsEnabled } from 'src/routes/safe/components/Transactions/helpers/utils'
import useSafeTxGas from 'src/routes/safe/components/Transactions/helpers/useSafeTxGas'
-import { isMaxFeeParam } from 'src/logic/safe/transactions/gas'
+import { AppReduxState } from 'src/store'
+import { isMultiSigExecutionDetails, Transaction } from 'src/logic/safe/store/models/types/gateway.d'
+import { getExplorerInfo } from 'src/config'
+import PrefixedEthHashInfo from 'src/components/PrefixedEthHashInfo'
+import { getByteLength } from 'src/utils/getByteLength'
+import { md } from 'src/theme/variables'
const TxParameterWrapper = styled.div`
display: flex;
justify-content: space-between;
`
+const TxParameterEndWrapper = styled.span`
+ display: flex;
+ justify-content: flex-end;
+ gap: 4px; // EthHashInfo uses a gap between the address and copy button
+`
+
const AccordionDetailsWrapper = styled.div`
width: 100%;
display: flex;
@@ -24,8 +46,9 @@ const StyledText = styled(Text)`
margin: 8px 0 0 0;
`
-const ColoredText = styled(Text)<{ isOutOfOrder: boolean }>`
- color: ${(props) => (props.isOutOfOrder ? props.theme.colors.error : props.color)};
+type ColoredTextProps = { isError?: boolean }
+const ColoredText = styled(Text)`
+ color: ${(props) => (props.isError ? props.theme.colors.error : props.color)};
`
const StyledButtonLink = styled(ButtonLink)`
@@ -37,13 +60,42 @@ const StyledButtonLink = styled(ButtonLink)`
}
`
+const StyledDivider = styled(Divider)`
+ margin-right: -${md};
+ margin-left: -${md};
+`
+
+type TxParam = string | ReactElement
+type TxParameterProps = { name: TxParam; value?: TxParam | null; color?: ThemeColors } & ColoredTextProps
+const TxParameter = ({ name, value, ...rest }: TxParameterProps): ReactElement | null => {
+ if (value == null || value === '') {
+ return null
+ }
+
+ const getEl = (prop?: TxParam) => {
+ return typeof prop === 'string' ? (
+
+ {prop}
+
+ ) : (
+ prop
+ )
+ }
+
+ return (
+
+ {getEl(name)}
+ {getEl(value)}
+
+ )
+}
+
type Props = {
txParameters: TxParameters
- onEdit: () => void
compact?: boolean
- parametersStatus?: ParametersStatus
+ parametersStatus: ParametersStatus
+ onEdit: () => void
isTransactionCreation: boolean
- isTransactionExecution: boolean
isOffChainSignature: boolean
}
@@ -52,13 +104,8 @@ export const TxParametersDetail = ({
txParameters,
compact = true,
parametersStatus,
- isTransactionCreation,
- isTransactionExecution,
- isOffChainSignature,
}: Props): ReactElement | null => {
const { nonce } = useSelector(currentSafe)
- const threshold = useSelector(currentSafeThreshold) || 1
- const defaultParameterStatus = isOffChainSignature && threshold > 1 ? 'ETH_HIDDEN' : 'ENABLED'
const [isTxNonceOutOfOrder, setIsTxNonceOutOfOrder] = useState(false)
const [isAccordionExpanded, setIsAccordionExpanded] = useState(false)
@@ -67,10 +114,12 @@ export const TxParametersDetail = ({
const safeNonceNumber = parseInt(safeNonce, 10)
const lastQueuedTxNonce = useSelector(getLastTxNonce)
const showSafeTxGas = useSafeTxGas()
+ const storedTx = useSelector((state: AppReduxState) => getTransactionsByNonce(state, safeNonceNumber))
useEffect(() => {
- if (Number.isNaN(safeNonceNumber)) return
- if (safeNonceNumber === nonce) return
+ if (Number.isNaN(safeNonceNumber) || safeNonceNumber === nonce) {
+ return
+ }
if (lastQueuedTxNonce === undefined && safeNonceNumber !== nonce) {
setIsAccordionExpanded(true)
setIsTxNonceOutOfOrder(true)
@@ -81,95 +130,119 @@ export const TxParametersDetail = ({
}
}, [lastQueuedTxNonce, nonce, safeNonceNumber])
- if (!isTransactionExecution && !isTransactionCreation && isOffChainSignature) {
- return null
- }
+ const color = useMemo(() => (areSafeParamsEnabled(parametersStatus) ? 'text' : 'secondaryLight'), [parametersStatus])
const onChangeExpand = () => {
setIsAccordionExpanded(!isAccordionExpanded)
}
+ if (parametersStatus === 'DISABLED') {
+ return null
+ }
+
return (
- Advanced options
+ Advanced parameters
- Safe transaction
+ Safe transaction parameters
-
-
-
- Safe nonce
-
-
- {txParameters.safeNonce}
-
-
-
- {showSafeTxGas && (
-
-
- SafeTxGas
-
-
- {txParameters.safeTxGas}
-
-
- )}
-
- {areEthereumParamsVisible(parametersStatus || defaultParameterStatus) && (
- <>
-
-
- {ethereumTxParametersTitle(isTransactionExecution)}
-
-
-
-
- Nonce
- {txParameters.ethNonce}
-
-
-
- Gas limit
- {txParameters.ethGasLimit}
-
-
-
- {isMaxFeeParam() ? 'Max fee per gas' : 'Gas price'}
- {txParameters.ethGasPrice}
-
-
- {isMaxFeeParam() && (
-
- Max priority fee
- {txParameters.ethMaxPrioFee}
-
- )}
- >
- )}
+
+
+ {showSafeTxGas && }
Edit
+ {storedTx?.length > 0 && }
)
}
+
+const TxAdvancedParametersDetail = ({ tx }: { tx: Transaction }) => {
+ const { txData, detailedExecutionInfo } = tx?.txDetails || {}
+
+ if (!txData || !detailedExecutionInfo) {
+ return null
+ }
+
+ const { value, to, operation, hexData } = txData
+ const { safeTxHash, baseGas, gasPrice, gasToken, refundReceiver, confirmations } =
+ (isMultiSigExecutionDetails(detailedExecutionInfo) && detailedExecutionInfo) || {}
+
+ return (
+ <>
+
+
+
+ )
+ }
+ />
+ }
+ />
+ {Object.values(Operation).includes(operation) && (
+
+ )}
+
+
+ }
+ />
+
+ }
+ />
+ {confirmations
+ ?.filter(({ signature }) => signature)
+ .map(({ signature }, i) => (
+
+
+ {signature ? getByteLength(signature) : 0} bytes
+
+ {signature && }
+
+ }
+ />
+ ))}
+
+
+ {hexData ? getByteLength(hexData) : 0} bytes
+
+ {hexData && }
+
+ }
+ />
+ >
+ )
+}
diff --git a/src/routes/safe/components/Transactions/helpers/utils.ts b/src/routes/safe/components/Transactions/helpers/utils.tsx
similarity index 88%
rename from src/routes/safe/components/Transactions/helpers/utils.ts
rename to src/routes/safe/components/Transactions/helpers/utils.tsx
index 3263849fbe..07c399047a 100644
--- a/src/routes/safe/components/Transactions/helpers/utils.ts
+++ b/src/routes/safe/components/Transactions/helpers/utils.tsx
@@ -4,9 +4,7 @@ import { sameString } from 'src/utils/strings'
export type ParametersStatus = 'ENABLED' | 'DISABLED' | 'SAFE_DISABLED' | 'ETH_HIDDEN' | 'CANCEL_TRANSACTION'
export const areEthereumParamsVisible = (parametersStatus: ParametersStatus): boolean => {
- return (
- parametersStatus === 'ENABLED' || (parametersStatus !== 'ETH_HIDDEN' && parametersStatus !== 'CANCEL_TRANSACTION')
- )
+ return parametersStatus === 'ENABLED' || parametersStatus !== 'ETH_HIDDEN'
}
export const areSafeParamsEnabled = (parametersStatus: ParametersStatus): boolean => {
diff --git a/src/routes/safe/container/hooks/useAddressedRouteKey.ts b/src/routes/safe/container/hooks/useAddressedRouteKey.ts
deleted file mode 100644
index 8a0f7e9349..0000000000
--- a/src/routes/safe/container/hooks/useAddressedRouteKey.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { useState, useEffect } from 'react'
-import { useHistory, matchPath } from 'react-router-dom'
-import { SafeRouteSlugs, ADDRESSED_ROUTE, SAFE_ADDRESS_SLUG } from 'src/routes/routes'
-
-// Legacy versions of the Safe relied on subdomains and by means of
-// a new page load it reset the state of the Safe Container component
-
-// By using a custom key, set based on the address of the safe, we can
-// trigger a re-render of the Safe Container depdendent on the address
-
-export const useAddressedRouteKey = (): { key: number } => {
- const [key, setKey] = useState(Date.now())
- const history = useHistory()
-
- const match = matchPath(history.location.pathname, {
- path: ADDRESSED_ROUTE,
- })
- const prefixedSafeAddress = match?.params?.[SAFE_ADDRESS_SLUG]
-
- useEffect(() => {
- setKey(Date.now())
- }, [prefixedSafeAddress])
-
- return { key }
-}
diff --git a/src/routes/safe/container/hooks/useTransactionParameters.ts b/src/routes/safe/container/hooks/useTransactionParameters.ts
index dfe1d1378b..0145b19ce6 100644
--- a/src/routes/safe/container/hooks/useTransactionParameters.ts
+++ b/src/routes/safe/container/hooks/useTransactionParameters.ts
@@ -6,7 +6,6 @@ import { getUserNonce } from 'src/logic/wallets/ethTransactions'
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
import { currentSafeCurrentVersion } from 'src/logic/safe/store/selectors'
import { ParametersStatus } from 'src/routes/safe/components/Transactions/helpers/utils'
-import { sameString } from 'src/utils/strings'
import { extractSafeAddress } from 'src/routes/routes'
import { AppReduxState } from 'src/store'
import { getRecommendedNonce } from 'src/logic/safe/api/fetchSafeTxGasEstimation'
@@ -30,7 +29,7 @@ export type TxParameters = {
}
type Props = {
- parameterStatus?: ParametersStatus
+ parametersStatus?: ParametersStatus
initialSafeNonce?: string
initialSafeTxGas?: string
initialEthGasLimit?: string
@@ -43,7 +42,6 @@ type Props = {
* It needs to be initialized calling setGasEstimation.
*/
export const useTransactionParameters = (props?: Props): TxParameters => {
- const isCancelTransaction = sameString(props?.parameterStatus || 'ENABLED', 'CANCEL_TRANSACTION')
const connectedWalletAddress = useSelector(userAccountSelector)
const safeAddress = extractSafeAddress()
const safeVersion = useSelector(currentSafeCurrentVersion) as string
@@ -52,7 +50,7 @@ export const useTransactionParameters = (props?: Props): TxParameters => {
// Safe Params
const [safeNonce, setSafeNonce] = useState(props?.initialSafeNonce)
// SafeTxGas: for a new Tx call requiredTxGas, for an existing tx get it from the backend.
- const [safeTxGas, setSafeTxGas] = useState(isCancelTransaction ? '0' : props?.initialSafeTxGas)
+ const [safeTxGas, setSafeTxGas] = useState(props?.initialSafeTxGas)
// ETH Params
const [ethNonce, setEthNonce] = useState() // we delegate it to the wallet
@@ -80,12 +78,8 @@ export const useTransactionParameters = (props?: Props): TxParameters => {
setEthGasPriceInGWei(undefined)
return
}
- if (isCancelTransaction) {
- setEthGasPrice('0')
- return
- }
setEthGasPriceInGWei(toWei(ethGasPrice, 'Gwei'))
- }, [ethGasPrice, isCancelTransaction])
+ }, [ethGasPrice])
// Get max prio fee
useEffect(() => {
@@ -93,12 +87,8 @@ export const useTransactionParameters = (props?: Props): TxParameters => {
setEthMaxPrioFee(undefined)
return
}
- if (isCancelTransaction) {
- setEthMaxPrioFee('0')
- return
- }
setEthMaxPrioFeeInGWei(toWei(ethMaxPrioFee, 'Gwei'))
- }, [ethMaxPrioFee, isCancelTransaction])
+ }, [ethMaxPrioFee])
// Calc safe nonce
useEffect(() => {
diff --git a/src/routes/safe/container/index.tsx b/src/routes/safe/container/index.tsx
index 629db26a5b..2b23768343 100644
--- a/src/routes/safe/container/index.tsx
+++ b/src/routes/safe/container/index.tsx
@@ -6,10 +6,12 @@ import { Redirect, Route, Switch } from 'react-router-dom'
import { currentSafeFeaturesEnabled, currentSafeOwners } from 'src/logic/safe/store/selectors'
import { wrapInSuspense } from 'src/utils/wrapInSuspense'
import { LoadingContainer } from 'src/components/LoaderContainer'
-import { generateSafeRoute, extractPrefixedSafeAddress, SAFE_ROUTES } from 'src/routes/routes'
+import { generateSafeRoute, extractPrefixedSafeAddress, SAFE_ROUTES, extractSafeAddress } from 'src/routes/routes'
import { FEATURES } from '@gnosis.pm/safe-react-gateway-sdk'
import { SAFE_POLLING_INTERVAL } from 'src/utils/constants'
import SafeLoadError from '../components/SafeLoadError'
+import { useLoadSafe } from 'src/logic/safe/hooks/useLoadSafe'
+import { useSafeScheduledUpdates } from 'src/logic/safe/hooks/useSafeScheduledUpdates'
export const BALANCES_TAB_BTN_TEST_ID = 'balances-tab-btn'
export const SETTINGS_TAB_BTN_TEST_ID = 'settings-tab-btn'
@@ -31,6 +33,10 @@ const Container = (): React.ReactElement => {
const isSafeLoaded = owners.length > 0
const [hasLoadFailed, setHasLoadFailed] = useState(false)
+ const addressFromUrl = extractSafeAddress()
+ useLoadSafe(addressFromUrl) // load initially
+ useSafeScheduledUpdates(addressFromUrl) // load every X seconds
+
useEffect(() => {
if (isSafeLoaded) {
return
diff --git a/src/theme/mui.ts b/src/theme/mui.ts
index 7518e8e7f5..d64e4a05d0 100644
--- a/src/theme/mui.ts
+++ b/src/theme/mui.ts
@@ -24,6 +24,7 @@ import {
sm,
smallFontSize,
xs,
+ alertWarning,
} from './variables'
const palette = {
@@ -431,6 +432,29 @@ const theme = createTheme({
},
},
},
+ MuiAlert: {
+ root: {
+ color: fontColor,
+ height: '48px',
+ alignItems: 'center',
+ },
+ standardWarning: {
+ backgroundColor: alertWarning,
+ },
+ icon: {
+ '& > svg': {
+ width: md,
+ height: md,
+ },
+ },
+ },
+ MuiAlertTitle: {
+ root: {
+ color: fontColor,
+ fontSize: md,
+ margin: 0,
+ },
+ },
},
palette,
} as any)
diff --git a/src/theme/variables.js b/src/theme/variables.js
index 66811e2abc..378f9227f2 100644
--- a/src/theme/variables.js
+++ b/src/theme/variables.js
@@ -15,12 +15,18 @@ const secondaryTextOrSvg = '#B2B5B2'
const secondaryBackground = '#f0efee'
const sm = '8px'
const warningColor = '#ffc05f'
+const alertWarningColor = '#FBE5C5'
const xl = '32px'
const xs = '4px'
const xxl = '40px'
+const grey500 = '#E2E3E3'
+const black400 = '#566976'
+const black600 = '#111B22'
+
module.exports = {
background,
+ black300: '#B2BBC0',
boldFont: 700,
bolderFont: 500,
border,
@@ -37,6 +43,7 @@ module.exports = {
fontSizeHeadingMd: 20,
fontSizeHeadingSm: 16,
fontSizeHeadingXs: 13,
+ gray500: '#e2e3e3',
headerHeight,
largeFontSize: '16px',
lg,
@@ -45,8 +52,11 @@ module.exports = {
marginButtonImg,
md,
mediumFontSize: '14px',
+ orange500: '#e8663d',
primary,
+ primary400: '#008C73',
regularFont: 400,
+ red400: '#C31717',
screenLg: 1200,
screenMd: 992,
screenMdMax: 1199,
@@ -61,8 +71,12 @@ module.exports = {
sm,
smallFontSize: '12px',
warning: warningColor,
+ alertWarning: alertWarningColor,
xl,
xs,
xxl,
xxlFontSize: '32px',
+ grey500,
+ black400,
+ black600,
}
diff --git a/src/theme/variables.scss b/src/theme/variables.scss
index f94de36c66..9e5ac62c17 100644
--- a/src/theme/variables.scss
+++ b/src/theme/variables.scss
@@ -13,6 +13,10 @@ $warning: #ffc05f;
$fancy: #f02525;
$secondary: #008C73;
+$grey500: #E2E3E3;
+$black400: #566976;
+$black600: #111B22;
+
$headerHeight: 52px;
$marginButtonImg: 12px;
diff --git a/src/utils/camelCaseToSpaces.ts b/src/utils/camelCaseToSpaces.ts
new file mode 100644
index 0000000000..8c7bae96da
--- /dev/null
+++ b/src/utils/camelCaseToSpaces.ts
@@ -0,0 +1,6 @@
+export const camelCaseToSpaces = (str: string): string => {
+ return str
+ .replace(/([A-Z][a-z0-9]+)/g, ' $1 ')
+ .replace(/\s{2}/g, ' ')
+ .trim()
+}
diff --git a/src/utils/getByteLength.ts b/src/utils/getByteLength.ts
new file mode 100644
index 0000000000..6a5c4091c8
--- /dev/null
+++ b/src/utils/getByteLength.ts
@@ -0,0 +1,16 @@
+import { hexToBytes } from 'web3-utils'
+
+export const getByteLength = (data: string | string[]): number => {
+ try {
+ if (!Array.isArray(data)) {
+ data = data.split(',')
+ }
+ // Return the sum of the byte sizes of each hex string
+ return data.reduce((result, hex) => {
+ const bytes = hexToBytes(hex)
+ return result + bytes.length
+ }, 0)
+ } catch (err) {
+ return 0
+ }
+}
diff --git a/src/utils/history.ts b/src/utils/history.ts
index 55dbbda433..c7592c18ec 100644
--- a/src/utils/history.ts
+++ b/src/utils/history.ts
@@ -1,12 +1,13 @@
import { _getChainId } from 'src/config'
import { getChains } from 'src/config/cache/chains'
import { setChainId } from 'src/logic/config/utils'
-import { hasPrefixedSafeAddressInUrl, extractPrefixedSafeAddress } from 'src/routes/routes'
+import { extractPrefixedSafeAddress } from 'src/routes/routes'
export const setChainIdFromUrl = (pathname: string): boolean => {
- if (!hasPrefixedSafeAddressInUrl()) return false
+ const { shortName, safeAddress } = extractPrefixedSafeAddress(pathname)
+
+ if (!safeAddress) return false
- const { shortName } = extractPrefixedSafeAddress(pathname)
const chainId = getChains().find((chain) => chain.shortName === shortName)?.chainId
if (chainId) {
diff --git a/yarn.lock b/yarn.lock
index a8e03435f9..8aca16a4c5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1936,10 +1936,10 @@
resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-deployments/-/safe-deployments-1.8.0.tgz#856c15517274f924539ea4df40fffe6f009249a7"
integrity sha512-xK2ZZXxCEGOw+6UZAeUmvqE/4C/XTpYmv1a8KzKUgSOxcGkHsIDqcjdKjqif7gOdnwHl4+XXJUtDQEuSLT4Scg==
-"@gnosis.pm/safe-react-components@^0.9.0":
- version "0.9.0"
- resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-react-components/-/safe-react-components-0.9.0.tgz#c5169cab41cde96c9a375005f380cd37bd909fe0"
- integrity sha512-+SAJEuK4Zf1eBCtMpe1uQlJBN73zttIQwevEch7PDUBhJysc/oyEHuhmhpZpMYryoFgVVGYz7FZtwv2NxmUsJQ==
+"@gnosis.pm/safe-react-components@^0.9.7":
+ version "0.9.7"
+ resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-react-components/-/safe-react-components-0.9.7.tgz#6f1a9815ddf9b0fe3c3b158bd8b8e357ba5e5979"
+ integrity sha512-3f1ZvIVzjtrUZ9A1FnmHZDZBurQHc2AGssPPkIkWYDGDEy0YGANNwhUWtIzFebpWKYBgaXZnzcn9HjlSEs8/sQ==
dependencies:
react-media "^1.10.0"
web3-utils "^1.6.0"
@@ -17902,9 +17902,9 @@ simple-concat@^1.0.0:
integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
simple-get@^2.7.0:
- version "2.8.1"
- resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-2.8.1.tgz#0e22e91d4575d87620620bc91308d57a77f44b5d"
- integrity sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==
+ version "2.8.2"
+ resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-2.8.2.tgz#5708fb0919d440657326cd5fe7d2599d07705019"
+ integrity sha512-Ijd/rV5o+mSBBs4F/x9oDPtTx9Zb6X9brmnXvMW4J7IR15ngi9q5xxqWBKU744jTZiaXtxaPL7uHG6vtN8kUkw==
dependencies:
decompress-response "^3.3.0"
once "^1.3.1"