WA-232 tokens route

This commit is contained in:
apanizo 2018-07-10 13:01:20 +02:00
parent d7193dcc9a
commit 0b5d14c8f2
33 changed files with 497 additions and 168 deletions

View File

@ -7,11 +7,11 @@ import GnoSafe from './Safe'
type Props = SelectorProps
const Layout = ({
safe, balances, provider, userAddress,
safe, tokens, provider, userAddress,
}: Props) => (
<React.Fragment>
{ safe
? <GnoSafe safe={safe} balances={balances} userAddress={userAddress} />
? <GnoSafe safe={safe} tokens={tokens} userAddress={userAddress} />
: <NoSafe provider={provider} text="Not found safe" />
}
</React.Fragment>

View File

@ -4,7 +4,7 @@ import * as React from 'react'
import { Map } from 'immutable'
import styles from '~/components/layout/PageFrame/index.scss'
import { SafeFactory } from '~/routes/safe/store/test/builder/safe.builder'
import { makeBalance } from '~/routes/safe/store/model/balance'
import { makeToken } from '~/routes/tokens/store/model/token'
import Component from './Layout'
@ -14,7 +14,7 @@ const FrameDecorator = story => (
</div>
)
const ethBalance = makeBalance({
const ethBalance = makeToken({
address: '0',
name: 'Ether',
symbol: 'ETH',
@ -30,7 +30,7 @@ storiesOf('Routes /safe:address', module)
userAddress="foo"
safe={undefined}
provider="METAMASK"
balances={Map()}
tokens={Map()}
fetchBalance={() => {}}
/>
))
@ -39,7 +39,7 @@ storiesOf('Routes /safe:address', module)
userAddress="foo"
safe={undefined}
provider=""
balances={Map()}
tokens={Map()}
fetchBalance={() => {}}
/>
))
@ -51,7 +51,7 @@ storiesOf('Routes /safe:address', module)
userAddress="foo"
safe={safe}
provider="METAMASK"
balances={Map().set('ETH', ethBalance)}
tokens={Map().set('ETH', ethBalance)}
fetchBalance={() => {}}
/>
)
@ -64,7 +64,7 @@ storiesOf('Routes /safe:address', module)
userAddress="foo"
safe={safe}
provider="METAMASK"
balances={Map().set('ETH', ethBalance)}
tokens={Map().set('ETH', ethBalance)}
fetchBalance={() => {}}
/>
)

View File

