refactor: transactions table info and details

This commit is contained in:
fernandomg 2020-05-22 16:50:24 -03:00
parent 6c1bc100b6
commit 926795eef1
7 changed files with 103 additions and 224 deletions

View File

@ -15,7 +15,6 @@ import Block from 'src/components/layout/Block'
import Col from 'src/components/layout/Col' import Col from 'src/components/layout/Col'
import Img from 'src/components/layout/Img' import Img from 'src/components/layout/Img'
import Paragraph from 'src/components/layout/Paragraph/index' import Paragraph from 'src/components/layout/Paragraph/index'
import { TX_TYPE_CONFIRMATION } from 'src/logic/safe/transactions/send'
import { userAccountSelector } from 'src/logic/wallets/store/selectors' import { userAccountSelector } from 'src/logic/wallets/store/selectors'
import { makeTransaction } from 'src/routes/safe/store/models/transaction' import { makeTransaction } from 'src/routes/safe/store/models/transaction'
import { safeOwnersSelector, safeThresholdSelector } from 'src/routes/safe/store/selectors' import { safeOwnersSelector, safeThresholdSelector } from 'src/routes/safe/store/selectors'
@ -29,11 +28,8 @@ function getOwnersConfirmations(tx, userAddress) {
currentUserAlreadyConfirmed = true currentUserAlreadyConfirmed = true
} }
if (conf.type === TX_TYPE_CONFIRMATION) { ownersWhoConfirmed.push(conf.owner)
ownersWhoConfirmed.push(conf.owner)
}
}) })
return [ownersWhoConfirmed, currentUserAlreadyConfirmed] return [ownersWhoConfirmed, currentUserAlreadyConfirmed]
} }

View File

