This commit is contained in:
Mikhail Mikheev 2019-07-01 14:05:34 +04:00
commit 9a70737fe5
29 changed files with 573 additions and 40 deletions

3
.gitignore vendored
View File

@ -2,4 +2,5 @@ node_modules/
build_webpack/
build_storybook/
.DS_Store
build/
build/
yarn-error.log

View File

@ -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",

View File

@ -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>

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -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'
}

View File

@ -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'
}
})

View File

@ -52,6 +52,7 @@ export const generateColumns = () => {
disablePadding: false,
label: '',
custom: true,
static: true,
}
return List([assetColumn, balanceColumn, actions])

View File

@ -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}

View File

@ -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)

View File

@ -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',
},
})

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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',
},
})

View File

@ -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])
}

View File

@ -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)

View File

@ -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',
},
})

View File

@ -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

View File

@ -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,
}

View File

@ -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>
)

View File

@ -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,
})

View File

@ -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))

View File

@ -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))

View File

@ -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>

View File

@ -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([])

View File

@ -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"