WA-238 Transaction List component

This commit is contained in:
apanizo 2018-05-26 09:56:17 +02:00
parent 48a5804051
commit 8abc514413
10 changed files with 353 additions and 5 deletions

View File

@ -94,9 +94,9 @@ export const createTransaction = async (
const CALL = 0 const CALL = 0
if (hasOneOwner(safe)) { if (hasOneOwner(safe)) {
const txHash = await gnosisSafe.execTransactionIfApproved(txDestination, valueInWei, '0x', CALL, nonce, { from: user, gas: '5000000' }) const txReceipt = await gnosisSafe.execTransactionIfApproved(txDestination, valueInWei, '0x', CALL, nonce, { from: user, gas: '5000000' })
const executedConfirmations: List<Confirmation> = buildExecutedConfirmationFrom(safe.get('owners'), user) const executedConfirmations: List<Confirmation> = buildExecutedConfirmationFrom(safe.get('owners'), user)
return storeTransaction(txName, nonce, txDestination, txValue, user, executedConfirmations, txHash.tx, safeAddress, safe.get('confirmations')) return storeTransaction(txName, nonce, txDestination, txValue, user, executedConfirmations, txReceipt.tx, safeAddress, safe.get('confirmations'))
} }
const txConfirmationHash = await gnosisSafe.approveTransactionWithParameters(txDestination, valueInWei, '0x', CALL, nonce, { from: user, gas: '5000000' }) const txConfirmationHash = await gnosisSafe.approveTransactionWithParameters(txDestination, valueInWei, '0x', CALL, nonce, { from: user, gas: '5000000' })

View File

@ -10,6 +10,7 @@ import { type Safe } from '~/routes/safe/store/model/safe'
import List from 'material-ui/List' import List from 'material-ui/List'
import Withdrawn from '~/routes/safe/component/Withdrawn' import Withdrawn from '~/routes/safe/component/Withdrawn'
import Transactions from '~/routes/safe/component/Transactions'
import AddTransaction from '~/routes/safe/component/AddTransaction' import AddTransaction from '~/routes/safe/component/AddTransaction'
import Address from './Address' import Address from './Address'
@ -55,7 +56,7 @@ class GnoSafe extends React.PureComponent<SafeProps, State> {
onListTransactions = () => { onListTransactions = () => {
const { safe } = this.props const { safe } = this.props
this.setState({ component: <Withdrawn safeAddress={safe.get('address')} dailyLimit={safe.get('dailyLimit')} /> }) this.setState({ component: <Transactions safeName={safe.get('name')} safeAddress={safe.get('address')} onAddTx={this.onAddTx} /> })
} }
render() { render() {
@ -81,7 +82,7 @@ class GnoSafe extends React.PureComponent<SafeProps, State> {
</Paragraph> </Paragraph>
</Block> </Block>
<Row grow> <Row grow>
<Col sm={12} center="sm" middle={component ? undefined : 'sm'} layout="column"> <Col sm={12} center={component ? undefined : 'sm'} middle={component ? undefined : 'sm'} layout="column">
{ component || <Img alt="Safe Icon" src={safeIcon} height={330} /> } { component || <Img alt="Safe Icon" src={safeIcon} height={330} /> }
</Col> </Col>
</Row> </Row>

View File

@ -0,0 +1,79 @@
// @flow
import * as React from 'react'
import openHoc, { type Open } from '~/components/hoc/OpenHoc'
import { withStyles } from 'material-ui/styles'
import Collapse from 'material-ui/transitions/Collapse'
import ListItemText from '~/components/List/ListItemText'
import List, { ListItem, ListItemIcon } from 'material-ui/List'
import Avatar from 'material-ui/Avatar'
import Group from 'material-ui-icons/Group'
import Person from 'material-ui-icons/Person'
import ExpandLess from 'material-ui-icons/ExpandLess'
import ExpandMore from 'material-ui-icons/ExpandMore'
import { type WithStyles } from '~/theme/mui'
import { type Confirmation, type ConfirmationProps } from '~/routes/safe/store/model/confirmation'
const styles = {
nested: {
paddingLeft: '40px',
},
}
type Props = Open & WithStyles & {
confirmations: List<Confirmation>,
}
const GnoConfirmation = ({ owner, status, hash }: ConfirmationProps) => {
const address = owner.get('address')
const text = status ? 'Confirmed' : 'Not confirmed'
const hashText = status ? `Confirmation hash: ${hash}` : undefined
return (
<React.Fragment>
<ListItem key={address}>
<ListItemIcon>
<Person />
</ListItemIcon>
<ListItemText
cut
primary={`${owner.get('name')} [${text}]`}
secondary={hashText}
/>
</ListItem>
</React.Fragment>
)
}
const Confirmaitons = openHoc(({
open, toggle, confirmations,
}: Props) => {
const threshold = confirmations.count()
return (
<React.Fragment>
<ListItem onClick={toggle}>
<Avatar>
<Group />
</Avatar>
<ListItemText primary="Threshold" secondary={`${threshold} confirmation${threshold === 1 ? '' : 's'} needed`} />
<ListItemIcon>
{open ? <ExpandLess /> : <ExpandMore />}
</ListItemIcon>
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding style={{ width: '100%' }}>
{confirmations.map(confirmation => (
<GnoConfirmation
key={confirmation.get('owner').get('address')}
owner={confirmation.get('owner')}
status={confirmation.get('status')}
hash={confirmation.get('hash')}
/>
))}
</List>
</Collapse>
</React.Fragment>
)
})
export default withStyles(styles)(Confirmaitons)

View File

@ -0,0 +1,56 @@
// @flow
import * as React from 'react'
import { List as ImmutableList } from 'immutable'
import Row from '~/components/layout/Row'
import Col from '~/components/layout/Col'
import List, { ListItem, ListItemText } from 'material-ui/List'
import Avatar from 'material-ui/Avatar'
import Group from 'material-ui-icons/Group'
import MailOutline from 'material-ui-icons/MailOutline'
import { type Confirmation } from '~/routes/safe/store/model/confirmation'
import Confirmations from './Confirmations'
type Props = {
safeName: string,
confirmations: ImmutableList<Confirmation>,
destination: string,
tx: string,
}
const listStyle = {
width: '100%',
}
class Collapsed extends React.PureComponent<Props, {}> {
render() {
const {
confirmations, destination, safeName, tx,
} = this.props
return (
<Row>
<Col sm={12} top="xs" overflow>
<List style={listStyle}>
<ListItem>
<Avatar><Group /></Avatar>
<ListItemText primary={safeName} secondary="Safe Name" />
</ListItem>
<Confirmations confirmations={confirmations} />
<ListItem>
<Avatar><MailOutline /></Avatar>
<ListItemText primary="Destination" secondary={destination} />
</ListItem>
{ tx &&
<ListItem>
<Avatar><MailOutline /></Avatar>
<ListItemText cut primary="Transaction Hash" secondary={tx} />
</ListItem>
}
</List>
</Col>
</Row>
)
}
}
export default Collapsed

View File

@ -0,0 +1,32 @@
// @flow
import * as React from 'react'
import Bold from '~/components/layout/Bold'
import Button from '~/components/layout/Button'
import Col from '~/components/layout/Col'
import Row from '~/components/layout/Row'
import Paragraph from '~/components/layout/Paragraph/index'
type Props = {
onAddTx: () => void
}
const NoRights = ({ onAddTx }: Props) => (
<Row>
<Col xs={12} center="xs" sm={10} smOffset={2} start="sm" margin="md">
<Paragraph size="lg">
<Bold>No transactions found for this safe</Bold>
</Paragraph>
</Col>
<Col xs={12} center="xs" sm={10} smOffset={2} start="sm" margin="md">
<Button
onClick={onAddTx}
variant="raised"
color="primary"
>
Add Multisig Transaction
</Button>
</Col>
</Row>
)
export default NoRights

View File

@ -0,0 +1,67 @@
// @flow
import * as React from 'react'
import { connect } from 'react-redux'
import openHoc, { type Open } from '~/components/hoc/OpenHoc'
import ExpandLess from 'material-ui-icons/ExpandLess'
import ExpandMore from 'material-ui-icons/ExpandMore'
import ListItemText from '~/components/List/ListItemText'
import Row from '~/components/layout/Row'
import { ListItem, ListItemIcon } from 'material-ui/List'
import Avatar from 'material-ui/Avatar'
import AttachMoney from 'material-ui-icons/AttachMoney'
import Atm from 'material-ui-icons/LocalAtm'
import DoneAll from 'material-ui-icons/DoneAll'
import Collapsed from '~/routes/safe/component/Transactions/Collapsed'
import { type Transaction } from '~/routes/safe/store/model/transaction'
import Hairline from '~/components/layout/Hairline/index'
import selector, { type SelectorProps } from './selector'
type Props = Open & SelectorProps & {
transaction: Transaction,
safeName: string,
}
class GnoTransaction extends React.PureComponent<Props, {}> {
render() {
const {
open, toggle, transaction, confirmed, safeName,
} = this.props
const txHash = transaction.get('tx')
const confirmationText = txHash ? 'Already executed' : `${confirmed} of the ${transaction.get('threshold')} confirmations needed`
return (
<React.Fragment>
<Row>
<ListItem onClick={toggle}>
<Avatar>
<Atm />
</Avatar>
<ListItemText primary="Tx Name" secondary={transaction.get('name')} />
<Avatar>
<AttachMoney />
</Avatar>
<ListItemText primary="Value" secondary={`${transaction.get('value')} ETH`} />
<Avatar>
<DoneAll />
</Avatar>
<ListItemText primary="Status" secondary={confirmationText} />
<ListItemIcon>
{open ? <ExpandLess /> : <ExpandMore />}
</ListItemIcon>
</ListItem>
</Row>
{ open &&
<Collapsed
safeName={safeName}
confirmations={transaction.get('confirmations')}
destination={transaction.get('destination')}
tx={transaction.get('tx')}
/> }
<Hairline />
</React.Fragment>
)
}
}
export default connect(selector)(openHoc(GnoTransaction))

View File

@ -0,0 +1,11 @@
// @flow
import { createStructuredSelector } from 'reselect'
import { confirmationsTransactionSelector } from '~/routes/safe/store/selectors/index'
export type SelectorProps = {
confirmed: confirmationsTransactionSelector,
}
export default createStructuredSelector({
confirmed: confirmationsTransactionSelector,
})

View File

@ -0,0 +1,40 @@
// @flow
import * as React from 'react'
import { connect } from 'react-redux'
import { type Transaction } from '~/routes/safe/store/model/transaction'
import NoTransactions from '~/routes/safe/component/Transactions/NoTransactions'
import GnoTransaction from '~/routes/safe/component/Transactions/Transaction'
import selector, { type SelectorProps } from './selector'
type Props = SelectorProps & {
onAddTx: () => void,
safeName: string,
}
class Transactions extends React.Component<Props, {}> {
onConfirm = () => {
// eslint-disable-next-line
console.log("Confirming tx")
}
onExecute = () => {
// eslint-disable-next-line
console.log("Confirming tx")
}
render() {
const { transactions, onAddTx, safeName } = this.props
const hasTransactions = transactions.count() > 0
return (
<React.Fragment>
{ hasTransactions
? transactions.map((tx: Transaction) => <GnoTransaction key={tx.get('nonce')} safeName={safeName} transaction={tx} />)
: <NoTransactions onAddTx={onAddTx} />
}
</React.Fragment>
)
}
}
export default connect(selector)(Transactions)

View File

@ -0,0 +1,13 @@
// @flow
import { List } from 'immutable'
import { createStructuredSelector } from 'reselect'
import { type Transaction } from '~/routes/safe/store/model/transaction'
import { safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
export type SelectorProps = {
transactions: List<Transaction>,
}
export default createStructuredSelector({
transactions: safeTransactionsSelector,
})

View File

@ -1,5 +1,5 @@
// @flow // @flow
import { Map } from 'immutable' import { Map, List } from 'immutable'
import { type Match } from 'react-router-dom' import { type Match } from 'react-router-dom'
import { createSelector, createStructuredSelector, type Selector } from 'reselect' import { createSelector, createStructuredSelector, type Selector } from 'reselect'
import { type GlobalState } from '~/store/index' import { type GlobalState } from '~/store/index'
@ -7,15 +7,64 @@ import { SAFE_PARAM_ADDRESS } from '~/routes/routes'
import { type Safe } from '~/routes/safe/store/model/safe' import { type Safe } from '~/routes/safe/store/model/safe'
import { safesMapSelector } from '~/routes/safeList/store/selectors' import { safesMapSelector } from '~/routes/safeList/store/selectors'
import { BALANCE_REDUCER_ID } from '~/routes/safe/store/reducer/balances' import { BALANCE_REDUCER_ID } from '~/routes/safe/store/reducer/balances'
import { type State as TransactionsState, TRANSACTIONS_REDUCER_ID } from '~/routes/safe/store/reducer/transactions'
import { type Transaction } from '~/routes/safe/store/model/transaction'
import { type Confirmation } from '~/routes/safe/store/model/confirmation'
export type RouterProps = { export type RouterProps = {
match: Match, match: Match,
} }
export type SafeProps = {
safeAddress: string,
}
type TransactionProps = {
transaction: Transaction,
}
const safeAddressSelector = (state: GlobalState, props: SafeProps) => props.safeAddress
const safeAddessSelector = (state: GlobalState, props: RouterProps) => props.match.params[SAFE_PARAM_ADDRESS] || '' const safeAddessSelector = (state: GlobalState, props: RouterProps) => props.match.params[SAFE_PARAM_ADDRESS] || ''
const balancesSelector = (state: GlobalState) => state[BALANCE_REDUCER_ID] const balancesSelector = (state: GlobalState) => state[BALANCE_REDUCER_ID]
const transactionsSelector = (state: GlobalState): TransactionsState => state[TRANSACTIONS_REDUCER_ID]
const oneTransactionSelector = (state: GlobalState, props: TransactionProps) => props.transaction
export const safeTransactionsSelector: Selector<GlobalState, SafeProps, List<Transaction>> = createSelector(
transactionsSelector,
safeAddressSelector,
(transactions: TransactionsState, address: string): List<Transaction> => {
if (!transactions) {
return List([])
}
if (!address) {
return List([])
}
return transactions.get(address) || List([])
},
)
export const confirmationsTransactionSelector = createSelector(
oneTransactionSelector,
(tx: Transaction) => {
if (!tx) {
return 0
}
const confirmations: List<Confirmation> = tx.get('confirmations')
if (!confirmations) {
return 0
}
return confirmations.filter(((confirmation: Confirmation) => confirmation.get('status'))).count()
},
)
export type SafeSelectorProps = Safe | typeof undefined export type SafeSelectorProps = Safe | typeof undefined
export const safeSelector: Selector<GlobalState, RouterProps, SafeSelectorProps> = createSelector( export const safeSelector: Selector<GlobalState, RouterProps, SafeSelectorProps> = createSelector(