diff --git a/.gitignore b/.gitignore index 021f4668..a75dc3eb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules/ build_webpack/ build_storybook/ .DS_Store -build/ \ No newline at end of file +build/ +yarn-error.log \ No newline at end of file diff --git a/package.json b/package.json index 3b840efa..432464b2 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "bignumber.js": "9.0.0", "connected-react-router": "^6.3.1", "final-form": "4.16.1", + "date-fns": "^1.30.1", "history": "^4.7.2", "immortal-db": "^1.0.2", "immutable": "^4.0.0-rc.9", diff --git a/src/components/Footer/index.jsx b/src/components/Footer/index.jsx index 2c5882c5..8ab1c99d 100644 --- a/src/components/Footer/index.jsx +++ b/src/components/Footer/index.jsx @@ -9,7 +9,7 @@ import styles from './index.scss' const Footer = () => ( - Welcome + Add Safe Safe List diff --git a/src/components/Table/index.jsx b/src/components/Table/index.jsx index 0cb2219d..d61c1dac 100644 --- a/src/components/Table/index.jsx +++ b/src/components/Table/index.jsx @@ -2,12 +2,12 @@ import * as React from 'react' import classNames from 'classnames' import { List } from 'immutable' -import Row from '~/components/layout/Row' import Table from '@material-ui/core/Table' import TableBody from '@material-ui/core/TableBody' import { withStyles } from '@material-ui/core/styles' import CircularProgress from '@material-ui/core/CircularProgress' import TablePagination from '@material-ui/core/TablePagination' +import Row from '~/components/layout/Row' import { type Order, stableSort, getSorting } from '~/components/Table/sorting' import TableHead, { type Column } from '~/components/Table/TableHead' import { xl } from '~/theme/variables' @@ -21,6 +21,7 @@ type Props = { children: Function, size: number, defaultFixed?: boolean, + defaultOrder?: 'desc' | 'asc', } type State = { @@ -61,7 +62,7 @@ const FIXED_HEIGHT = 49 class GnoTable extends React.Component, State> { state = { page: 0, - order: 'asc', + order: undefined, orderBy: undefined, fixed: undefined, orderProp: false, @@ -70,9 +71,14 @@ class GnoTable extends React.Component, State> { onSort = (newOrderBy: string, orderProp: boolean) => { const { order, orderBy } = this.state + const { defaultOrder } = this.props let newOrder = 'desc' - if (orderBy === newOrderBy && order === 'desc') { + // if table was previously sorted by the user + if (order && orderBy === newOrderBy && order === 'desc') { + newOrder = 'asc' + } else if (!order && defaultOrder === 'desc') { + // if it was not sorted and defaultOrder is used newOrder = 'asc' } @@ -99,12 +105,13 @@ class GnoTable extends React.Component, State> { render() { const { - data, label, columns, classes, children, size, defaultOrderBy, defaultFixed, + data, label, columns, classes, children, size, defaultOrderBy, defaultOrder, defaultFixed, } = this.props const { order, orderBy, page, orderProp, rowsPerPage, fixed, } = this.state const orderByParam = orderBy || defaultOrderBy + const orderParam = order || defaultOrder const fixedParam = typeof fixed !== 'undefined' ? fixed : !!defaultFixed const backProps = { @@ -121,7 +128,7 @@ class GnoTable extends React.Component, State> { input: classes.white, } - const sortedData = stableSort(data, getSorting(order, orderByParam, orderProp), fixedParam).slice( + const sortedData = stableSort(data, getSorting(orderParam, orderByParam, orderProp), fixedParam).slice( page * rowsPerPage, page * rowsPerPage + rowsPerPage, ) @@ -133,7 +140,7 @@ class GnoTable extends React.Component, State> { {!isEmpty && ( - + {children(sortedData)}
)} @@ -159,4 +166,8 @@ class GnoTable extends React.Component, State> { } } +GnoTable.defaultProps = { + defaultOrder: 'asc', +} + export default withStyles(styles)(GnoTable) diff --git a/src/logic/safe/transactions/send.js b/src/logic/safe/transactions/send.js index c0a16a05..d1ec0b13 100644 --- a/src/logic/safe/transactions/send.js +++ b/src/logic/safe/transactions/send.js @@ -5,18 +5,60 @@ import { EMPTY_DATA } from '~/logic/wallets/ethTransactions' import { isEther } from '~/logic/tokens/utils/tokenHelpers' import { type Token } from '~/logic/tokens/store/model/token' import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' -import { saveTxToHistory } from '~/logic/safe/transactions' +import { type Operation, saveTxToHistory } from '~/logic/safe/transactions' import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses' export const CALL = 0 export const TX_TYPE_EXECUTION = 'execution' +export const TX_TYPE_CONFIRMATION = 'confirmation' + +export const approveTransaction = async ( + safeInstance: any, + to: string, + valueInWei: number | string, + data: string, + operation: Operation, + nonce: number, + sender: string, +) => { + const contractTxHash = await safeInstance.getTransactionHash( + to, + valueInWei, + data, + operation, + 0, + 0, + 0, + ZERO_ADDRESS, + ZERO_ADDRESS, + nonce, + { + from: sender, + }, + ) + const receipt = await safeInstance.approveHash(contractTxHash, { from: sender }) + + await saveTxToHistory( + safeInstance, + to, + valueInWei, + data, + operation, + nonce, + receipt.tx, // tx hash, + sender, + TX_TYPE_CONFIRMATION, + ) + + return receipt +} export const executeTransaction = async ( safeInstance: any, to: string, valueInWei: number | string, data: string, - operation: number | string, + operation: Operation, nonce: string | number, sender: string, ) => { @@ -66,7 +108,7 @@ export const createTransaction = async (safeAddress: string, to: string, valueIn const web3 = getWeb3() const from = web3.currentProvider.selectedAddress const threshold = await safeInstance.getThreshold() - const nonce = await safeInstance.nonce() + const nonce = (await safeInstance.nonce()).toString() const valueInWei = web3.utils.toWei(valueInEth, 'ether') const isExecution = threshold.toNumber() === 1 diff --git a/src/logic/safe/transactions/txHistory.js b/src/logic/safe/transactions/txHistory.js index d1fc04e3..d29e69c4 100644 --- a/src/logic/safe/transactions/txHistory.js +++ b/src/logic/safe/transactions/txHistory.js @@ -10,13 +10,13 @@ export type Operation = 0 | 1 | 2 const calculateBodyFrom = async ( safeInstance: any, to: string, - valueInWei: number, + valueInWei: number | string, data: string, operation: Operation, nonce: string | number, transactionHash: string, sender: string, - type: TxServiceType, + confirmationType: TxServiceType, ) => { const contractTransactionHash = await safeInstance.getTransactionHash( to, @@ -38,14 +38,14 @@ const calculateBodyFrom = async ( operation, nonce, safeTxGas: 0, - dataGas: 0, + baseGas: 0, gasPrice: 0, gasToken: ZERO_ADDRESS, refundReceiver: ZERO_ADDRESS, contractTransactionHash, transactionHash, sender: getWeb3().utils.toChecksumAddress(sender), - type, + confirmationType, } } @@ -59,7 +59,7 @@ export const buildTxServiceUrl = (safeAddress: string) => { export const saveTxToHistory = async ( safeInstance: any, to: string, - valueInWei: number, + valueInWei: number | string, data: string, operation: Operation, nonce: number | string, diff --git a/src/logic/tokens/utils/tokenHelpers.js b/src/logic/tokens/utils/tokenHelpers.js index a7aecd2f..9d714005 100644 --- a/src/logic/tokens/utils/tokenHelpers.js +++ b/src/logic/tokens/utils/tokenHelpers.js @@ -1,7 +1,8 @@ // @flow import { List } from 'immutable' -import logo from '~/assets/icons/icon_etherTokens.svg' +import { getWeb3 } from '~/logic/wallets/getWeb3' import { makeToken, type Token } from '~/logic/tokens/store/model/token' +import logo from '~/assets/icons/icon_etherTokens.svg' export const ETH_ADDRESS = '0x000' export const isEther = (symbol: string) => symbol === 'ETH' @@ -31,3 +32,19 @@ export const calculateActiveErc20TokensFrom = (tokens: List) => { return activeTokens } + +export const isAddressAToken = async (tokenAddress: string) => { + // SECOND APPROACH: + // They both seem to work the same + // const tokenContract = await getStandardTokenContract() + // try { + // await tokenContract.at(tokenAddress) + // } catch { + // return 'Not a token address' + // } + + const web3 = getWeb3() + const call = await web3.eth.call({ to: tokenAddress, data: web3.utils.sha3('totalSupply()') }) + + return call !== '0x' +} diff --git a/src/routes/safe/components/Balances/Tokens/screens/AddCustomToken/validators.js b/src/routes/safe/components/Balances/Tokens/screens/AddCustomToken/validators.js index 07feaadb..56478291 100644 --- a/src/routes/safe/components/Balances/Tokens/screens/AddCustomToken/validators.js +++ b/src/routes/safe/components/Balances/Tokens/screens/AddCustomToken/validators.js @@ -1,9 +1,8 @@ // @flow import { List } from 'immutable' -import { getWeb3 } from '~/logic/wallets/getWeb3' import { type Token } from '~/logic/tokens/store/model/token' import { sameAddress } from '~/logic/wallets/ethAddresses' -// import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens' +import { isAddressAToken } from '~/logic/tokens/utils/tokenHelpers' export const simpleMemoize = (fn: Function) => { let lastArg @@ -28,10 +27,9 @@ export const addressIsTokenContract = simpleMemoize(async (tokenAddress: string) // return 'Not a token address' // } - const web3 = getWeb3() - const call = await web3.eth.call({ to: tokenAddress, data: web3.utils.sha3('totalSupply()') }) + const isToken = await isAddressAToken(tokenAddress) - if (call === '0x') { + if (!isToken) { return 'Not a token address' } }) diff --git a/src/routes/safe/components/Balances/dataFetcher.js b/src/routes/safe/components/Balances/dataFetcher.js index a601c553..568a804a 100644 --- a/src/routes/safe/components/Balances/dataFetcher.js +++ b/src/routes/safe/components/Balances/dataFetcher.js @@ -52,6 +52,7 @@ export const generateColumns = () => { disablePadding: false, label: '', custom: true, + static: true, } return List([assetColumn, balanceColumn, actions]) diff --git a/src/routes/safe/components/Layout.jsx b/src/routes/safe/components/Layout.jsx index 7b1640e8..37b5c09c 100644 --- a/src/routes/safe/components/Layout.jsx +++ b/src/routes/safe/components/Layout.jsx @@ -101,6 +101,7 @@ class Layout extends React.Component { createTransaction, fetchTransactions, updateSafe, + transactions, } = this.props const { tabIndex } = this.state @@ -152,7 +153,15 @@ class Layout extends React.Component { createTransaction={createTransaction} /> )} - {tabIndex === 1 && } + {tabIndex === 1 && ( + + )} {tabIndex === 2 && ( , +} + +const ExpandedTx = ({ + classes, tx, threshold, owners, +}: Props) => { + const [tabIndex, setTabIndex] = useState(0) + const confirmedLabel = `Confirmed [${tx.confirmations.size}/${threshold}]` + const unconfirmedLabel = `Unconfirmed [${owners.size - tx.confirmations.size}]` + + const handleTabChange = (event, tabClicked) => { + setTabIndex(tabClicked) + } + + return ( + + + + + + TX hash: + n/a + + + TX status: + n/a + + + TX created: + {formatDate(tx.submissionDate)} + + {tx.executionDate && ( + + TX executed: + {formatDate(tx.executionDate)} + + )} + + + + + Send 1.00 ETH to: +
+ {tx.recipient} +
+
+ + + + + + + + + +
+
+ ) +} + +export default withStyles(styles)(ExpandedTx) diff --git a/src/routes/safe/components/TransactionsNew/TxsTable/ExpandedTx/style.js b/src/routes/safe/components/TransactionsNew/TxsTable/ExpandedTx/style.js new file mode 100644 index 00000000..c59b9fea --- /dev/null +++ b/src/routes/safe/components/TransactionsNew/TxsTable/ExpandedTx/style.js @@ -0,0 +1,12 @@ +// @flow +import { md, lg } from '~/theme/variables' + +export const styles = () => ({ + txDataContainer: { + padding: `${lg} ${md}`, + }, + rightCol: { + boxSizing: 'border-box', + borderLeft: 'solid 1px #c8ced4', + }, +}) diff --git a/src/routes/safe/components/TransactionsNew/TxsTable/Status/assets/awaiting.svg b/src/routes/safe/components/TransactionsNew/TxsTable/Status/assets/awaiting.svg new file mode 100644 index 00000000..a8dedcd6 --- /dev/null +++ b/src/routes/safe/components/TransactionsNew/TxsTable/Status/assets/awaiting.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/routes/safe/components/TransactionsNew/TxsTable/Status/assets/error.svg b/src/routes/safe/components/TransactionsNew/TxsTable/Status/assets/error.svg new file mode 100644 index 00000000..bb6dbf77 --- /dev/null +++ b/src/routes/safe/components/TransactionsNew/TxsTable/Status/assets/error.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/routes/safe/components/TransactionsNew/TxsTable/Status/assets/ok.svg b/src/routes/safe/components/TransactionsNew/TxsTable/Status/assets/ok.svg new file mode 100644 index 00000000..cb8f6656 --- /dev/null +++ b/src/routes/safe/components/TransactionsNew/TxsTable/Status/assets/ok.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/routes/safe/components/TransactionsNew/TxsTable/Status/index.jsx b/src/routes/safe/components/TransactionsNew/TxsTable/Status/index.jsx new file mode 100644 index 00000000..b183a7d3 --- /dev/null +++ b/src/routes/safe/components/TransactionsNew/TxsTable/Status/index.jsx @@ -0,0 +1,35 @@ +// @flow +import * as React from 'react' +import { withStyles } from '@material-ui/core/styles' +import Block from '~/components/layout/Block' +import Paragraph from '~/components/layout/Paragraph/' +import Img from '~/components/layout/Img' +import ErrorIcon from './assets/error.svg' +import OkIcon from './assets/ok.svg' +import AwaitingIcon from './assets/awaiting.svg' +import { styles } from './style' + +type Props = { + classes: Object, + status: 'pending' | 'awaiting' | 'success' | 'failed', +} + +const statusToIcon = { + success: OkIcon, + failed: ErrorIcon, + awaiting: AwaitingIcon, +} + +const statusIconStyle = { + height: '14px', + width: '14px', +} + +const Status = ({ classes, status }: Props) => ( + + OK Icon + {status} + +) + +export default withStyles(styles)(Status) diff --git a/src/routes/safe/components/TransactionsNew/TxsTable/Status/style.js b/src/routes/safe/components/TransactionsNew/TxsTable/Status/style.js new file mode 100644 index 00000000..cd0e65d5 --- /dev/null +++ b/src/routes/safe/components/TransactionsNew/TxsTable/Status/style.js @@ -0,0 +1,30 @@ +// @flow +import { smallFontSize, boldFont, sm } from '~/theme/variables' + +export const styles = () => ({ + container: { + display: 'flex', + fontSize: smallFontSize, + fontWeight: boldFont, + width: '100px', + padding: sm, + alignItems: 'center', + boxSizing: 'border-box', + }, + success: { + backgroundColor: '#d7f3f3', + color: '#346d6d', + }, + failed: { + backgroundColor: 'transparent', + color: '#fd7890', + }, + awaiting: { + backgroundColor: '#dfebff', + color: '#2e73d9', + }, + statusText: { + marginLeft: 'auto', + textTransform: 'uppercase', + }, +}) diff --git a/src/routes/safe/components/TransactionsNew/TxsTable/columns.js b/src/routes/safe/components/TransactionsNew/TxsTable/columns.js new file mode 100644 index 00000000..2343322c --- /dev/null +++ b/src/routes/safe/components/TransactionsNew/TxsTable/columns.js @@ -0,0 +1,101 @@ +// @flow +import { format } from 'date-fns' +import { List } from 'immutable' +import { type Transaction } from '~/routes/safe/store/models/transaction' +import { type SortRow } from '~/components/Table/sorting' +import { type Column } from '~/components/Table/TableHead' +import { getWeb3 } from '~/logic/wallets/getWeb3' + +export const TX_TABLE_NONCE_ID = 'nonce' +export const TX_TABLE_TYPE_ID = 'type' +export const TX_TABLE_DATE_ID = 'date' +export const TX_TABLE_AMOUNT_ID = 'amount' +export const TX_TABLE_STATUS_ID = 'status' +export const TX_TABLE_RAW_TX_ID = 'tx' +export const TX_TABLE_EXPAND_ICON = 'expand' + +const web3 = getWeb3() +const { toBN, fromWei } = web3.utils + +type TxData = { + nonce: number, + type: string, + date: string, + amount: number | string, + status: string, + tx: Transaction, +} + +export const formatDate = (date: Date): string => format(date, 'MMM D, YYYY - h:m:s') + +export type TransactionRow = SortRow + +export const getTxTableData = (transactions: List): List => { + const rows = transactions.map((tx: Transaction) => ({ + [TX_TABLE_NONCE_ID]: tx.nonce, + [TX_TABLE_TYPE_ID]: 'Outgoing transfer', + [TX_TABLE_DATE_ID]: formatDate(tx.isExecuted ? tx.executionDate : tx.submissionDate), + [TX_TABLE_AMOUNT_ID]: Number(tx.value) > 0 ? `${fromWei(toBN(tx.value), 'ether')} ${tx.symbol}` : 'n/a', + [TX_TABLE_STATUS_ID]: tx.isExecuted ? 'success' : 'awaiting', + [TX_TABLE_RAW_TX_ID]: tx, + })) + + return rows +} + +export const generateColumns = () => { + const nonceColumn: Column = { + id: TX_TABLE_NONCE_ID, + disablePadding: false, + label: 'Nonce', + custom: false, + order: false, + width: 50, + } + + const typeColumn: Column = { + id: TX_TABLE_TYPE_ID, + order: false, + disablePadding: false, + label: 'Type', + custom: false, + width: 150, + } + + const valueColumn: Column = { + id: TX_TABLE_AMOUNT_ID, + order: false, + disablePadding: false, + label: 'Amount', + custom: false, + width: 100, + } + + const dateColumn: Column = { + id: TX_TABLE_DATE_ID, + order: false, + disablePadding: false, + label: 'Date', + custom: false, + } + + const statusColumn: Column = { + id: TX_TABLE_STATUS_ID, + order: false, + disablePadding: false, + label: 'Status', + custom: true, + } + + const expandIconColumn: Column = { + id: TX_TABLE_EXPAND_ICON, + order: false, + disablePadding: true, + label: '', + custom: true, + width: 50, + static: true, + } + + return List([nonceColumn, typeColumn, valueColumn, dateColumn, statusColumn, expandIconColumn]) +} diff --git a/src/routes/safe/components/TransactionsNew/TxsTable/index.jsx b/src/routes/safe/components/TransactionsNew/TxsTable/index.jsx new file mode 100644 index 00000000..1136952f --- /dev/null +++ b/src/routes/safe/components/TransactionsNew/TxsTable/index.jsx @@ -0,0 +1,114 @@ +// @flow +import React, { useState } from 'react' +import cn from 'classnames' +import { List } from 'immutable' +import Collapse from '@material-ui/core/Collapse' +import IconButton from '@material-ui/core/IconButton' +import ExpandLess from '@material-ui/icons/ExpandLess' +import ExpandMore from '@material-ui/icons/ExpandMore' +import TableRow from '@material-ui/core/TableRow' +import TableCell from '@material-ui/core/TableCell' +import { withStyles } from '@material-ui/core/styles' +import Block from '~/components/layout/Block' +import Row from '~/components/layout/Row' +import { type Column, cellWidth } from '~/components/Table/TableHead' +import Table from '~/components/Table' +import { type Transaction } from '~/routes/safe/store/models/transaction' +import { type Owner } from '~/routes/safe/store/models/owner' +import ExpandedTxComponent from './ExpandedTx' +import { + getTxTableData, generateColumns, TX_TABLE_NONCE_ID, type TransactionRow, TX_TABLE_RAW_TX_ID, +} from './columns' +import { styles } from './style' +import Status from './Status' + +const expandCellStyle = { + paddingLeft: 0, + paddingRight: 0, +} + +type Props = { + classes: Object, + transactions: List, + threshold: number, + owners: List, +} + +const TxsTable = (props: Props) => { + const { + classes, transactions, threshold, owners, + } = props + const [expandedTx, setExpandedTx] = useState(null) + + const handleTxExpand = (nonce) => { + setExpandedTx(prevTx => (prevTx === nonce ? null : nonce)) + } + + const columns = generateColumns() + const autoColumns = columns.filter(c => !c.custom) + const filteredData = getTxTableData(transactions) + + return ( + + + {(sortedData: Array) => sortedData.map((row: any, index: number) => ( + + handleTxExpand(row.nonce)} + > + {autoColumns.map((column: Column) => ( + + {row[column.id]} + + ))} + + + + + + + {expandedTx === row.nonce ? : } + + + + + + + + + )) + } +
+
+ ) +} + +export default withStyles(styles)(TxsTable) diff --git a/src/routes/safe/components/TransactionsNew/TxsTable/style.js b/src/routes/safe/components/TransactionsNew/TxsTable/style.js new file mode 100644 index 00000000..c5d5ddbc --- /dev/null +++ b/src/routes/safe/components/TransactionsNew/TxsTable/style.js @@ -0,0 +1,21 @@ +// @flow +import { lg } from '~/theme/variables' + +export const styles = () => ({ + container: { + marginTop: lg, + }, + row: { + cursor: 'pointer', + '&:hover': { + backgroundColor: '#fff3e2', + }, + }, + expandedRow: { + backgroundColor: '#fff3e2', + }, + extendedTxContainer: { + padding: 0, + backgroundColor: '#fffaf4', + }, +}) diff --git a/src/routes/safe/components/TransactionsNew/index.jsx b/src/routes/safe/components/TransactionsNew/index.jsx index bb5b1bc9..de6addb0 100644 --- a/src/routes/safe/components/TransactionsNew/index.jsx +++ b/src/routes/safe/components/TransactionsNew/index.jsx @@ -1,11 +1,17 @@ // @flow import * as React from 'react' import { List } from 'immutable' -import NoTransactions from '~/routes/safe/components/Transactions/NoTransactions' +import NoTransactions from '~/routes/safe/components/TransactionsNew/NoTransactions' +import TxsTable from '~/routes/safe/components/TransactionsNew/TxsTable' +import { type Transaction } from '~/routes/safe/store/models/transaction' +import { type Owner } from '~/routes/safe/store/models/owner' type Props = { safeAddress: string, threshold: number, + fetchTransactions: Function, + transactions: List, + owners: List, } class Transactions extends React.Component { @@ -16,11 +22,23 @@ class Transactions extends React.Component { } render() { - const { transactions = List(), safeName, threshold } = this.props - const hasTransactions = false + const { transactions, owners, threshold } = this.props + const hasTransactions = transactions.size > 0 - return {hasTransactions ?
: } + return ( + + {hasTransactions ? ( + + ) : ( + + )} + + ) } } +Transactions.defaultProps = { + transactions: List(), +} + export default Transactions diff --git a/src/routes/safe/container/actions.js b/src/routes/safe/container/actions.js index bbd91b82..6638a28d 100644 --- a/src/routes/safe/container/actions.js +++ b/src/routes/safe/container/actions.js @@ -4,6 +4,7 @@ import fetchTokenBalances from '~/routes/safe/store/actions/fetchTokenBalances' import createTransaction from '~/routes/safe/store/actions/createTransaction' import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions' import updateSafe from '~/routes/safe/store/actions/updateSafe' +import fetchTokens from '~/logic/tokens/store/actions/fetchTokens' export type Actions = { fetchSafe: typeof fetchSafe, @@ -11,12 +12,14 @@ export type Actions = { createTransaction: typeof createTransaction, fetchTransactions: typeof fetchTransactions, updateSafe: typeof updateSafe, + fetchTokens: typeof fetchTokens, } export default { fetchSafe, fetchTokenBalances, createTransaction, + fetchTokens, fetchTransactions, updateSafe, } diff --git a/src/routes/safe/container/index.jsx b/src/routes/safe/container/index.jsx index a3058012..6d4de495 100644 --- a/src/routes/safe/container/index.jsx +++ b/src/routes/safe/container/index.jsx @@ -16,11 +16,13 @@ const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 5000 class SafeView extends React.Component { componentDidMount() { const { - fetchSafe, activeTokens, safeUrl, fetchTokenBalances, + fetchSafe, activeTokens, safeUrl, fetchTokenBalances, fetchTokens, } = this.props fetchSafe(safeUrl) fetchTokenBalances(safeUrl, activeTokens) + // fetch tokens there to get symbols for tokens in TXs list + fetchTokens() this.intervalId = setInterval(() => { this.checkForUpdates() @@ -63,6 +65,7 @@ class SafeView extends React.Component { createTransaction, fetchTransactions, updateSafe, + transactions, } = this.props return ( @@ -78,6 +81,7 @@ class SafeView extends React.Component { createTransaction={createTransaction} fetchTransactions={fetchTransactions} updateSafe={updateSafe} + transactions={transactions} /> ) diff --git a/src/routes/safe/container/selector.js b/src/routes/safe/container/selector.js index 14db2918..4e90cac7 100644 --- a/src/routes/safe/container/selector.js +++ b/src/routes/safe/container/selector.js @@ -13,8 +13,10 @@ import { type Safe } from '~/routes/safe/store/models/safe' import { type Owner } from '~/routes/safe/store/models/owner' import { type GlobalState } from '~/store' import { sameAddress } from '~/logic/wallets/ethAddresses' +import { safeTransactionsSelector } from '~/routes/safe/store/selectors/index' import { orderedTokenListSelector, tokensSelector } from '~/logic/tokens/store/selectors' import { type Token } from '~/logic/tokens/store/model/token' +import { type Transaction } from '~/routes/safe/store/models/transaction' import { type TokenBalance } from '~/routes/safe/store/models/tokenBalance' import { safeParamAddressSelector } from '../store/selectors' import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers' @@ -27,6 +29,7 @@ export type SelectorProps = { userAddress: string, network: string, safeUrl: string, + transactions: List, } export const grantedSelector: Selector = createSelector( @@ -95,4 +98,5 @@ export default createStructuredSelector({ userAddress: userAccountSelector, network: networkSelector, safeUrl: safeParamAddressSelector, + transactions: safeTransactionsSelector, }) diff --git a/src/routes/safe/store/actions/createTransaction.js b/src/routes/safe/store/actions/createTransaction.js index ecc4634e..3fd402f9 100644 --- a/src/routes/safe/store/actions/createTransaction.js +++ b/src/routes/safe/store/actions/createTransaction.js @@ -1,11 +1,10 @@ // @flow import type { Dispatch as ReduxDispatch, GetState } from 'redux' -import { createAction } from 'redux-actions' import { EMPTY_DATA } from '~/logic/wallets/ethTransactions' import { userAccountSelector } from '~/logic/wallets/store/selectors' import { type GlobalState } from '~/store' import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' -import { executeTransaction, CALL } from '~/logic/safe/transactions' +import { approveTransaction, executeTransaction, CALL } from '~/logic/safe/transactions' const createTransaction = ( safeAddress: string, @@ -19,7 +18,7 @@ const createTransaction = ( const safeInstance = await getGnosisSafeInstanceAt(safeAddress) const from = userAccountSelector(state) const threshold = await safeInstance.getThreshold() - const nonce = await safeInstance.nonce() + const nonce = (await safeInstance.nonce()).toString() const isExecution = threshold.toNumber() === 1 let txHash @@ -28,7 +27,7 @@ const createTransaction = ( txHash = await executeTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from) openSnackbar('Transaction has been confirmed', 'success') } else { - // txHash = await approveTransaction(safeAddress, to, valueInWei, txData, CALL, nonce) + txHash = await approveTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from) } // dispatch(addTransactions(txHash)) diff --git a/src/routes/safe/store/actions/fetchTransactions.js b/src/routes/safe/store/actions/fetchTransactions.js index d50f31ef..0a0858c2 100644 --- a/src/routes/safe/store/actions/fetchTransactions.js +++ b/src/routes/safe/store/actions/fetchTransactions.js @@ -11,11 +11,13 @@ import { buildTxServiceUrl, type TxServiceType } from '~/logic/safe/transactions import { getOwners } from '~/logic/safe/utils' import { EMPTY_DATA } from '~/logic/wallets/ethTransactions' import { addTransactions } from './addTransactions' +import { getHumanFriendlyToken } from '~/logic/tokens/store/actions/fetchTokens' +import { isAddressAToken } from '~/logic/tokens/utils/tokenHelpers' type ConfirmationServiceModel = { owner: string, submissionDate: Date, - type: string, + confirmationType: string, transactionHash: string, } @@ -40,20 +42,31 @@ const buildTransactionFrom = async (safeAddress: string, tx: TxServiceModel, saf return makeConfirmation({ owner: makeOwner({ address: conf.owner, name: ownerName }), - type: ((conf.type.toLowerCase(): any): TxServiceType), + type: ((conf.confirmationType.toLowerCase(): any): TxServiceType), hash: conf.transactionHash, }) }), ) + const isToken = await isAddressAToken(tx.to) + + let symbol = 'ETH' + if (isToken) { + const tokenContract = await getHumanFriendlyToken() + const tokenInstance = await tokenContract.at(tx.to) + symbol = await tokenInstance.symbol() + } return makeTransaction({ name, + symbol, nonce: tx.nonce, value: Number(tx.value), confirmations, recipient: tx.to, data: tx.data ? tx.data : EMPTY_DATA, isExecuted: tx.isExecuted, + submissionDate: tx.submissionDate, + executionDate: tx.executionDate, }) } @@ -62,8 +75,8 @@ export const loadSafeTransactions = async (safeAddress: string) => { const response = await axios.get(url) const transactions: TxServiceModel[] = response.data.results const safeSubjects = loadSafeSubjects(safeAddress) - const txsRecord = transactions.map( - async (tx: TxServiceModel) => await buildTransactionFrom(safeAddress, tx, safeSubjects), + const txsRecord = await Promise.all( + transactions.map((tx: TxServiceModel) => buildTransactionFrom(safeAddress, tx, safeSubjects)), ) return Map().set(safeAddress, List(txsRecord)) diff --git a/src/routes/safe/store/models/transaction.js b/src/routes/safe/store/models/transaction.js index 76e54419..ccb84dd3 100644 --- a/src/routes/safe/store/models/transaction.js +++ b/src/routes/safe/store/models/transaction.js @@ -11,6 +11,9 @@ export type TransactionProps = { recipient: string, data: string, isExecuted: boolean, + submissionDate: Date, + executionDate: Date, + symbol: string, } export const makeTransaction: RecordFactory = Record({ @@ -21,6 +24,9 @@ export const makeTransaction: RecordFactory = Record({ recipient: '', data: '', isExecuted: false, + submissionDate: '', + executionDate: '', + symbol: '', }) export type Transaction = RecordOf diff --git a/src/routes/safe/store/selectors/index.js b/src/routes/safe/store/selectors/index.js index 948a0d06..f194061c 100644 --- a/src/routes/safe/store/selectors/index.js +++ b/src/routes/safe/store/selectors/index.js @@ -23,17 +23,15 @@ type TransactionProps = { transaction: Transaction, } -const safePropAddressSelector = (state: GlobalState, props: SafeProps) => props.safeAddress - const transactionsSelector = (state: GlobalState): TransactionsState => state[TRANSACTIONS_REDUCER_ID] const oneTransactionSelector = (state: GlobalState, props: TransactionProps) => props.transaction export const safeParamAddressSelector = (state: GlobalState, props: RouterProps) => props.match.params[SAFE_PARAM_ADDRESS] || '' -export const safeTransactionsSelector: Selector> = createSelector( +export const safeTransactionsSelector: Selector> = createSelector( transactionsSelector, - safePropAddressSelector, + safeParamAddressSelector, (transactions: TransactionsState, address: string): List => { if (!transactions) { return List([]) diff --git a/yarn.lock b/yarn.lock index 39959f66..49b05f4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5727,6 +5727,11 @@ data-urls@^1.0.0: whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" +date-fns@^1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" + integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== + date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"