pull
This commit is contained in:
commit
9a70737fe5
|
@ -3,3 +3,4 @@ build_webpack/
|
|||
build_storybook/
|
||||
.DS_Store
|
||||
build/
|
||||
yarn-error.log
|
|
@ -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",
|
||||
|
|
|
@ -9,7 +9,7 @@ import styles from './index.scss'
|
|||
const Footer = () => (
|
||||
<Block className={styles.footer}>
|
||||
<Link to={WELCOME_ADDRESS}>
|
||||
<Paragraph size="sm" color="primary" noMargin>Welcome</Paragraph>
|
||||
<Paragraph size="sm" color="primary" noMargin>Add Safe</Paragraph>
|
||||
</Link>
|
||||
<Link to={SAFELIST_ADDRESS}>
|
||||
<Paragraph size="sm" color="primary" noMargin>Safe List</Paragraph>
|
||||
|
|
|
@ -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<K> = {
|
|||
children: Function,
|
||||
size: number,
|
||||
defaultFixed?: boolean,
|
||||
defaultOrder?: 'desc' | 'asc',
|
||||
}
|
||||
|
||||
type State = {
|
||||
|
@ -61,7 +62,7 @@ const FIXED_HEIGHT = 49
|
|||
class GnoTable<K> extends React.Component<Props<K>, State> {
|
||||
state = {
|
||||
page: 0,
|
||||
order: 'asc',
|
||||
order: undefined,
|
||||
orderBy: undefined,
|
||||
fixed: undefined,
|
||||
orderProp: false,
|
||||
|
@ -70,9 +71,14 @@ class GnoTable<K> extends React.Component<Props<K>, 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<K> extends React.Component<Props<K>, 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<K> extends React.Component<Props<K>, 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<K> extends React.Component<Props<K>, State> {
|
|||
<React.Fragment>
|
||||
{!isEmpty && (
|
||||
<Table aria-labelledby={label} className={classes.root}>
|
||||
<TableHead columns={columns} order={order} orderBy={orderByParam} onSort={this.onSort} />
|
||||
<TableHead columns={columns} order={orderParam} orderBy={orderByParam} onSort={this.onSort} />
|
||||
<TableBody>{children(sortedData)}</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
|
@ -159,4 +166,8 @@ class GnoTable<K> extends React.Component<Props<K>, State> {
|
|||
}
|
||||
}
|
||||
|
||||
GnoTable.defaultProps = {
|
||||
defaultOrder: 'asc',
|
||||
}
|
||||
|
||||
export default withStyles(styles)(GnoTable)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<Token>) => {
|
|||
|
||||
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'
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
})
|
||||
|
|
|
@ -52,6 +52,7 @@ export const generateColumns = () => {
|
|||
disablePadding: false,
|
||||
label: '',
|
||||
custom: true,
|
||||
static: true,
|
||||
}
|
||||
|
||||
return List([assetColumn, balanceColumn, actions])
|
||||
|
|
|
@ -101,6 +101,7 @@ class Layout extends React.Component<Props, State> {
|
|||
createTransaction,
|
||||
fetchTransactions,
|
||||
updateSafe,
|
||||
transactions,
|
||||
} = this.props
|
||||
const { tabIndex } = this.state
|
||||
|
||||
|
@ -152,7 +153,15 @@ class Layout extends React.Component<Props, State> {
|
|||
createTransaction={createTransaction}
|
||||
/>
|
||||
)}
|
||||
{tabIndex === 1 && <Transactions fetchTransactions={fetchTransactions} safeAddress={address} />}
|
||||
{tabIndex === 1 && (
|
||||
<Transactions
|
||||
threshold={safe.threshold}
|
||||
owners={safe.owners}
|
||||
transactions={transactions}
|
||||
fetchTransactions={fetchTransactions}
|
||||
safeAddress={address}
|
||||
/>
|
||||
)}
|
||||
{tabIndex === 2 && (
|
||||
<Settings
|
||||
granted={granted}
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
// @flow
|
||||
import React, { useState } from 'react'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import Tabs from '@material-ui/core/Tabs'
|
||||
import Tab from '@material-ui/core/Tab'
|
||||
import Row from '~/components/layout/Row'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Bold from '~/components/layout/Bold'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Hairline from '~/components/layout/Hairline'
|
||||
import { type Transaction } from '~/routes/safe/store/models/transaction'
|
||||
import { type Owner } from '~/routes/safe/store/models/owner'
|
||||
import { styles } from './style'
|
||||
import { formatDate } from '../columns'
|
||||
|
||||
type Props = {
|
||||
classes: Object,
|
||||
tx: Transaction,
|
||||
threshold: number,
|
||||
owners: List<Owner>,
|
||||
}
|
||||
|
||||
const ExpandedTx = ({
|
||||
classes, tx, threshold, owners,
|
||||
}: Props) => {
|
||||
const [tabIndex, setTabIndex] = useState<number>(0)
|
||||
const confirmedLabel = `Confirmed [${tx.confirmations.size}/${threshold}]`
|
||||
const unconfirmedLabel = `Unconfirmed [${owners.size - tx.confirmations.size}]`
|
||||
|
||||
const handleTabChange = (event, tabClicked) => {
|
||||
setTabIndex(tabClicked)
|
||||
}
|
||||
|
||||
return (
|
||||
<Block>
|
||||
<Row>
|
||||
<Col xs={6} layout="column">
|
||||
<Block className={classes.txDataContainer}>
|
||||
<Paragraph noMargin>
|
||||
<Bold>TX hash: </Bold>
|
||||
n/a
|
||||
</Paragraph>
|
||||
<Paragraph noMargin>
|
||||
<Bold>TX status: </Bold>
|
||||
n/a
|
||||
</Paragraph>
|
||||
<Paragraph noMargin>
|
||||
<Bold>TX created: </Bold>
|
||||
{formatDate(tx.submissionDate)}
|
||||
</Paragraph>
|
||||
{tx.executionDate && (
|
||||
<Paragraph noMargin>
|
||||
<Bold>TX executed: </Bold>
|
||||
{formatDate(tx.executionDate)}
|
||||
</Paragraph>
|
||||
)}
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Block className={classes.txDataContainer}>
|
||||
<Paragraph noMargin>
|
||||
<Bold>Send 1.00 ETH to:</Bold>
|
||||
<br />
|
||||
{tx.recipient}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
</Col>
|
||||
<Col xs={6} className={classes.rightCol}>
|
||||
<Row>
|
||||
<Tabs value={tabIndex} onChange={handleTabChange} indicatorColor="secondary" textColor="secondary">
|
||||
<Tab label={confirmedLabel} />
|
||||
<Tab label={unconfirmedLabel} />
|
||||
</Tabs>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
export default withStyles(styles)(ExpandedTx)
|
|
@ -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',
|
||||
},
|
||||
})
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="13" viewBox="0 0 14 13">
|
||||
<path fill="#2E73D9" fill-rule="nonzero" d="M2.332 0C1.412 0 .666.746.666 1.666v.333A.666.666 0 0 0 0 2.665V5.33c0 .368.298.666.666.666h3.332a.666.666 0 0 0 .666-.666V2.665A.666.666 0 0 0 3.998 2v-.333C3.998.746 3.252 0 2.332 0zm0 .666a1 1 0 0 1 1 1v.333h-2v-.333a1 1 0 0 1 1-1zm9.434 0a.634.634 0 0 0-.46.187l-1.225 1.232 2.498 2.499 1.226-1.233a.655.655 0 0 0 0-.932L12.246.853a.703.703 0 0 0-.48-.187zM9.368 2.792l-7.37 7.369v2.498h2.5l7.368-7.369-2.498-2.498z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 562 B |
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
|
||||
<path fill="#FD7890" fill-rule="nonzero" d="M7.7 7.7H6.3V3.5h1.4v4.2zm0 2.8H6.3V9.1h1.4v1.4zM7 0a7 7 0 1 0 0 14A7 7 0 0 0 7 0z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 225 B |
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
|
||||
<path fill="#346D6D" fill-rule="nonzero" d="M7 0a7 7 0 1 1 0 14A7 7 0 0 1 7 0zm-.913 9.8L11.2 5.139 10.17 4.2 6.087 7.916 3.83 5.865l-1.03.939L6.087 9.8z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 252 B |
|
@ -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) => (
|
||||
<Block className={`${classes.container} ${classes[status]}`}>
|
||||
<Img src={statusToIcon[status]} alt="OK Icon" style={statusIconStyle} />
|
||||
<Paragraph noMargin className={classes.statusText}>{status}</Paragraph>
|
||||
</Block>
|
||||
)
|
||||
|
||||
export default withStyles(styles)(Status)
|
|
@ -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',
|
||||
},
|
||||
})
|
|
@ -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<TxData>
|
||||
|
||||
export const getTxTableData = (transactions: List<Transaction>): List<TransactionRow> => {
|
||||
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])
|
||||
}
|
|
@ -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<Transaction>,
|
||||
threshold: number,
|
||||
owners: List<Owner>,
|
||||
}
|
||||
|
||||
const TxsTable = (props: Props) => {
|
||||
const {
|
||||
classes, transactions, threshold, owners,
|
||||
} = props
|
||||
const [expandedTx, setExpandedTx] = useState<string | null>(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 (
|
||||
<Block className={classes.container}>
|
||||
<Table
|
||||
label="Transactions"
|
||||
defaultOrderBy={TX_TABLE_NONCE_ID}
|
||||
defaultOrder="desc"
|
||||
columns={columns}
|
||||
data={filteredData}
|
||||
size={filteredData.size}
|
||||
defaultFixed
|
||||
>
|
||||
{(sortedData: Array<TransactionRow>) => sortedData.map((row: any, index: number) => (
|
||||
<React.Fragment key={index}>
|
||||
<TableRow
|
||||
tabIndex={-1}
|
||||
className={cn(classes.row, expandedTx === row.nonce && classes.expandedRow)}
|
||||
onClick={() => handleTxExpand(row.nonce)}
|
||||
>
|
||||
{autoColumns.map((column: Column) => (
|
||||
<TableCell
|
||||
key={column.id}
|
||||
className={classes.cell}
|
||||
style={cellWidth(column.width)}
|
||||
align={column.align}
|
||||
component="td"
|
||||
>
|
||||
{row[column.id]}
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell component="td">
|
||||
<Row align="end" className={classes.actions}>
|
||||
<Status status={row.status} />
|
||||
</Row>
|
||||
</TableCell>
|
||||
<TableCell style={expandCellStyle}>
|
||||
<IconButton disableRipple>{expandedTx === row.nonce ? <ExpandLess /> : <ExpandMore />}</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
style={{ paddingBottom: 0, paddingTop: 0 }}
|
||||
colSpan={6}
|
||||
className={classes.extendedTxContainer}
|
||||
>
|
||||
<Collapse
|
||||
in={expandedTx === row.nonce}
|
||||
timeout="auto"
|
||||
component={ExpandedTxComponent}
|
||||
unmountOnExit
|
||||
tx={row[TX_TABLE_RAW_TX_ID]}
|
||||
threshold={threshold}
|
||||
owners={owners}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
</Table>
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
export default withStyles(styles)(TxsTable)
|
|
@ -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',
|
||||
},
|
||||
})
|
|
@ -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<Transaction>,
|
||||
owners: List<Owner>,
|
||||
}
|
||||
|
||||
class Transactions extends React.Component<Props, {}> {
|
||||
|
@ -16,11 +22,23 @@ class Transactions extends React.Component<Props, {}> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { transactions = List(), safeName, threshold } = this.props
|
||||
const hasTransactions = false
|
||||
const { transactions, owners, threshold } = this.props
|
||||
const hasTransactions = transactions.size > 0
|
||||
|
||||
return <React.Fragment>{hasTransactions ? <div /> : <NoTransactions />}</React.Fragment>
|
||||
return (
|
||||
<React.Fragment>
|
||||
{hasTransactions ? (
|
||||
<TxsTable transactions={transactions} threshold={threshold} owners={owners} />
|
||||
) : (
|
||||
<NoTransactions />
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Transactions.defaultProps = {
|
||||
transactions: List(),
|
||||
}
|
||||
|
||||
export default Transactions
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -16,11 +16,13 @@ const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 5000
|
|||
class SafeView extends React.Component<Props> {
|
||||
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<Props> {
|
|||
createTransaction,
|
||||
fetchTransactions,
|
||||
updateSafe,
|
||||
transactions,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
|
@ -78,6 +81,7 @@ class SafeView extends React.Component<Props> {
|
|||
createTransaction={createTransaction}
|
||||
fetchTransactions={fetchTransactions}
|
||||
updateSafe={updateSafe}
|
||||
transactions={transactions}
|
||||
/>
|
||||
</Page>
|
||||
)
|
||||
|
|
|
@ -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<Transaction>,
|
||||
}
|
||||
|
||||
export const grantedSelector: Selector<GlobalState, RouterProps, boolean> = createSelector(
|
||||
|
@ -95,4 +98,5 @@ export default createStructuredSelector<Object, *>({
|
|||
userAddress: userAccountSelector,
|
||||
network: networkSelector,
|
||||
safeUrl: safeParamAddressSelector,
|
||||
transactions: safeTransactionsSelector,
|
||||
})
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -11,6 +11,9 @@ export type TransactionProps = {
|
|||
recipient: string,
|
||||
data: string,
|
||||
isExecuted: boolean,
|
||||
submissionDate: Date,
|
||||
executionDate: Date,
|
||||
symbol: string,
|
||||
}
|
||||
|
||||
export const makeTransaction: RecordFactory<TransactionProps> = Record({
|
||||
|
@ -21,6 +24,9 @@ export const makeTransaction: RecordFactory<TransactionProps> = Record({
|
|||
recipient: '',
|
||||
data: '',
|
||||
isExecuted: false,
|
||||
submissionDate: '',
|
||||
executionDate: '',
|
||||
symbol: '',
|
||||
})
|
||||
|
||||
export type Transaction = RecordOf<TransactionProps>
|
||||
|
|
|
@ -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<GlobalState, SafeProps, List<Transaction>> = createSelector(
|
||||
export const safeTransactionsSelector: Selector<GlobalState, RouterProps, List<Transaction>> = createSelector(
|
||||
transactionsSelector,
|
||||
safePropAddressSelector,
|
||||
safeParamAddressSelector,
|
||||
(transactions: TransactionsState, address: string): List<Transaction> => {
|
||||
if (!transactions) {
|
||||
return List([])
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue