WA-238 Transaction List component
This commit is contained in:
parent
48a5804051
commit
8abc514413
|
@ -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' })
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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))
|
|
@ -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,
|
||||||
|
})
|
|
@ -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)
|
|
@ -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,
|
||||||
|
})
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in New Issue