@ -1,5 +1,4 @@
import { SAFE_METHODS_NAMES } from 'src/logic/contracts/methodIds' import { SAFE_METHODS_NAMES } from 'src/logic/contracts/methodIds'
import { getWeb3 } from 'src/logic/wallets/getWeb3'
const getSafeVersion = (data) => { const getSafeVersion = (data) => {
const contractAddress = data.substr(340, 40).toLowerCase() const contractAddress = data.substr(340, 40).toLowerCase()
@ -12,43 +11,50 @@ const getSafeVersion = (data) => {
} }
export const getTxData = (tx) => { export const getTxData = (tx) => {
const web3 = getWeb3()
const { fromWei, toBN } = web3.utils
const txData: any = {} const txData: any = {}
if (tx.isTokenTransfer && tx.decodedParams) { if (tx.decodedParams) {
txData.recipient = tx.decodedParams.recipient if (tx.isTokenTransfer) {
txData.value = fromWei(toBN(tx.decodedParams.value), 'ether') const { to } = tx.decodedParams.transfer
txData.recipient = to
txData.isTokenTransfer = true
}
if (tx.isCollectibleTransfer) {
const { safeTransferFrom, transfer, transferFrom } = tx.decodedParams
const { to, value } = safeTransferFrom || transferFrom || transfer
txData.recipient = to
txData.tokenId = value
txData.isCollectibleTransfer = true
}
} else if (tx.customTx) { } else if (tx.customTx) {
txData.recipient = tx.recipient txData.recipient = tx.recipient
txData.value = fromWei(toBN(tx.value), 'ether')
txData.data = tx.data txData.data = tx.data
txData.customTx = true txData.customTx = true
} else if (Number(tx.value) > 0) { } else if (Number(tx.value) > 0) {
txData.recipient = tx.recipient txData.recipient = tx.recipient
txData.value = fromWei(toBN(tx.value), 'ether')
} else if (tx.modifySettingsTx) { } else if (tx.modifySettingsTx) {
txData.recipient = tx.recipient txData.recipient = tx.recipient
txData.modifySettingsTx = true txData.modifySettingsTx = true
if (tx.decodedParams) { if (tx.decodedParams) {
txData.action = tx.decodedParams.methodName if (tx.decodedParams[SAFE_METHODS_NAMES.REMOVE_OWNER]) {
const { _threshold, owner } = tx.decodedParams[SAFE_METHODS_NAMES.REMOVE_OWNER]
if (txData.action === SAFE_METHODS_NAMES.REMOVE_OWNER) { txData.removedOwner = owner
txData.removedOwner = tx.decodedParams.args[1] txData.newThreshold = _threshold
txData.newThreshold = tx.decodedParams.args[2] } else if (tx.decodedParams[SAFE_METHODS_NAMES.CHANGE_THRESHOLD]) {
} else if (txData.action === SAFE_METHODS_NAMES.CHANGE_THRESHOLD) { const { _threshold } = tx.decodedParams[SAFE_METHODS_NAMES.CHANGE_THRESHOLD]
txData.newThreshold = tx.decodedParams.args[0] txData.newThreshold = _threshold
} else if (txData.action === SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD) { } else if (tx.decodedParams[SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD]) {
txData.addedOwner = tx.decodedParams.args[0] const { _threshold, owner } = tx.decodedParams[SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD]
txData.newThreshold = tx.decodedParams.args[1] txData.addedOwner = owner
} else if (txData.action === SAFE_METHODS_NAMES.SWAP_OWNER) { txData.newThreshold = _threshold
txData.removedOwner = tx.decodedParams.args[1] } else if (tx.decodedParams[SAFE_METHODS_NAMES.SWAP_OWNER]) {
txData.addedOwner = tx.decodedParams.args[2] const { newOwner, oldOwner } = tx.decodedParams[SAFE_METHODS_NAMES.SWAP_OWNER]
txData.removedOwner = oldOwner
txData.addedOwner = newOwner
} }
} }
} else if (tx.cancellationTx) { } else if (tx.isCancellationTx) {
txData.cancellationTx = true txData.cancellationTx = true
} else if (tx.creationTx) { } else if (tx.creationTx) {
txData.creationTx = true txData.creationTx = true
@ -57,7 +63,6 @@ export const getTxData = (tx) => {
txData.data = `The contract of this Safe is upgraded to Version ${getSafeVersion(tx.data)}` txData.data = `The contract of this Safe is upgraded to Version ${getSafeVersion(tx.data)}`
} else { } else {
txData.recipient = tx.recipient txData.recipient = tx.recipient
txData.value = 0
} }
return txData return txData

View File

@ -11,6 +11,8 @@ import { getAppInfoFromOrigin, getAppInfoFromUrl } from 'src/routes/safe/compone
const typeToIcon = { const typeToIcon = {
outgoing: OutgoingTxIcon, outgoing: OutgoingTxIcon,
token: OutgoingTxIcon,
collectible: OutgoingTxIcon,
incoming: IncomingTxIcon, incoming: IncomingTxIcon,
custom: CustomTxIcon, custom: CustomTxIcon,
settings: SettingsTxIcon, settings: SettingsTxIcon,
@ -21,6 +23,8 @@ const typeToIcon = {
const typeToLabel = { const typeToLabel = {
outgoing: 'Outgoing transfer', outgoing: 'Outgoing transfer',
token: 'Outgoing transfer',
collectible: 'Outgoing transfer',
incoming: 'Incoming transfer', incoming: 'Incoming transfer',
custom: 'Contract Interaction', custom: 'Contract Interaction',
settings: 'Modify settings', settings: 'Modify settings',

View File

@ -2,7 +2,7 @@ import { BigNumber } from 'bignumber.js'
import format from 'date-fns/format' import format from 'date-fns/format'
import getTime from 'date-fns/getTime' import getTime from 'date-fns/getTime'
import parseISO from 'date-fns/parseISO' import parseISO from 'date-fns/parseISO'
import { List, Map } from 'immutable' import { List } from 'immutable'
import React from 'react' import React from 'react'
import TxType from './TxType' import TxType from './TxType'
@ -43,7 +43,7 @@ export const getIncomingTxAmount = (tx, formatted = true) => {
export const getTxAmount = (tx, formatted = true) => { export const getTxAmount = (tx, formatted = true) => {
const { decimals = 18, decodedParams, isTokenTransfer, symbol } = tx const { decimals = 18, decodedParams, isTokenTransfer, symbol } = tx
const { value } = isTokenTransfer && decodedParams && decodedParams.value ? decodedParams : tx const { value } = isTokenTransfer && !!decodedParams && !!decodedParams.transfer ? decodedParams.transfer : tx
if (!isTokenTransfer && !(Number(value) > 0)) { if (!isTokenTransfer && !(Number(value) > 0)) {
return NOT_AVAILABLE return NOT_AVAILABLE
@ -65,22 +65,9 @@ const getIncomingTxTableData = (tx) => ({
const getTransactionTableData = (tx, cancelTx) => { const getTransactionTableData = (tx, cancelTx) => {
const txDate = tx.submissionDate const txDate = tx.submissionDate
let txType = 'outgoing'
if (tx.modifySettingsTx) {
txType = 'settings'
} else if (tx.cancellationTx) {
txType = 'cancellation'
} else if (tx.customTx) {
txType = 'custom'
} else if (tx.creationTx) {
txType = 'creation'
} else if (tx.upgradeTx) {
txType = 'upgrade'
}
return { return {
[TX_TABLE_ID]: tx.blockNumber, [TX_TABLE_ID]: tx.blockNumber,
[TX_TABLE_TYPE_ID]: <TxType origin={tx.origin} txType={txType} />, [TX_TABLE_TYPE_ID]: <TxType origin={tx.origin} txType={tx.type} />,
[TX_TABLE_DATE_ID]: txDate ? formatDate(txDate) : '', [TX_TABLE_DATE_ID]: txDate ? formatDate(txDate) : '',
[buildOrderFieldFrom(TX_TABLE_DATE_ID)]: txDate ? getTime(parseISO(txDate)) : null, [buildOrderFieldFrom(TX_TABLE_DATE_ID)]: txDate ? getTime(parseISO(txDate)) : null,
[TX_TABLE_AMOUNT_ID]: getTxAmount(tx), [TX_TABLE_AMOUNT_ID]: getTxAmount(tx),
@ -91,17 +78,12 @@ const getTransactionTableData = (tx, cancelTx) => {
} }
export const getTxTableData = (transactions, cancelTxs) => { export const getTxTableData = (transactions, cancelTxs) => {
const cancelTxsByNonce = cancelTxs.reduce((acc, tx) => acc.set(tx.nonce, tx), Map())
return transactions.map((tx) => { return transactions.map((tx) => {
if (INCOMING_TX_TYPES[tx.type]) { if (INCOMING_TX_TYPES[tx.type] !== undefined) {
return getIncomingTxTableData(tx) return getIncomingTxTableData(tx)
} }
return getTransactionTableData( return getTransactionTableData(tx, cancelTxs.get(`${tx.nonce}`))
tx,
Number.isInteger(Number.parseInt(tx.nonce, 10)) ? cancelTxsByNonce.get(tx.nonce) : undefined,
)
}) })
} }

View File

@ -19,8 +19,8 @@ import Table from 'src/components/Table'
import { cellWidth } from 'src/components/Table/TableHead' import { cellWidth } from 'src/components/Table/TableHead'
import Block from 'src/components/layout/Block' import Block from 'src/components/layout/Block'
import Row from 'src/components/layout/Row' import Row from 'src/components/layout/Row'
import { extendedTransactionsSelector } from 'src/routes/safe/container/selector'
import { safeCancellationTransactionsSelector } from 'src/routes/safe/store/selectors' import { safeCancellationTransactionsSelector } from 'src/routes/safe/store/selectors'
import { extendedTransactionsSelector } from '../../../store/selectors/transactions'
export const TRANSACTION_ROW_TEST_ID = 'transaction-row' export const TRANSACTION_ROW_TEST_ID = 'transaction-row'
@ -36,8 +36,8 @@ const TxsTable = ({ classes }) => {
const cancellationTransactions = useSelector(safeCancellationTransactionsSelector) const cancellationTransactions = useSelector(safeCancellationTransactionsSelector)
const transactions = useSelector(extendedTransactionsSelector) const transactions = useSelector(extendedTransactionsSelector)
const handleTxExpand = (safeTxHash) => { const handleTxExpand = (rowCombinedId) => {
setExpandedTx((prevTx) => (prevTx === safeTxHash ? null : safeTxHash)) setExpandedTx((prevId) => (prevId === rowCombinedId ? null : rowCombinedId))
} }
const columns = generateColumns() const columns = generateColumns()
@ -83,58 +83,65 @@ const TxsTable = ({ classes }) => {
size={filteredData.size} size={filteredData.size}
> >
{(sortedData) => {(sortedData) =>
sortedData.map((row, index) => ( sortedData.map((row, index) => {
<React.Fragment key={index}> const rowCombinedId = `${row.tx.nonce + row.tx.data}`
<TableRow
className={cn(classes.row, expandedTx === row.tx.safeTxHash && classes.expandedRow)} return (
data-testid={TRANSACTION_ROW_TEST_ID} <React.Fragment key={index}>
onClick={() => handleTxExpand(row.tx.safeTxHash)} <TableRow
tabIndex={-1} className={cn(classes.row, expandedTx === rowCombinedId && classes.expandedRow)}
> data-testid={TRANSACTION_ROW_TEST_ID}
{autoColumns.map((column: any) => ( onClick={() => handleTxExpand(rowCombinedId)}
<TableCell tabIndex={-1}
align={column.align} >
className={cn(classes.cell, ['cancelled', 'failed'].includes(row.status) && classes.cancelledRow)} {autoColumns.map((column: any) => (
component="td" <TableCell
key={column.id} align={column.align}
style={cellWidth(column.width)} className={cn(
> classes.cell,
{row[column.id]} ['cancelled', 'failed'].includes(row.status) && classes.cancelledRow,
)}
component="td"
key={column.id}
style={cellWidth(column.width)}
>
{row[column.id]}
</TableCell>
))}
<TableCell component="td">
<Row align="end" className={classes.actions}>
<Status status={row.status} />
</Row>
</TableCell> </TableCell>
))} <TableCell style={expandCellStyle}>
<TableCell component="td"> {!row.tx.creationTx && (
<Row align="end" className={classes.actions}> <IconButton disableRipple>
<Status status={row.status} /> {expandedTx === rowCombinedId ? <ExpandLess /> : <ExpandMore />}
</Row> </IconButton>
</TableCell> )}
<TableCell style={expandCellStyle}>
{!row.tx.creationTx && (
<IconButton disableRipple>
{expandedTx === row.safeTxHash ? <ExpandLess /> : <ExpandMore />}
</IconButton>
)}
</TableCell>
</TableRow>
{!row.tx.creationTx && (
<TableRow>
<TableCell
className={classes.extendedTxContainer}
colSpan={6}
style={{ paddingBottom: 0, paddingTop: 0 }}
>
<CollapseAux
cancelTx={row[TX_TABLE_RAW_CANCEL_TX_ID]}
component={ExpandedTxComponent}
in={expandedTx === row.tx.safeTxHash}
timeout="auto"
tx={row[TX_TABLE_RAW_TX_ID]}
unmountOnExit
/>
</TableCell> </TableCell>
</TableRow> </TableRow>
)} {!row.tx.creationTx && (
</React.Fragment> <TableRow>
)) <TableCell
className={classes.extendedTxContainer}
colSpan={6}
style={{ paddingBottom: 0, paddingTop: 0 }}
>
<CollapseAux
cancelTx={row[TX_TABLE_RAW_CANCEL_TX_ID]}
component={ExpandedTxComponent}
in={expandedTx === rowCombinedId}
timeout="auto"
tx={row[TX_TABLE_RAW_TX_ID]}
unmountOnExit
/>
</TableCell>
</TableRow>
)}
</React.Fragment>
)
})
} }
</Table> </Table>
</TableContainer> </TableContainer>

View File

@ -1,4 +1,4 @@
import { List, Map } from 'immutable' import { Map } from 'immutable'
import { createSelector } from 'reselect' import { createSelector } from 'reselect'
import { tokensSelector } from 'src/logic/tokens/store/selectors' import { tokensSelector } from 'src/logic/tokens/store/selectors'
@ -6,39 +6,7 @@ import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers'
import { isUserOwner } from 'src/logic/wallets/ethAddresses' import { isUserOwner } from 'src/logic/wallets/ethAddresses'
import { userAccountSelector } from 'src/logic/wallets/store/selectors' import { userAccountSelector } from 'src/logic/wallets/store/selectors'
import { import { safeActiveTokensSelector, safeBalancesSelector, safeSelector } from 'src/routes/safe/store/selectors'
safeActiveTokensSelector,
safeBalancesSelector,
safeCancellationTransactionsSelector,
safeIncomingTransactionsSelector,
safeSelector,
safeTransactionsSelector,
} from 'src/routes/safe/store/selectors'
const getTxStatus = (tx, userAddress, safe) => {
let txStatus
if (tx.executionTxHash) {
txStatus = 'success'
} else if (tx.cancelled) {
txStatus = 'cancelled'
} else if (tx.confirmations.size === safe.threshold) {
txStatus = 'awaiting_execution'
} else if (tx.creationTx) {
txStatus = 'success'
} else if (!tx.confirmations.size) {
txStatus = 'pending'
} else {
const userConfirmed = tx.confirmations.filter((conf) => conf.owner === userAddress).size === 1
const userIsSafeOwner = safe.owners.filter((owner) => owner.address === userAddress).size === 1
txStatus = !userConfirmed && userIsSafeOwner ? 'awaiting_your_confirmation' : 'awaiting_confirmations'
}
if (tx.isSuccessful === false) {
txStatus = 'failed'
}
return txStatus
}
export const grantedSelector = createSelector(userAccountSelector, safeSelector, (userAccount, safe) => export const grantedSelector = createSelector(userAccountSelector, safeSelector, (userAccount, safe) =>
isUserOwner(safe, userAccount), isUserOwner(safe, userAccount),
@ -76,31 +44,3 @@ export const extendedSafeTokensSelector = createSelector(
return extendedTokens.toList() return extendedTokens.toList()
}, },
) )
export const extendedTransactionsSelector = createSelector(
safeSelector,
userAccountSelector,
safeTransactionsSelector,
safeCancellationTransactionsSelector,
safeIncomingTransactionsSelector,
(safe, userAddress, transactions, cancellationTransactions, incomingTransactions) => {
const cancellationTransactionsByNonce = cancellationTransactions.reduce((acc, tx) => acc.set(tx.nonce, tx), Map())
const extendedTransactions = transactions.map((tx) => {
let extendedTx = tx
if (!tx.isExecuted) {
if (
(cancellationTransactionsByNonce.get(tx.nonce) &&
cancellationTransactionsByNonce.get(tx.nonce).get('isExecuted')) ||
transactions.find((safeTx) => tx.nonce === safeTx.nonce && safeTx.isExecuted)
) {
extendedTx = tx.set('cancelled', true)
}
}
return extendedTx.set('status', getTxStatus(extendedTx, userAddress, safe))
})
return List([...extendedTransactions, ...incomingTransactions])
},
)

View File

@ -1,65 +1,10 @@
import { List, Map } from 'immutable' import { List } from 'immutable'
import { createSelector } from 'reselect' import { createSelector } from 'reselect'
import { userAccountSelector } from 'src/logic/wallets/store/selectors' import { safeIncomingTransactionsSelector, safeTransactionsSelector } from 'src/routes/safe/store/selectors'
import {
safeCancellationTransactionsSelector,
safeIncomingTransactionsSelector,
safeSelector,
safeTransactionsSelector,
} from 'src/routes/safe/store/selectors'
const getTxStatus = (tx: any, userAddress: string, safe): string => {
let txStatus
if (tx.executionTxHash) {
txStatus = 'success'
} else if (tx.cancelled) {
txStatus = 'cancelled'
} else if (tx.confirmations.size === safe.threshold) {
txStatus = 'awaiting_execution'
} else if (tx.creationTx) {
txStatus = 'success'
} else if (!tx.confirmations.size) {
txStatus = 'pending'
} else {
const userConfirmed = tx.confirmations.filter((conf) => conf.owner === userAddress).size === 1
const userIsSafeOwner = safe.owners.filter((owner) => owner.address === userAddress).size === 1
txStatus = !userConfirmed && userIsSafeOwner ? 'awaiting_your_confirmation' : 'awaiting_confirmations'
}
if (tx.isSuccessful === false) {
txStatus = 'failed'
}
return txStatus
}
export const extendedTransactionsSelector = createSelector( export const extendedTransactionsSelector = createSelector(
safeSelector,
userAccountSelector,
safeTransactionsSelector, safeTransactionsSelector,
safeCancellationTransactionsSelector,
safeIncomingTransactionsSelector, safeIncomingTransactionsSelector,
(safe, userAddress, transactions, cancellationTransactions, incomingTransactions) => { (transactions, incomingTransactions) => List([...transactions, ...incomingTransactions]),
const cancellationTransactionsByNonce = cancellationTransactions.reduce((acc, tx) => acc.set(tx.nonce, tx), Map())
const extendedTransactions = transactions.map((tx: any) =>
tx.withMutations((transaction) => {
if (!transaction.isExecuted) {
if (
(cancellationTransactionsByNonce.get(tx.nonce) &&
cancellationTransactionsByNonce.get(tx.nonce).get('isExecuted')) ||
transactions.find((safeTx) => tx.nonce === safeTx.nonce && safeTx.isExecuted)
) {
transaction.set('cancelled', true)
}
}
transaction.set('status', getTxStatus(transaction, userAddress, safe))
return transaction
}),
)
return List([...extendedTransactions, ...incomingTransactions])
},
) )