@ -17,11 +17,11 @@ import { Map } from 'immutable'
import Button from '~/components/layout/Button'
import openHoc, { type Open } from '~/components/hoc/OpenHoc'
import { type WithStyles } from '~/theme/mui'
import { type Balance } from '~/routes/safe/store/model/balance'
import { type Token } from '~/routes/tokens/store/model/token'
type Props = Open & WithStyles & {
balances: Map<string, Balance>,
onMoveFunds: (balance: Balance) => void,
tokens: Map<string, Token>,
onMoveFunds: (token: Token) => void,
}
const styles = {
@ -33,9 +33,9 @@ const styles = {
export const MOVE_FUNDS_BUTTON_TEXT = 'Move'
const BalanceComponent = openHoc(({
open, toggle, balances, classes, onMoveFunds,
open, toggle, tokens, classes, onMoveFunds,
}: Props) => {
const hasBalances = balances.count() > 0
const hasBalances = tokens.count() > 0
return (
<React.Fragment>
@ -53,18 +53,18 @@ const BalanceComponent = openHoc(({
</ListItem>
<Collapse in={open} timeout="auto">
<List component="div" disablePadding>
{balances.valueSeq().map((balance: Balance) => {
const symbol = balance.get('symbol')
const name = balance.get('name')
const disabled = Number(balance.get('funds')) === 0
const onMoveFundsClick = () => onMoveFunds(balance)
{tokens.valueSeq().map((token: Token) => {
const symbol = token.get('symbol')
const name = token.get('name')
const disabled = Number(token.get('funds')) === 0
const onMoveFundsClick = () => onMoveFunds(token)
return (
<ListItem key={symbol} className={classNames(classes.nested, symbol)}>
<ListItemIcon>
<Img src={balance.get('logoUrl')} height={30} alt={name} />
<Img src={token.get('logoUrl')} height={30} alt={name} />
</ListItemIcon>
<ListItemText primary={name} secondary={`${balance.get('funds')} ${symbol}`} />
<ListItemText primary={name} secondary={`${token.get('funds')} ${symbol}`} />
<Button variant="raised" color="primary" onClick={onMoveFundsClick} disabled={disabled}>
{MOVE_FUNDS_BUTTON_TEXT}
</Button>

View File

@ -9,7 +9,7 @@ import Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Balance } from '~/routes/safe/store/model/balance'
import { type Token } from '~/routes/tokens/store/model/token'
import Withdraw from '~/routes/safe/component/Withdraw'
import Transactions from '~/routes/safe/component/Transactions'
@ -30,7 +30,7 @@ const safeIcon = require('./assets/gnosis_safe.svg')
type SafeProps = {
safe: Safe,
balances: Map<string, Balance>,
tokens: Map<string, Token>,
userAddress: string,
}
@ -42,7 +42,7 @@ const listStyle = {
width: '100%',
}
const getEthBalanceFrom = (balances: Map<string, Balance>) => {
const getEthBalanceFrom = (balances: Map<string, Token>) => {
const ethBalance = balances.get('ETH')
if (!ethBalance) {
return 0
@ -93,13 +93,13 @@ class GnoSafe extends React.PureComponent<SafeProps, State> {
this.setState({ component: <RemoveOwner safeAddress={safe.get('address')} threshold={safe.get('threshold')} safe={safe} name={name} userToRemove={address} /> })
}
onMoveTokens = (ercToken: Balance) => {
onMoveTokens = (ercToken: Token) => {
const { safe } = this.props
this.setState({
component: <SendToken
safe={safe}
balance={ercToken}
token={ercToken}
key={ercToken.get('symbol')}
onReset={this.onListTransactions}
/>,
@ -107,15 +107,15 @@ class GnoSafe extends React.PureComponent<SafeProps, State> {
}
render() {
const { safe, balances, userAddress } = this.props
const { safe, tokens, userAddress } = this.props
const { component } = this.state
const ethBalance = getEthBalanceFrom(balances)
const ethBalance = getEthBalanceFrom(tokens)
return (
<Row grow>
<Col sm={12} top="xs" md={5} margin="xl" overflow>
<List style={listStyle}>
<BalanceInfo balances={balances} onMoveFunds={this.onMoveTokens} />
<BalanceInfo tokens={tokens} onMoveFunds={this.onMoveTokens} />
<Owners
owners={safe.owners}
onAddOwner={this.onAddOwner}

View File

@ -5,9 +5,9 @@ import { connect } from 'react-redux'
import Stepper from '~/components/Stepper'
import { sleep } from '~/utils/timer'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Balance } from '~/routes/safe/store/model/balance'
import { getStandardTokenContract } from '~/routes/tokens/store/actions/fetchTokens'
import { type Token } from '~/routes/tokens/store/model/token'
import { createTransaction } from '~/wallets/createTransactions'
import { getStandardTokenContract } from '~/routes/safe/store/actions/fetchBalances'
import { EMPTY_DATA } from '~/wallets/ethTransactions'
import { toNative } from '~/wallets/tokens'
import actions, { type Actions } from './actions'
@ -21,7 +21,7 @@ const getSteps = () => [
type Props = SelectorProps & Actions & {
safe: Safe,
balance: Balance,
token: Token,
onReset: () => void,
}
@ -40,16 +40,16 @@ const getTransferData = async (tokenAddress: string, to: string, amount: BigNumb
return myToken.contract.transfer.getData(to, amount)
}
const processTokenTransfer = async (safe: Safe, balance: Balance, to: string, amount: number, userAddress: string) => {
const symbol = balance.get('symbol')
const processTokenTransfer = async (safe: Safe, token: Token, to: string, amount: number, userAddress: string) => {
const symbol = token.get('symbol')
const nonce = Date.now()
const name = `Send ${amount} ${balance.get('symbol')} to ${to}`
const name = `Send ${amount} ${token.get('symbol')} to ${to}`
const value = isEther(symbol) ? amount : 0
const tokenAddress = balance.get('address')
const tokenAddress = token.get('address')
const destination = isEther(symbol) ? to : tokenAddress
const data = isEther(symbol)
? EMPTY_DATA
: await getTransferData(tokenAddress, to, await toNative(amount, balance.get('decimals')))
: await getTransferData(tokenAddress, to, await toNative(amount, token.get('decimals')))
return createTransaction(safe, name, destination, value, nonce, userAddress, data)
}
@ -61,12 +61,12 @@ class SendToken extends React.Component<Props, State> {
onTransaction = async (values: Object) => {
try {
const { safe, balance, userAddress } = this.props
const { safe, token, userAddress } = this.props
const amount = values[TKN_VALUE_PARAM]
const destination = values[TKN_DESTINATION_PARAM]
await processTokenTransfer(safe, balance, destination, amount, userAddress)
await processTokenTransfer(safe, token, destination, amount, userAddress)
await sleep(1500)
this.props.fetchTransactions()
this.setState({ done: true })
@ -84,10 +84,10 @@ class SendToken extends React.Component<Props, State> {
render() {
const { done } = this.state
const { balance } = this.props
const { token } = this.props
const steps = getSteps()
const finishedButton = <Stepper.FinishButton title={SEE_TXS_BUTTON_TEXT} />
const symbol = balance.get('symbol')
const symbol = token.get('symbol')
return (
<React.Fragment>
@ -98,7 +98,7 @@ class SendToken extends React.Component<Props, State> {
steps={steps}
onReset={this.onReset}
>
<Stepper.Page funds={balance.get('funds')} symbol={symbol}>
<Stepper.Page funds={token.get('funds')} symbol={symbol}>
{ SendTokenForm }
</Stepper.Page>
<Stepper.Page symbol={symbol}>

View File

@ -1,13 +1,13 @@
// @flow
import fetchSafe from '~/routes/safe/store/actions/fetchSafe'
import { fetchBalances } from '~/routes/safe/store/actions/fetchBalances'
import { fetchTokens } from '~/routes/tokens/store/actions/fetchTokens'
export type Actions = {
fetchSafe: typeof fetchSafe,
fetchBalances: typeof fetchBalances,
fetchTokens: typeof fetchTokens,
}
export default {
fetchSafe,
fetchBalances,
fetchTokens,
}

View File

@ -16,12 +16,12 @@ const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 15000
class SafeView extends React.PureComponent<Props> {
componentDidMount() {
this.intervalId = setInterval(() => {
const { safe, fetchBalances, fetchSafe } = this.props
const { safe, fetchTokens, fetchSafe } = this.props
if (!safe) {
return
}
const safeAddress = safe.get('address')
fetchBalances(safeAddress)
fetchTokens(safeAddress)
fetchSafe(safe)
}, TIMEOUT)
}
@ -33,7 +33,7 @@ class SafeView extends React.PureComponent<Props> {
if (this.props.safe) {
const safeAddress = this.props.safe.get('address')
this.props.fetchBalances(safeAddress)
this.props.fetchTokens(safeAddress)
}
}
@ -45,13 +45,13 @@ class SafeView extends React.PureComponent<Props> {
render() {
const {
safe, provider, balances, granted, userAddress,
safe, provider, tokens, granted, userAddress,
} = this.props
return (
<Page>
{ granted
? <Layout balances={balances} provider={provider} safe={safe} userAddress={userAddress} />
? <Layout tokens={tokens} provider={provider} safe={safe} userAddress={userAddress} />
: <NoRights />
}
</Page>

View File

@ -1,18 +1,19 @@
// @flow
import { List, Map } from 'immutable'
import { createSelector, createStructuredSelector, type Selector } from 'reselect'
import { balanceSelector, safeSelector, type RouterProps, type SafeSelectorProps } from '~/routes/safe/store/selectors'
import { safeSelector, type RouterProps, type SafeSelectorProps } from '~/routes/safe/store/selectors'
import { providerNameSelector, userAccountSelector } from '~/wallets/store/selectors/index'
import { type Safe } from '~/routes/safe/store/model/safe'
import { type Owner } from '~/routes/safe/store/model/owner'
import { type GlobalState } from '~/store/index'
import { sameAddress } from '~/wallets/ethAddresses'
import { type Balance } from '~/routes/safe/store/model/balance'
import { tokensSelector } from '~/routes/tokens/store/selectors'
import { type Token } from '~/routes/tokens/store/model/token'
export type SelectorProps = {
safe: SafeSelectorProps,
provider: string,
balances: Map<string, Balance>,
tokens: Map<string, Token>,
userAddress: string,
}
@ -40,7 +41,7 @@ export const grantedSelector: Selector<GlobalState, RouterProps, boolean> = crea
export default createStructuredSelector({
safe: safeSelector,
provider: providerNameSelector,
balances: balanceSelector,
tokens: tokensSelector,
granted: grantedSelector,
userAddress: userAccountSelector,
})

View File

@ -1,21 +0,0 @@
// @flow
import { Map } from 'immutable'
import { createAction } from 'redux-actions'
import { type Balance } from '~/routes/safe/store/model/balance'
export const ADD_BALANCES = 'ADD_BALANCES'
type BalanceProps = {
safeAddress: string,
balances: Map<string, Balance>,
}
const addBalances = createAction(
ADD_BALANCES,
(safeAddress: string, balances: Map<string, Balance>): BalanceProps => ({
safeAddress,
balances,
}),
)
export default addBalances

View File

@ -11,15 +11,23 @@ const buildSafesFrom = async (loadedSafes: Object): Promise<Map<string, Safe>> =
const safes = Map()
const keys = Object.keys(loadedSafes)
const safeRecords = await Promise.all(keys.map((address: string) => buildSafe(loadedSafes[address])))
try {
const safeRecords = await Promise.all(keys.map((address: string) => buildSafe(loadedSafes[address])))
return safes.withMutations(async (map) => {
safeRecords.forEach((safe: Safe) => map.set(safe.get('address'), safe))
})
return safes.withMutations(async (map) => {
safeRecords.forEach((safe: Safe) => map.set(safe.get('address'), safe))
})
} catch (err) {
// eslint-disable-next-line
console.log("Error while fetching safes information")
return Map()
}
}
export default () => async (dispatch: ReduxDispatch<GlobalState>) => {
const storedSafes = load(SAFES_KEY)
const safes = storedSafes ? await buildSafesFrom(storedSafes) : Map()
return dispatch(updateSafes(safes))

View File

@ -1,20 +0,0 @@
// @flow
import { Map } from 'immutable'
import { handleActions, type ActionType } from 'redux-actions'
import addBalances, { ADD_BALANCES } from '~/routes/safe/store/actions/addBalances'
import { type Balance } from '~/routes/safe/store/model/balance'
export const BALANCE_REDUCER_ID = 'balances'
export type State = Map<string, Map<string, Balance>>
export default handleActions({
[ADD_BALANCES]: (state: State, action: ActionType<typeof addBalances>): State =>
state.update(action.payload.safeAddress, (prevSafe: Map<string, Balance>) => {
if (!prevSafe) {
return action.payload.balances
}
return prevSafe.equals(action.payload.balances) ? prevSafe : action.payload.balances
}),
}, Map())

View File

@ -6,11 +6,9 @@ import { type GlobalState } from '~/store/index'
import { SAFE_PARAM_ADDRESS } from '~/routes/routes'
import { type Safe } from '~/routes/safe/store/model/safe'
import { safesMapSelector } from '~/routes/safeList/store/selectors'
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'
import { type Balance } from '~/routes/safe/store/model/balance'
export type RouterProps = {
match: Match,
@ -26,14 +24,12 @@ type TransactionProps = {
const safePropAddressSelector = (state: GlobalState, props: SafeProps) => props.safeAddress
const safeParamAddressSelector = (state: GlobalState, props: RouterProps) => props.match.params[SAFE_PARAM_ADDRESS] || ''
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 safeParamAddressSelector = (state: GlobalState, props: RouterProps) => props.match.params[SAFE_PARAM_ADDRESS] || ''
export const safeTransactionsSelector: Selector<GlobalState, SafeProps, List<Transaction>> = createSelector(
transactionsSelector,
safePropAddressSelector,
@ -82,18 +78,6 @@ export const safeSelector: Selector<GlobalState, RouterProps, SafeSelectorProps>
},
)
export const balanceSelector: Selector<GlobalState, RouterProps, Map<string, Balance>> = createSelector(
balancesSelector,
safeParamAddressSelector,
(balances: Map<string, Map<string, Balance>>, address: string) => {
if (!address) {
return Map()
}
return balances.get(address) || Map()
},
)
export default createStructuredSelector({
safe: safeSelector,
})

View File

@ -29,7 +29,7 @@ const grantedSelectorTests = () => {
const reduxStore = {
safes: Map(),
providers: makeProvider(),
balances: Map(),
tokens: Map(),
transactions: Map(),
}
@ -67,7 +67,7 @@ const grantedSelectorTests = () => {
const reduxStore = {
safes: Map(),
providers: makeProvider(),
balances: Map(),
tokens: Map(),
transactions: Map(),
}
@ -82,7 +82,7 @@ const grantedSelectorTests = () => {
const reduxStore = {
safes: Map(),
providers: makeProvider(),
balances: Map(),
tokens: Map(),
transactions: Map(),
}

View File

@ -26,7 +26,7 @@ const grantedSelectorTests = () => {
const reduxStore = {
[SAFE_REDUCER_ID]: map,
providers: makeProvider(provider),
balances: undefined,
tokens: undefined,
transactions: undefined,
}
@ -47,7 +47,7 @@ const grantedSelectorTests = () => {
const reduxStore = {
[SAFE_REDUCER_ID]: map,
providers: makeProvider(provider),
balances: undefined,
tokens: undefined,
transactions: undefined,
}
@ -68,7 +68,7 @@ const grantedSelectorTests = () => {
const reduxStore = {
[SAFE_REDUCER_ID]: map,
providers: makeProvider(provider),
balances: undefined,
tokens: undefined,
transactions: undefined,
}

View File

@ -14,7 +14,7 @@ const safeSelectorTests = () => {
const reduxStore = {
[SAFE_REDUCER_ID]: Map(),
providers: undefined,
balances: undefined,
tokens: undefined,
transactions: undefined,
}
const match: Match = buildMathPropsFrom('fooAddress')
@ -38,7 +38,7 @@ const safeSelectorTests = () => {
const reduxStore = {
[SAFE_REDUCER_ID]: map,
providers: undefined,
balances: undefined,
tokens: undefined,
transactions: undefined,
}

View File

@ -14,7 +14,7 @@ const grantedSelectorTests = () => {
const reduxStore = {
[SAFE_REDUCER_ID]: Map(),
providers: makeProvider(),
balances: undefined,
tokens: undefined,
transactions: Map(),
}
@ -46,7 +46,7 @@ const grantedSelectorTests = () => {
const reduxStore = {
[SAFE_REDUCER_ID]: Map(),
providers: makeProvider(),
balances: undefined,
tokens: undefined,
transactions: Map({ fooAddress: List([transaction]) }),
}
@ -81,7 +81,7 @@ const grantedSelectorTests = () => {
const reduxStore = {
[SAFE_REDUCER_ID]: Map(),
providers: makeProvider(),
balances: undefined,
tokens: undefined,
transactions: Map({ fooAddress: List([transaction]) }),
}
@ -113,7 +113,7 @@ const grantedSelectorTests = () => {
const reduxStore = {
[SAFE_REDUCER_ID]: Map(),
providers: makeProvider(),
balances: undefined,
tokens: undefined,
transactions: Map({ fooAddress: List([transaction]) }),
}

View File

@ -21,7 +21,7 @@ const safesListSelectorTests = () => {
const reduxStore = {
[PROVIDER_REDUCER_ID]: walletRecord,
[SAFE_REDUCER_ID]: Map(),
balances: undefined,
tokens: undefined,
transactions: undefined,
}
const emptyList = List([])
@ -42,7 +42,7 @@ const safesListSelectorTests = () => {
const reduxStore = {
[PROVIDER_REDUCER_ID]: walletRecord,
[SAFE_REDUCER_ID]: map,
balances: undefined,
tokens: undefined,
transactions: undefined,
}
@ -62,7 +62,7 @@ const safesListSelectorTests = () => {
const reduxStore = {
[PROVIDER_REDUCER_ID]: walletRecord,
[SAFE_REDUCER_ID]: map,
balances: undefined,
tokens: undefined,
transactions: undefined,
}
@ -83,7 +83,7 @@ const safesListSelectorTests = () => {
const reduxStore = {
[SAFE_REDUCER_ID]: map,
[PROVIDER_REDUCER_ID]: walletRecord,
balances: undefined,
tokens: undefined,
transactions: undefined,
}
@ -105,7 +105,7 @@ const safesListSelectorTests = () => {
const reduxStore = {
[SAFE_REDUCER_ID]: map,
[PROVIDER_REDUCER_ID]: walletRecord,
balances: undefined,
tokens: undefined,
transactions: undefined,
}

View File

@ -0,0 +1,90 @@
// @flow
import * as MuiList from '@material-ui/core/List'
import * as React from 'react'
import Block from '~/components/layout/Block'
import Col from '~/components/layout/Col'
import Bold from '~/components/layout/Bold'
import Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
import { type Token } from '~/routes/tokens/store/model/token'
import { type SelectorProps } from '~/routes/tokens/container/selector'
import { type Actions } from '~/routes/tokens/container/actions'
import TokenComponent from './Token'
// import AddToken from '~/routes/tokens/component/AddToken'
// import RemoveToken from '~/routes/tokens/component/RemoveToken'
const safeIcon = require('~/routes/safe/component/Safe/assets/gnosis_safe.svg')
type TokenProps = SelectorProps & Actions
type State = {
component: React$Node,
}
const listStyle = {
width: '100%',
}
class TokenLayout extends React.PureComponent<TokenProps, State> {
state = {
component: undefined,
}
/*
onAddToken = () => {
const { addresses } = this.props
this.setState({ component: <AddToken/> })
}
onRemoveToken = () => {
this.setState({ component: <RemoveToken /> })
}
*/
onEnableToken = (token: Token) => {
const { enableToken, safe } = this.props
const safeAddress = safe.get('address')
enableToken(safeAddress, token)
}
onDisableToken = (token: Token) => {
const { disableToken, safe } = this.props
const safeAddress = safe.get('address')
disableToken(safeAddress, token)
}
render() {
const { safe, tokens } = this.props
const { component } = this.state
const name = safe ? safe.get('name') : ''
return (
<Row grow>
<Col sm={12} top="xs" md={5} margin="xl" overflow>
<MuiList style={listStyle}>
{tokens.map((token: Token) => (<TokenComponent
token={token}
onDisableToken={this.onDisableToken}
onEnableToken={this.onEnableToken}
/>))}
</MuiList>
</Col>
<Col sm={12} center="xs" md={7} margin="xl" layout="column">
<Block margin="xl">
<Paragraph size="lg" noMargin align="right">
<Bold>{name}</Bold>
</Paragraph>
</Block>
<Row grow>
<Col sm={12} center={component ? undefined : 'sm'} middle={component ? undefined : 'sm'} layout="column">
{ component || <Img alt="Safe Icon" src={safeIcon} height={330} /> }
</Col>
</Row>
</Col>
</Row>
)
}
}
export default TokenLayout

View File

@ -0,0 +1,107 @@
// @flow
import * as React from 'react'
import { type Token } from '~/routes/tokens/store/model/token'
import { withStyles } from '@material-ui/core/styles'
import Block from '~/components/layout/Block'
import Bold from '~/components/layout/Bold'
import Checkbox from '@material-ui/core/Checkbox'
import Card from '@material-ui/core/Card'
import CardContent from '@material-ui/core/CardContent'
import CardMedia from '@material-ui/core/CardMedia'
import Typography from '@material-ui/core/Typography'
// import Delete from '@material-ui/icons/Delete'
// import IconButton from '@material-ui/core/IconButton'
import { type WithStyles } from '~/theme/mui'
type Props = WithStyles & {
token: Token,
onRemoveToken: (balance: Token)=> void,
onEnableToken: (token: Token) => void,
onDisableToken: (token: Token) => void,
}
type State = {
checked: boolean,
}
const styles = theme => ({
card: {
display: 'flex',
},
details: {
display: 'flex',
flexDirection: 'column',
},
content: {
flex: '1 0 auto',
},
cover: {
width: 45,
height: 45,
},
controls: {
display: 'flex',
alignItems: 'center',
paddingLeft: theme.spacing.unit,
paddingBottom: theme.spacing.unit,
},
playIcon: {
height: 38,
width: 38,
},
})
class TokenComponent extends React.Component<Props, State> {
state = {
checked: true,
}
// onRemoveClick = () => this.props.onRemoveToken(this.props.token)
handleChange = (e: SyntheticInputEvent<HTMLInputElement>) => {
const { checked } = e.target
const callback = checked ? this.props.onDisableToken : this.props.onDisableToken
this.setState(() => ({ checked: e.target.checked }), () => callback(this.props.token))
}
render() {
const { classes, token } = this.props
const name = token.get('name')
const symbol = token.get('symbol')
return (
<Card className={classes.card}>
<Block className={classes.details}>
<CardContent className={classes.content}>
<Typography variant="headline">{name}</Typography>
<Typography variant="subheading" color="textSecondary">
{symbol}
</Typography>
</CardContent>
<Block className={classes.controls}>
<Bold>
{symbol}
</Bold>
<Checkbox
checked={this.state.checked}
onChange={this.handleChange}
color="primary"
/>
{/*
<IconButton aria-label="Delete" onClick={this.onRemoveClick}>
<Delete />
</IconButton>
*/}
</Block>
</Block>
<CardMedia
className={classes.cover}
image={token.get('logoUrl')}
title={name}
/>
</Card>
)
}
}
export default withStyles(styles, { withTheme: true })(TokenComponent)

View File

@ -0,0 +1,13 @@
// @flow
import enableToken from '~/routes/tokens/store/actions/enableToken'
import disableToken from '~/routes/tokens/store/actions/disableToken'
export type Actions = {
enableToken: typeof enableToken,
disableToken: typeof disableToken,
}
export default {
enableToken,
disableToken,
}

View File

@ -0,0 +1,31 @@
// @flow
import * as React from 'react'
import { connect } from 'react-redux'
import Page from '~/components/layout/Page'
import Layout from '~/routes/tokens/component/Layout'
import selector, { type SelectorProps } from './selector'
import actions, { type Actions } from './actions'
type Props = Actions & SelectorProps
class TokensView extends React.PureComponent<Props> {
render() {
const {
tokens, addresses, safe, disableToken, enableToken,
} = this.props
return (
<Page>
<Layout
tokens={tokens}
addresses={addresses}
safe={safe}
disableToken={disableToken}
enableToken={enableToken}
/>
</Page>
)
}
}
export default connect(selector, actions)(TokensView)

View File

@ -0,0 +1,19 @@
// @flow
import { List } from 'immutable'
import { createStructuredSelector } from 'reselect'
import { tokenListSelector, tokenAddressesSelector } from '~/routes/tokens/store/selectors'
import { type Safe } from '~/routes/safe/store/model/safe'
import { safeSelector } from '~/routes/safe/store/selectors'
import { type Token } from '~/routes/tokens/store/model/token'
export type SelectorProps = {
tokens: List<Token>,
addresses: List<String>,
safe: Safe,
}
export default createStructuredSelector({
safe: safeSelector,
tokens: tokenListSelector,
addresses: tokenAddressesSelector,
})

View File

@ -0,0 +1,21 @@
// @flow
import { Map } from 'immutable'
import { createAction } from 'redux-actions'
import { type Token } from '~/routes/tokens/store/model/token'
export const ADD_TOKENS = 'ADD_TOKENS'
type TokenProps = {
safeAddress: string,
tokens: Map<string, Token>,
}
const addTokens = createAction(
ADD_TOKENS,
(safeAddress: string, tokens: Map<string, Token>): TokenProps => ({
safeAddress,
tokens,
}),
)
export default addTokens

View File

@ -0,0 +1,15 @@
// @flow
import { createAction } from 'redux-actions'
import { type Token } from '~/routes/tokens/store/model/token'
export const DISABLE_TOKEN = 'DISABLE_TOKEN'
const disableToken = createAction(
DISABLE_TOKEN,
(safeAddress: string, token: Token) => ({
safeAddress,
symbol: token.get('symbol'),
}),
)
export default disableToken

View File

@ -0,0 +1,15 @@
// @flow
import { createAction } from 'redux-actions'
import { type Token } from '~/routes/tokens/store/model/token'
export const ENABLE_TOKEN = 'ENABLE_TOKEN'
const enableToken = createAction(
ENABLE_TOKEN,
(safeAddress: string, token: Token) => ({
safeAddress,
symbol: token.get('symbol'),
}),
)
export default enableToken

View File

@ -5,10 +5,10 @@ import type { Dispatch as ReduxDispatch } from 'redux'
import StandardToken from '@gnosis.pm/util-contracts/build/contracts/StandardToken.json'
import { getBalanceInEtherOf, getWeb3 } from '~/wallets/getWeb3'
import { type GlobalState } from '~/store/index'
import { makeBalance, type Balance, type BalanceProps } from '~/routes/safe/store/model/balance'
import { makeToken, type Token, type TokenProps } from '~/routes/tokens/store/model/token'
import logo from '~/assets/icons/icon_etherTokens.svg'
import { ensureOnce } from '~/utils/singleton'
import addBalances from './addBalances'
import addTokens from './addTokens'
const createStandardTokenContract = async () => {
@ -29,9 +29,9 @@ export const calculateBalanceOf = async (tokenAddress: string, address: string,
.catch(() => '0')
}
export const fetchBalances = (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
export const fetchTokens = (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
const balance = await getBalanceInEtherOf(safeAddress)
const ethBalance = makeBalance({
const ethBalance = makeToken({
address: '0',
name: 'Ether',
symbol: 'ETH',
@ -57,17 +57,17 @@ export const fetchBalances = (safeAddress: string) => async (dispatch: ReduxDisp
const json = await response.json()
try {
const balancesRecords = await Promise.all(json.map(async (item: BalanceProps) => {
const balancesRecords = await Promise.all(json.map(async (item: TokenProps) => {
const funds = await calculateBalanceOf(item.address, safeAddress, item.decimals)
return makeBalance({ ...item, funds })
return makeToken({ ...item, funds })
}))
const balances: Map<string, Balance> = Map().withMutations((map) => {
const balances: Map<string, Token> = Map().withMutations((map) => {
balancesRecords.forEach(record => map.set(record.get('symbol'), record))
map.set('ETH', ethBalance)
})
return dispatch(addBalances(safeAddress, balances))
return dispatch(addTokens(safeAddress, balances))
} catch (err) {
// eslint-disable-next-line
console.log("Error fetching token balances...")

View File

@ -2,22 +2,26 @@
import { Record } from 'immutable'
import type { RecordFactory, RecordOf } from 'immutable'
export type BalanceProps = {
export type TokenProps = {
address: string,
name: string,
symbol: string,
decimals: number,
logoUrl: string,
funds: string,
status: boolean,
removable: boolean,
}
export const makeBalance: RecordFactory<BalanceProps> = Record({
export const makeToken: RecordFactory<TokenProps> = Record({
address: '',
name: '',
symbol: '',
decimals: 0,
logoUrl: '',
funds: '0',
status: true,
removable: false,
})
export type Balance = RecordOf<BalanceProps>
export type Token = RecordOf<TokenProps>

View File

@ -0,0 +1,26 @@
// @flow
import { Map } from 'immutable'
import { handleActions, type ActionType } from 'redux-actions'
import addTokens, { ADD_TOKENS } from '~/routes/tokens/store/actions/addTokens'
import { type Token } from '~/routes/tokens/store/model/token'
import disableToken, { DISABLE_TOKEN } from '~/routes/tokens/store/actions/disableToken'
import enableToken, { ENABLE_TOKEN } from '~/routes/tokens/store/actions/enableToken'
export const TOKEN_REDUCER_ID = 'tokens'
export type State = Map<string, Map<string, Token>>
export default handleActions({
[ADD_TOKENS]: (state: State, action: ActionType<typeof addTokens>): State =>
state.update(action.payload.safeAddress, (prevSafe: Map<string, Token>) => {
if (!prevSafe) {
return action.payload.tokens
}
return prevSafe.equals(action.payload.tokens) ? prevSafe : action.payload.tokens
}),
[DISABLE_TOKEN]: (state: State, action: ActionType<typeof disableToken>): State =>
state.setIn([action.payload.safeAddress, action.payload.symbol, 'status'], false),
[ENABLE_TOKEN]: (state: State, action: ActionType<typeof enableToken>): State =>
state.setIn([action.payload.safeAddress, action.payload.symbol, 'status'], true),
}, Map())

View File

@ -0,0 +1,36 @@
// @flow
import { List, Map } from 'immutable'
import { createSelector, type Selector } from 'reselect'
import { safeParamAddressSelector, type RouterProps } from '~/routes/safe/store/selectors'
import { type GlobalState } from '~/store'
import { TOKEN_REDUCER_ID } from '~/routes/tokens/store/reducer/tokens'
import { type Token } from '~/routes/tokens/store/model/token'
const balancesSelector = (state: GlobalState) => state[TOKEN_REDUCER_ID]
export const tokensSelector: Selector<GlobalState, RouterProps, Map<string, Token>> = createSelector(
balancesSelector,
safeParamAddressSelector,
(balances: Map<string, Map<string, Token>>, address: string) => {
if (!address) {
return Map()
}
return balances.get(address) || Map()
},
)
export const tokenListSelector = createSelector(
tokensSelector,
(balances: Map<string, Token>) => balances.toList(),
)
export const tokenAddressesSelector = createSelector(
tokenListSelector,
(balances: List<Token>) => {
const addresses = List().withMutations(list =>
balances.map(token => list.push(token.address)))
return addresses
},
)

View File

@ -5,7 +5,7 @@ import { combineReducers, createStore, applyMiddleware, compose, type Reducer, t
import thunk from 'redux-thunk'
import provider, { PROVIDER_REDUCER_ID, type State as ProviderState } from '~/wallets/store/reducer/provider'
import safe, { SAFE_REDUCER_ID, type State as SafeState } from '~/routes/safe/store/reducer/safe'
import balances, { BALANCE_REDUCER_ID, type State as BalancesState } from '~/routes/safe/store/reducer/balances'
import tokens, { TOKEN_REDUCER_ID, type State as TokensState } from '~/routes/tokens/store/reducer/tokens'
import transactions, { type State as TransactionsState, transactionsInitialState, TRANSACTIONS_REDUCER_ID } from '~/routes/safe/store/reducer/transactions'
export const history = createBrowserHistory()
@ -20,7 +20,7 @@ const finalCreateStore = composeEnhancers(applyMiddleware(
export type GlobalState = {
providers: ProviderState,
safes: SafeState,
balances: BalancesState,
tokens: TokensState,
transactions: TransactionsState,
}
@ -28,7 +28,7 @@ const reducers: Reducer<GlobalState> = combineReducers({
routing: routerReducer,
[PROVIDER_REDUCER_ID]: provider,
[SAFE_REDUCER_ID]: safe,
[BALANCE_REDUCER_ID]: balances,
[TOKEN_REDUCER_ID]: tokens,
[TRANSACTIONS_REDUCER_ID]: transactions,
})

View File

@ -1,7 +1,7 @@
// @flow
import TestUtils from 'react-dom/test-utils'
import * as fetchBalancesAction from '~/routes/safe/store/actions/fetchBalances'
import * as fetchBalancesAction from '~/routes/tokens/store/actions/fetchTokens'
import { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { addTknTo, getTokenContract } from '~/test/utils/tokenMovements'

View File

@ -1,10 +1,10 @@
// @flow
import { Map } from 'immutable'
import { BALANCE_REDUCER_ID } from '~/routes/safe/store/reducer/balances'
import * as fetchBalancesAction from '~/routes/safe/store/actions/fetchBalances'
import * as fetchTokensAction from '~/routes/tokens/store/actions/fetchTokens'
import { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { type Balance } from '~/routes/safe/store/model/balance'
import { type Token } from '~/routes/tokens/store/model/token'
import { TOKEN_REDUCER_ID } from '~/routes/tokens/store/reducer/tokens'
import { addEtherTo, addTknTo } from '~/test/utils/tokenMovements'
import { dispatchTknBalance } from '~/test/utils/transactions/moveTokens.helper'
@ -21,13 +21,13 @@ describe('Safe - redux balance property', () => {
const tokenList = ['WE', '<3', 'GNO', 'OMG', 'RDN']
// WHEN
await store.dispatch(fetchBalancesAction.fetchBalances(address))
await store.dispatch(fetchTokensAction.fetchTokens(address))
// THEN
const balances: Map<string, Map<string, Balance>> | typeof undefined = store.getState()[BALANCE_REDUCER_ID]
if (!balances) throw new Error()
const tokens: Map<string, Map<string, Token>> | typeof undefined = store.getState()[TOKEN_REDUCER_ID]
if (!tokens) throw new Error()
const safeBalances: Map<string, Balance> | typeof undefined = balances.get(address)
const safeBalances: Map<string, Token> | typeof undefined = tokens.get(address)
if (!safeBalances) throw new Error()
expect(safeBalances.size).toBe(6)
@ -41,13 +41,13 @@ describe('Safe - redux balance property', () => {
it('reducer should return 0.03456 ETH as funds to safe with 0.03456 ETH', async () => {
// WHEN
await addEtherTo(address, '0.03456')
await store.dispatch(fetchBalancesAction.fetchBalances(address))
await store.dispatch(fetchTokensAction.fetchTokens(address))
// THEN
const balances: Map<string, Map<string, Balance>> | typeof undefined = store.getState()[BALANCE_REDUCER_ID]
if (!balances) throw new Error()
const tokens: Map<string, Map<string, Token>> | typeof undefined = store.getState()[TOKEN_REDUCER_ID]
if (!tokens) throw new Error()
const safeBalances: Map<string, Balance> | typeof undefined = balances.get(address)
const safeBalances: Map<string, Token> | typeof undefined = tokens.get(address)
if (!safeBalances) throw new Error()
expect(safeBalances.size).toBe(6)
@ -65,7 +65,7 @@ describe('Safe - redux balance property', () => {
await dispatchTknBalance(store, tokenAddress, address)
// THEN
const safeBalances = store.getState()[BALANCE_REDUCER_ID].get(address)
const safeBalances = store.getState()[TOKEN_REDUCER_ID].get(address)
expect(safeBalances.size).toBe(1)
const tknBalance = safeBalances.get('TKN')

View File

@ -2,12 +2,12 @@
import { Map } from 'immutable'
import TestUtils from 'react-dom/test-utils'
import { sleep } from '~/utils/timer'
import * as fetchBalancesAction from '~/routes/safe/store/actions/fetchBalances'
import * as fetchTokensAction from '~/routes/tokens/store/actions/fetchTokens'
import { checkMinedTx, checkPendingTx, EXPAND_BALANCE_INDEX } from '~/test/builder/safe.dom.utils'
import { makeBalance, type Balance } from '~/routes/safe/store/model/balance'
import addBalances from '~/routes/safe/store/actions/addBalances'
import { whenExecuted } from '~/test/utils/logTransactions'
import SendToken from '~/routes/safe/component/SendToken'
import { makeToken, type Token } from '~/routes/tokens/store/model/token'
import addTokens from '~/routes/tokens/store/actions/addTokens'
export const sendMoveTokensForm = async (
SafeDom: React$Component<any, any>,
@ -44,9 +44,9 @@ export const sendMoveTokensForm = async (
}
export const dispatchTknBalance = async (store: Store, tokenAddress: string, address: string) => {
const fetchBalancesMock = jest.spyOn(fetchBalancesAction, 'fetchBalances')
const funds = await fetchBalancesAction.calculateBalanceOf(tokenAddress, address, 18)
const balances: Map<string, Balance> = Map().set('TKN', makeBalance({
const fetchBalancesMock = jest.spyOn(fetchTokensAction, 'fetchTokens')
const funds = await fetchTokensAction.calculateBalanceOf(tokenAddress, address, 18)
const balances: Map<string, Token> = Map().set('TKN', makeToken({
address: tokenAddress,
name: 'Token',
symbol: 'TKN',
@ -54,8 +54,8 @@ export const dispatchTknBalance = async (store: Store, tokenAddress: string, add
logoUrl: 'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true',
funds,
}))
fetchBalancesMock.mockImplementation(() => store.dispatch(addBalances(address, balances)))
await store.dispatch(fetchBalancesAction.fetchBalances(address))
fetchBalancesMock.mockImplementation(() => store.dispatch(addTokens(address, balances)))
await store.dispatch(fetchTokensAction.fetchTokens(address))
fetchBalancesMock.mockRestore()
}