pull
This commit is contained in:
commit
9a70737fe5
|
@ -3,3 +3,4 @@ build_webpack/
|
||||||
build_storybook/
|
build_storybook/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
build/
|
build/
|
||||||
|
yarn-error.log
|
|
@ -39,6 +39,7 @@
|
||||||
"bignumber.js": "9.0.0",
|
"bignumber.js": "9.0.0",
|
||||||
"connected-react-router": "^6.3.1",
|
"connected-react-router": "^6.3.1",
|
||||||
"final-form": "4.16.1",
|
"final-form": "4.16.1",
|
||||||
|
"date-fns": "^1.30.1",
|
||||||
"history": "^4.7.2",
|
"history": "^4.7.2",
|
||||||
"immortal-db": "^1.0.2",
|
"immortal-db": "^1.0.2",
|
||||||
"immutable": "^4.0.0-rc.9",
|
"immutable": "^4.0.0-rc.9",
|
||||||
|
|
|
@ -9,7 +9,7 @@ import styles from './index.scss'
|
||||||
const Footer = () => (
|
const Footer = () => (
|
||||||
<Block className={styles.footer}>
|
<Block className={styles.footer}>
|
||||||
<Link to={WELCOME_ADDRESS}>
|
<Link to={WELCOME_ADDRESS}>
|
||||||
<Paragraph size="sm" color="primary" noMargin>Welcome</Paragraph>
|
<Paragraph size="sm" color="primary" noMargin>Add Safe</Paragraph>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to={SAFELIST_ADDRESS}>
|
<Link to={SAFELIST_ADDRESS}>
|
||||||
<Paragraph size="sm" color="primary" noMargin>Safe List</Paragraph>
|
<Paragraph size="sm" color="primary" noMargin>Safe List</Paragraph>
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
import Row from '~/components/layout/Row'
|
|
||||||
import Table from '@material-ui/core/Table'
|
import Table from '@material-ui/core/Table'
|
||||||
import TableBody from '@material-ui/core/TableBody'
|
import TableBody from '@material-ui/core/TableBody'
|
||||||
import { withStyles } from '@material-ui/core/styles'
|
import { withStyles } from '@material-ui/core/styles'
|
||||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||||
import TablePagination from '@material-ui/core/TablePagination'
|
import TablePagination from '@material-ui/core/TablePagination'
|
||||||
|
import Row from '~/components/layout/Row'
|
||||||
import { type Order, stableSort, getSorting } from '~/components/Table/sorting'
|
import { type Order, stableSort, getSorting } from '~/components/Table/sorting'
|
||||||
import TableHead, { type Column } from '~/components/Table/TableHead'
|
import TableHead, { type Column } from '~/components/Table/TableHead'
|
||||||
import { xl } from '~/theme/variables'
|
import { xl } from '~/theme/variables'
|
||||||
|
@ -21,6 +21,7 @@ type Props<K> = {
|
||||||
children: Function,
|
children: Function,
|
||||||
size: number,
|
size: number,
|
||||||
defaultFixed?: boolean,
|
defaultFixed?: boolean,
|
||||||
|
defaultOrder?: 'desc' | 'asc',
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
@ -61,7 +62,7 @@ const FIXED_HEIGHT = 49
|
||||||
class GnoTable<K> extends React.Component<Props<K>, State> {
|
class GnoTable<K> extends React.Component<Props<K>, State> {
|
||||||
state = {
|
state = {
|
||||||
page: 0,
|
page: 0,
|
||||||
order: 'asc',
|
order: undefined,
|
||||||
orderBy: undefined,
|
orderBy: undefined,
|
||||||
fixed: undefined,
|
fixed: undefined,
|
||||||
orderProp: false,
|
orderProp: false,
|
||||||
|
@ -70,9 +71,14 @@ class GnoTable<K> extends React.Component<Props<K>, State> {
|
||||||
|
|
||||||
onSort = (newOrderBy: string, orderProp: boolean) => {
|
onSort = (newOrderBy: string, orderProp: boolean) => {
|
||||||
const { order, orderBy } = this.state
|
const { order, orderBy } = this.state
|
||||||
|
const { defaultOrder } = this.props
|
||||||
let newOrder = 'desc'
|
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'
|
newOrder = 'asc'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,12 +105,13 @@ class GnoTable<K> extends React.Component<Props<K>, State> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
data, label, columns, classes, children, size, defaultOrderBy, defaultFixed,
|
data, label, columns, classes, children, size, defaultOrderBy, defaultOrder, defaultFixed,
|
||||||
} = this.props
|
} = this.props
|
||||||
const {
|
const {
|
||||||
order, orderBy, page, orderProp, rowsPerPage, fixed,
|
order, orderBy, page, orderProp, rowsPerPage, fixed,
|
||||||
} = this.state
|
} = this.state
|
||||||
const orderByParam = orderBy || defaultOrderBy
|
const orderByParam = orderBy || defaultOrderBy
|
||||||
|
const orderParam = order || defaultOrder
|
||||||
const fixedParam = typeof fixed !== 'undefined' ? fixed : !!defaultFixed
|
const fixedParam = typeof fixed !== 'undefined' ? fixed : !!defaultFixed
|
||||||
|
|
||||||
const backProps = {
|
const backProps = {
|
||||||
|
@ -121,7 +128,7 @@ class GnoTable<K> extends React.Component<Props<K>, State> {
|
||||||
input: classes.white,
|
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,
|
||||||
page * rowsPerPage + rowsPerPage,
|
page * rowsPerPage + rowsPerPage,
|
||||||
)
|
)
|
||||||
|
@ -133,7 +140,7 @@ class GnoTable<K> extends React.Component<Props<K>, State> {
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{!isEmpty && (
|
{!isEmpty && (
|
||||||
<Table aria-labelledby={label} className={classes.root}>
|
<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>
|
<TableBody>{children(sortedData)}</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
)}
|
)}
|
||||||
|
@ -159,4 +166,8 @@ class GnoTable<K> extends React.Component<Props<K>, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GnoTable.defaultProps = {
|
||||||
|
defaultOrder: 'asc',
|
||||||
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(GnoTable)
|
export default withStyles(styles)(GnoTable)
|
||||||
|
|
|
@ -5,18 +5,60 @@ import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||||
import { isEther } from '~/logic/tokens/utils/tokenHelpers'
|
import { isEther } from '~/logic/tokens/utils/tokenHelpers'
|
||||||
import { type Token } from '~/logic/tokens/store/model/token'
|
import { type Token } from '~/logic/tokens/store/model/token'
|
||||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
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'
|
import { ZERO_ADDRESS } from '~/logic/wallets/ethAddresses'
|
||||||
|
|
||||||
export const CALL = 0
|
export const CALL = 0
|
||||||
export const TX_TYPE_EXECUTION = 'execution'
|
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 (
|
export const executeTransaction = async (
|
||||||
safeInstance: any,
|
safeInstance: any,
|
||||||
to: string,
|
to: string,
|
||||||
valueInWei: number | string,
|
valueInWei: number | string,
|
||||||
data: string,
|
data: string,
|
||||||
operation: number | string,
|
operation: Operation,
|
||||||
nonce: string | number,
|
nonce: string | number,
|
||||||
sender: string,
|
sender: string,
|
||||||
) => {
|
) => {
|
||||||
|
@ -66,7 +108,7 @@ export const createTransaction = async (safeAddress: string, to: string, valueIn
|
||||||
const web3 = getWeb3()
|
const web3 = getWeb3()
|
||||||
const from = web3.currentProvider.selectedAddress
|
const from = web3.currentProvider.selectedAddress
|
||||||
const threshold = await safeInstance.getThreshold()
|
const threshold = await safeInstance.getThreshold()
|
||||||
const nonce = await safeInstance.nonce()
|
const nonce = (await safeInstance.nonce()).toString()
|
||||||
const valueInWei = web3.utils.toWei(valueInEth, 'ether')
|
const valueInWei = web3.utils.toWei(valueInEth, 'ether')
|
||||||
const isExecution = threshold.toNumber() === 1
|
const isExecution = threshold.toNumber() === 1
|
||||||
|
|
||||||
|
|
|
@ -10,13 +10,13 @@ export type Operation = 0 | 1 | 2
|
||||||
const calculateBodyFrom = async (
|
const calculateBodyFrom = async (
|
||||||
safeInstance: any,
|
safeInstance: any,
|
||||||
to: string,
|
to: string,
|
||||||
valueInWei: number,
|
valueInWei: number | string,
|
||||||
data: string,
|
data: string,
|
||||||
operation: Operation,
|
operation: Operation,
|
||||||
nonce: string | number,
|
nonce: string | number,
|
||||||
transactionHash: string,
|
transactionHash: string,
|
||||||
sender: string,
|
sender: string,
|
||||||
type: TxServiceType,
|
confirmationType: TxServiceType,
|
||||||
) => {
|
) => {
|
||||||
const contractTransactionHash = await safeInstance.getTransactionHash(
|
const contractTransactionHash = await safeInstance.getTransactionHash(
|
||||||
to,
|
to,
|
||||||
|
@ -38,14 +38,14 @@ const calculateBodyFrom = async (
|
||||||
operation,
|
operation,
|
||||||
nonce,
|
nonce,
|
||||||
safeTxGas: 0,
|
safeTxGas: 0,
|
||||||
dataGas: 0,
|
baseGas: 0,
|
||||||
gasPrice: 0,
|
gasPrice: 0,
|
||||||
gasToken: ZERO_ADDRESS,
|
gasToken: ZERO_ADDRESS,
|
||||||
refundReceiver: ZERO_ADDRESS,
|
refundReceiver: ZERO_ADDRESS,
|
||||||
contractTransactionHash,
|
contractTransactionHash,
|
||||||
transactionHash,
|
transactionHash,
|
||||||
sender: getWeb3().utils.toChecksumAddress(sender),
|
sender: getWeb3().utils.toChecksumAddress(sender),
|
||||||
type,
|
confirmationType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ export const buildTxServiceUrl = (safeAddress: string) => {
|
||||||
export const saveTxToHistory = async (
|
export const saveTxToHistory = async (
|
||||||
safeInstance: any,
|
safeInstance: any,
|
||||||
to: string,
|
to: string,
|
||||||
valueInWei: number,
|
valueInWei: number | string,
|
||||||
data: string,
|
data: string,
|
||||||
operation: Operation,
|
operation: Operation,
|
||||||
nonce: number | string,
|
nonce: number | string,
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { List } from 'immutable'
|
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 { makeToken, type Token } from '~/logic/tokens/store/model/token'
|
||||||
|
import logo from '~/assets/icons/icon_etherTokens.svg'
|
||||||
|
|
||||||
export const ETH_ADDRESS = '0x000'
|
export const ETH_ADDRESS = '0x000'
|
||||||
export const isEther = (symbol: string) => symbol === 'ETH'
|
export const isEther = (symbol: string) => symbol === 'ETH'
|
||||||
|
@ -31,3 +32,19 @@ export const calculateActiveErc20TokensFrom = (tokens: List<Token>) => {
|
||||||
|
|
||||||
return activeTokens
|
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
|
// @flow
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
|
||||||
import { type Token } from '~/logic/tokens/store/model/token'
|
import { type Token } from '~/logic/tokens/store/model/token'
|
||||||
import { sameAddress } from '~/logic/wallets/ethAddresses'
|
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) => {
|
export const simpleMemoize = (fn: Function) => {
|
||||||
let lastArg
|
let lastArg
|
||||||
|
@ -28,10 +27,9 @@ export const addressIsTokenContract = simpleMemoize(async (tokenAddress: string)
|
||||||
// return 'Not a token address'
|
// return 'Not a token address'
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const web3 = getWeb3()
|
const isToken = await isAddressAToken(tokenAddress)
|
||||||
const call = await web3.eth.call({ to: tokenAddress, data: web3.utils.sha3('totalSupply()') })
|
|
||||||
|
|
||||||
if (call === '0x') {
|
if (!isToken) {
|
||||||
return 'Not a token address'
|
return 'Not a token address'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -52,6 +52,7 @@ export const generateColumns = () => {
|
||||||
disablePadding: false,
|
disablePadding: false,
|
||||||
label: '',
|
label: '',
|
||||||
custom: true,
|
custom: true,
|
||||||
|
static: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
return List([assetColumn, balanceColumn, actions])
|
return List([assetColumn, balanceColumn, actions])
|
||||||
|
|
|
@ -101,6 +101,7 @@ class Layout extends React.Component<Props, State> {
|
||||||
createTransaction,
|
createTransaction,
|
||||||
fetchTransactions,
|
fetchTransactions,
|
||||||
updateSafe,
|
updateSafe,
|
||||||
|
transactions,
|
||||||
} = this.props
|
} = this.props
|
||||||
const { tabIndex } = this.state
|
const { tabIndex } = this.state
|
||||||
|
|
||||||
|
@ -152,7 +153,15 @@ class Layout extends React.Component<Props, State> {
|
||||||
createTransaction={createTransaction}
|
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 && (
|
{tabIndex === 2 && (
|
||||||
<Settings
|
<Settings
|
||||||
granted={granted}
|
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
|
// @flow
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { List } from 'immutable'
|
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 = {
|
type Props = {
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
threshold: number,
|
threshold: number,
|
||||||
|
fetchTransactions: Function,
|
||||||
|
transactions: List<Transaction>,
|
||||||
|
owners: List<Owner>,
|
||||||
}
|
}
|
||||||
|
|
||||||
class Transactions extends React.Component<Props, {}> {
|
class Transactions extends React.Component<Props, {}> {
|
||||||
|
@ -16,11 +22,23 @@ class Transactions extends React.Component<Props, {}> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { transactions = List(), safeName, threshold } = this.props
|
const { transactions, owners, threshold } = this.props
|
||||||
const hasTransactions = false
|
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
|
export default Transactions
|
||||||
|
|
|
@ -4,6 +4,7 @@ import fetchTokenBalances from '~/routes/safe/store/actions/fetchTokenBalances'
|
||||||
import createTransaction from '~/routes/safe/store/actions/createTransaction'
|
import createTransaction from '~/routes/safe/store/actions/createTransaction'
|
||||||
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||||
import updateSafe from '~/routes/safe/store/actions/updateSafe'
|
import updateSafe from '~/routes/safe/store/actions/updateSafe'
|
||||||
|
import fetchTokens from '~/logic/tokens/store/actions/fetchTokens'
|
||||||
|
|
||||||
export type Actions = {
|
export type Actions = {
|
||||||
fetchSafe: typeof fetchSafe,
|
fetchSafe: typeof fetchSafe,
|
||||||
|
@ -11,12 +12,14 @@ export type Actions = {
|
||||||
createTransaction: typeof createTransaction,
|
createTransaction: typeof createTransaction,
|
||||||
fetchTransactions: typeof fetchTransactions,
|
fetchTransactions: typeof fetchTransactions,
|
||||||
updateSafe: typeof updateSafe,
|
updateSafe: typeof updateSafe,
|
||||||
|
fetchTokens: typeof fetchTokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
fetchSafe,
|
fetchSafe,
|
||||||
fetchTokenBalances,
|
fetchTokenBalances,
|
||||||
createTransaction,
|
createTransaction,
|
||||||
|
fetchTokens,
|
||||||
fetchTransactions,
|
fetchTransactions,
|
||||||
updateSafe,
|
updateSafe,
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,11 +16,13 @@ const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 5000
|
||||||
class SafeView extends React.Component<Props> {
|
class SafeView extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const {
|
const {
|
||||||
fetchSafe, activeTokens, safeUrl, fetchTokenBalances,
|
fetchSafe, activeTokens, safeUrl, fetchTokenBalances, fetchTokens,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
fetchSafe(safeUrl)
|
fetchSafe(safeUrl)
|
||||||
fetchTokenBalances(safeUrl, activeTokens)
|
fetchTokenBalances(safeUrl, activeTokens)
|
||||||
|
// fetch tokens there to get symbols for tokens in TXs list
|
||||||
|
fetchTokens()
|
||||||
|
|
||||||
this.intervalId = setInterval(() => {
|
this.intervalId = setInterval(() => {
|
||||||
this.checkForUpdates()
|
this.checkForUpdates()
|
||||||
|
@ -63,6 +65,7 @@ class SafeView extends React.Component<Props> {
|
||||||
createTransaction,
|
createTransaction,
|
||||||
fetchTransactions,
|
fetchTransactions,
|
||||||
updateSafe,
|
updateSafe,
|
||||||
|
transactions,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -78,6 +81,7 @@ class SafeView extends React.Component<Props> {
|
||||||
createTransaction={createTransaction}
|
createTransaction={createTransaction}
|
||||||
fetchTransactions={fetchTransactions}
|
fetchTransactions={fetchTransactions}
|
||||||
updateSafe={updateSafe}
|
updateSafe={updateSafe}
|
||||||
|
transactions={transactions}
|
||||||
/>
|
/>
|
||||||
</Page>
|
</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 Owner } from '~/routes/safe/store/models/owner'
|
||||||
import { type GlobalState } from '~/store'
|
import { type GlobalState } from '~/store'
|
||||||
import { sameAddress } from '~/logic/wallets/ethAddresses'
|
import { sameAddress } from '~/logic/wallets/ethAddresses'
|
||||||
|
import { safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
|
||||||
import { orderedTokenListSelector, tokensSelector } from '~/logic/tokens/store/selectors'
|
import { orderedTokenListSelector, tokensSelector } from '~/logic/tokens/store/selectors'
|
||||||
import { type Token } from '~/logic/tokens/store/model/token'
|
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 { type TokenBalance } from '~/routes/safe/store/models/tokenBalance'
|
||||||
import { safeParamAddressSelector } from '../store/selectors'
|
import { safeParamAddressSelector } from '../store/selectors'
|
||||||
import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers'
|
import { getEthAsToken } from '~/logic/tokens/utils/tokenHelpers'
|
||||||
|
@ -27,6 +29,7 @@ export type SelectorProps = {
|
||||||
userAddress: string,
|
userAddress: string,
|
||||||
network: string,
|
network: string,
|
||||||
safeUrl: string,
|
safeUrl: string,
|
||||||
|
transactions: List<Transaction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const grantedSelector: Selector<GlobalState, RouterProps, boolean> = createSelector(
|
export const grantedSelector: Selector<GlobalState, RouterProps, boolean> = createSelector(
|
||||||
|
@ -95,4 +98,5 @@ export default createStructuredSelector<Object, *>({
|
||||||
userAddress: userAccountSelector,
|
userAddress: userAccountSelector,
|
||||||
network: networkSelector,
|
network: networkSelector,
|
||||||
safeUrl: safeParamAddressSelector,
|
safeUrl: safeParamAddressSelector,
|
||||||
|
transactions: safeTransactionsSelector,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { Dispatch as ReduxDispatch, GetState } from 'redux'
|
import type { Dispatch as ReduxDispatch, GetState } from 'redux'
|
||||||
import { createAction } from 'redux-actions'
|
|
||||||
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||||
import { userAccountSelector } from '~/logic/wallets/store/selectors'
|
import { userAccountSelector } from '~/logic/wallets/store/selectors'
|
||||||
import { type GlobalState } from '~/store'
|
import { type GlobalState } from '~/store'
|
||||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||||
import { executeTransaction, CALL } from '~/logic/safe/transactions'
|
import { approveTransaction, executeTransaction, CALL } from '~/logic/safe/transactions'
|
||||||
|
|
||||||
const createTransaction = (
|
const createTransaction = (
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
|
@ -19,7 +18,7 @@ const createTransaction = (
|
||||||
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
const from = userAccountSelector(state)
|
const from = userAccountSelector(state)
|
||||||
const threshold = await safeInstance.getThreshold()
|
const threshold = await safeInstance.getThreshold()
|
||||||
const nonce = await safeInstance.nonce()
|
const nonce = (await safeInstance.nonce()).toString()
|
||||||
const isExecution = threshold.toNumber() === 1
|
const isExecution = threshold.toNumber() === 1
|
||||||
|
|
||||||
let txHash
|
let txHash
|
||||||
|
@ -28,7 +27,7 @@ const createTransaction = (
|
||||||
txHash = await executeTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from)
|
txHash = await executeTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from)
|
||||||
openSnackbar('Transaction has been confirmed', 'success')
|
openSnackbar('Transaction has been confirmed', 'success')
|
||||||
} else {
|
} else {
|
||||||
// txHash = await approveTransaction(safeAddress, to, valueInWei, txData, CALL, nonce)
|
txHash = await approveTransaction(safeInstance, to, valueInWei, txData, CALL, nonce, from)
|
||||||
}
|
}
|
||||||
// dispatch(addTransactions(txHash))
|
// dispatch(addTransactions(txHash))
|
||||||
|
|
||||||
|
|
|
@ -11,11 +11,13 @@ import { buildTxServiceUrl, type TxServiceType } from '~/logic/safe/transactions
|
||||||
import { getOwners } from '~/logic/safe/utils'
|
import { getOwners } from '~/logic/safe/utils'
|
||||||
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
|
||||||
import { addTransactions } from './addTransactions'
|
import { addTransactions } from './addTransactions'
|
||||||
|
import { getHumanFriendlyToken } from '~/logic/tokens/store/actions/fetchTokens'
|
||||||
|
import { isAddressAToken } from '~/logic/tokens/utils/tokenHelpers'
|
||||||
|
|
||||||
type ConfirmationServiceModel = {
|
type ConfirmationServiceModel = {
|
||||||
owner: string,
|
owner: string,
|
||||||
submissionDate: Date,
|
submissionDate: Date,
|
||||||
type: string,
|
confirmationType: string,
|
||||||
transactionHash: string,
|
transactionHash: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,20 +42,31 @@ const buildTransactionFrom = async (safeAddress: string, tx: TxServiceModel, saf
|
||||||
|
|
||||||
return makeConfirmation({
|
return makeConfirmation({
|
||||||
owner: makeOwner({ address: conf.owner, name: ownerName }),
|
owner: makeOwner({ address: conf.owner, name: ownerName }),
|
||||||
type: ((conf.type.toLowerCase(): any): TxServiceType),
|
type: ((conf.confirmationType.toLowerCase(): any): TxServiceType),
|
||||||
hash: conf.transactionHash,
|
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({
|
return makeTransaction({
|
||||||
name,
|
name,
|
||||||
|
symbol,
|
||||||
nonce: tx.nonce,
|
nonce: tx.nonce,
|
||||||
value: Number(tx.value),
|
value: Number(tx.value),
|
||||||
confirmations,
|
confirmations,
|
||||||
recipient: tx.to,
|
recipient: tx.to,
|
||||||
data: tx.data ? tx.data : EMPTY_DATA,
|
data: tx.data ? tx.data : EMPTY_DATA,
|
||||||
isExecuted: tx.isExecuted,
|
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 response = await axios.get(url)
|
||||||
const transactions: TxServiceModel[] = response.data.results
|
const transactions: TxServiceModel[] = response.data.results
|
||||||
const safeSubjects = loadSafeSubjects(safeAddress)
|
const safeSubjects = loadSafeSubjects(safeAddress)
|
||||||
const txsRecord = transactions.map(
|
const txsRecord = await Promise.all(
|
||||||
async (tx: TxServiceModel) => await buildTransactionFrom(safeAddress, tx, safeSubjects),
|
transactions.map((tx: TxServiceModel) => buildTransactionFrom(safeAddress, tx, safeSubjects)),
|
||||||
)
|
)
|
||||||
|
|
||||||
return Map().set(safeAddress, List(txsRecord))
|
return Map().set(safeAddress, List(txsRecord))
|
||||||
|
|
|
@ -11,6 +11,9 @@ export type TransactionProps = {
|
||||||
recipient: string,
|
recipient: string,
|
||||||
data: string,
|
data: string,
|
||||||
isExecuted: boolean,
|
isExecuted: boolean,
|
||||||
|
submissionDate: Date,
|
||||||
|
executionDate: Date,
|
||||||
|
symbol: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeTransaction: RecordFactory<TransactionProps> = Record({
|
export const makeTransaction: RecordFactory<TransactionProps> = Record({
|
||||||
|
@ -21,6 +24,9 @@ export const makeTransaction: RecordFactory<TransactionProps> = Record({
|
||||||
recipient: '',
|
recipient: '',
|
||||||
data: '',
|
data: '',
|
||||||
isExecuted: false,
|
isExecuted: false,
|
||||||
|
submissionDate: '',
|
||||||
|
executionDate: '',
|
||||||
|
symbol: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
export type Transaction = RecordOf<TransactionProps>
|
export type Transaction = RecordOf<TransactionProps>
|
||||||
|
|
|
@ -23,17 +23,15 @@ type TransactionProps = {
|
||||||
transaction: Transaction,
|
transaction: Transaction,
|
||||||
}
|
}
|
||||||
|
|
||||||
const safePropAddressSelector = (state: GlobalState, props: SafeProps) => props.safeAddress
|
|
||||||
|
|
||||||
const transactionsSelector = (state: GlobalState): TransactionsState => state[TRANSACTIONS_REDUCER_ID]
|
const transactionsSelector = (state: GlobalState): TransactionsState => state[TRANSACTIONS_REDUCER_ID]
|
||||||
|
|
||||||
const oneTransactionSelector = (state: GlobalState, props: TransactionProps) => props.transaction
|
const oneTransactionSelector = (state: GlobalState, props: TransactionProps) => props.transaction
|
||||||
|
|
||||||
export const safeParamAddressSelector = (state: GlobalState, props: RouterProps) => props.match.params[SAFE_PARAM_ADDRESS] || ''
|
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,
|
transactionsSelector,
|
||||||
safePropAddressSelector,
|
safeParamAddressSelector,
|
||||||
(transactions: TransactionsState, address: string): List<Transaction> => {
|
(transactions: TransactionsState, address: string): List<Transaction> => {
|
||||||
if (!transactions) {
|
if (!transactions) {
|
||||||
return List([])
|
return List([])
|
||||||
|
|
|
@ -5727,6 +5727,11 @@ data-urls@^1.0.0:
|
||||||
whatwg-mimetype "^2.2.0"
|
whatwg-mimetype "^2.2.0"
|
||||||
whatwg-url "^7.0.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:
|
date-now@^0.1.4:
|
||||||
version "0.1.4"
|
version "0.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
|
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
|
||||||
|
|
Loading…
Reference in New Issue