From afe7d4fc2f10b74771687c4dda91859a7548259d Mon Sep 17 00:00:00 2001 From: apanizo Date: Thu, 5 Jul 2018 17:56:36 +0200 Subject: [PATCH 01/15] WA-232 Reusing standard token --- src/routes/safe/store/actions/fetchBalances.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/routes/safe/store/actions/fetchBalances.js b/src/routes/safe/store/actions/fetchBalances.js index 76f2705a..d833b9c4 100644 --- a/src/routes/safe/store/actions/fetchBalances.js +++ b/src/routes/safe/store/actions/fetchBalances.js @@ -7,9 +7,11 @@ 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 logo from '~/assets/icons/icon_etherTokens.svg' +import { ensureOnce } from '~/utils/singleton' import addBalances from './addBalances' -export const getStandardTokenContract = async () => { + +const createStandardTokenContract = async () => { const web3 = getWeb3() const erc20Token = await contract(StandardToken) erc20Token.setProvider(web3.currentProvider) @@ -17,6 +19,8 @@ export const getStandardTokenContract = async () => { return erc20Token } +export const getStandardTokenContract = ensureOnce(createStandardTokenContract) + export const calculateBalanceOf = async (tokenAddress: string, address: string, decimals: number) => { const erc20Token = await getStandardTokenContract() From d7193dcc9a52e9d172173b15de68160c10973057 Mon Sep 17 00:00:00 2001 From: apanizo Date: Fri, 6 Jul 2018 11:06:32 +0200 Subject: [PATCH 02/15] WA-232 Do not update redux if error is raised fetching balance or safe information --- src/routes/safe/store/actions/fetchBalances.js | 18 +++++++++++++----- src/routes/safe/store/actions/fetchSafe.js | 11 +++++++++-- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/routes/safe/store/actions/fetchBalances.js b/src/routes/safe/store/actions/fetchBalances.js index d833b9c4..764b8eb3 100644 --- a/src/routes/safe/store/actions/fetchBalances.js +++ b/src/routes/safe/store/actions/fetchBalances.js @@ -55,15 +55,23 @@ export const fetchBalances = (safeAddress: string) => async (dispatch: ReduxDisp } const json = await response.json() - return Promise.all(json.map(async (item: BalanceProps) => { - const funds = await calculateBalanceOf(item.address, safeAddress, item.decimals) - return makeBalance({ ...item, funds }) - })).then((balancesRecords) => { + + try { + const balancesRecords = await Promise.all(json.map(async (item: BalanceProps) => { + const funds = await calculateBalanceOf(item.address, safeAddress, item.decimals) + return makeBalance({ ...item, funds }) + })) + const balances: Map = Map().withMutations((map) => { balancesRecords.forEach(record => map.set(record.get('symbol'), record)) map.set('ETH', ethBalance) }) return dispatch(addBalances(safeAddress, balances)) - }) + } catch (err) { + // eslint-disable-next-line + console.log("Error fetching token balances...") + + return Promise.resolve() + } } diff --git a/src/routes/safe/store/actions/fetchSafe.js b/src/routes/safe/store/actions/fetchSafe.js index 9e9a48ff..7f41f8b9 100644 --- a/src/routes/safe/store/actions/fetchSafe.js +++ b/src/routes/safe/store/actions/fetchSafe.js @@ -37,7 +37,14 @@ export const buildSafe = async (storedSafe: Object) => { } export default (safe: Safe) => async (dispatch: ReduxDispatch) => { - const safeRecord = await buildSafe(safe.toJSON()) + try { + const safeRecord = await buildSafe(safe.toJSON()) - return dispatch(updateSafe(safeRecord)) + return dispatch(updateSafe(safeRecord)) + } catch (err) { + // eslint-disable-next-line + console.log("Error while updating safe information") + + return Promise.resolve() + } } From 0b5d14c8f23328b3a37ae698f1ceca8f9618cdb3 Mon Sep 17 00:00:00 2001 From: apanizo Date: Tue, 10 Jul 2018 13:01:20 +0200 Subject: [PATCH 03/15] WA-232 tokens route --- src/routes/safe/component/Layout.jsx | 4 +- src/routes/safe/component/Layout.stories.js | 12 +- .../safe/component/Safe/BalanceInfo.jsx | 24 ++-- src/routes/safe/component/Safe/index.jsx | 16 +-- src/routes/safe/component/SendToken/index.jsx | 26 ++--- src/routes/safe/container/actions.js | 6 +- src/routes/safe/container/index.jsx | 10 +- src/routes/safe/container/selector.js | 9 +- src/routes/safe/store/actions/addBalances.js | 21 ---- src/routes/safe/store/actions/fetchSafes.js | 16 ++- src/routes/safe/store/reducer/balances.js | 20 ---- src/routes/safe/store/selectors/index.js | 20 +--- .../safe/store/test/confirmations.selector.js | 6 +- .../safe/store/test/granted.selector.js | 6 +- src/routes/safe/store/test/safe.selector.js | 4 +- .../safe/store/test/transactions.selector.js | 8 +- .../safeList/store/test/safes.selector.js | 10 +- src/routes/tokens/component/Layout.jsx | 90 +++++++++++++++ src/routes/tokens/component/Token/index.jsx | 107 ++++++++++++++++++ src/routes/tokens/container/actions.js | 13 +++ src/routes/tokens/container/index.jsx | 31 +++++ src/routes/tokens/container/selector.js | 19 ++++ src/routes/tokens/store/actions/addTokens.js | 21 ++++ .../tokens/store/actions/disableToken.js | 15 +++ .../tokens/store/actions/enableToken.js | 15 +++ .../store/actions/fetchTokens.js} | 16 +-- .../store/model/token.js} | 10 +- src/routes/tokens/store/reducer/tokens.js | 26 +++++ src/routes/tokens/store/selectors/index.js | 36 ++++++ src/store/index.js | 6 +- src/test/safe.dom.tokens.test.js | 2 +- src/test/safe.redux.balance.test.js | 24 ++-- .../utils/transactions/moveTokens.helper.js | 16 +-- 33 files changed, 497 insertions(+), 168 deletions(-) delete mode 100644 src/routes/safe/store/actions/addBalances.js delete mode 100644 src/routes/safe/store/reducer/balances.js create mode 100644 src/routes/tokens/component/Layout.jsx create mode 100644 src/routes/tokens/component/Token/index.jsx create mode 100644 src/routes/tokens/container/actions.js create mode 100644 src/routes/tokens/container/index.jsx create mode 100644 src/routes/tokens/container/selector.js create mode 100644 src/routes/tokens/store/actions/addTokens.js create mode 100644 src/routes/tokens/store/actions/disableToken.js create mode 100644 src/routes/tokens/store/actions/enableToken.js rename src/routes/{safe/store/actions/fetchBalances.js => tokens/store/actions/fetchTokens.js} (81%) rename src/routes/{safe/store/model/balance.js => tokens/store/model/token.js} (59%) create mode 100644 src/routes/tokens/store/reducer/tokens.js create mode 100644 src/routes/tokens/store/selectors/index.js diff --git a/src/routes/safe/component/Layout.jsx b/src/routes/safe/component/Layout.jsx index 4ae7803f..91254cad 100644 --- a/src/routes/safe/component/Layout.jsx +++ b/src/routes/safe/component/Layout.jsx @@ -7,11 +7,11 @@ import GnoSafe from './Safe' type Props = SelectorProps const Layout = ({ - safe, balances, provider, userAddress, + safe, tokens, provider, userAddress, }: Props) => ( { safe - ? + ? : } diff --git a/src/routes/safe/component/Layout.stories.js b/src/routes/safe/component/Layout.stories.js index 0c1cb5e9..b73000bb 100644 --- a/src/routes/safe/component/Layout.stories.js +++ b/src/routes/safe/component/Layout.stories.js @@ -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 => ( ) -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={() => {}} /> ) diff --git a/src/routes/safe/component/Safe/BalanceInfo.jsx b/src/routes/safe/component/Safe/BalanceInfo.jsx index dc46ac49..7f4c04f4 100644 --- a/src/routes/safe/component/Safe/BalanceInfo.jsx +++ b/src/routes/safe/component/Safe/BalanceInfo.jsx @@ -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, - onMoveFunds: (balance: Balance) => void, + tokens: Map, + 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 ( @@ -53,18 +53,18 @@ const BalanceComponent = openHoc(({ - {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 ( - {name} + {name} - + diff --git a/src/routes/safe/component/Safe/index.jsx b/src/routes/safe/component/Safe/index.jsx index f3404c60..bc800860 100644 --- a/src/routes/safe/component/Safe/index.jsx +++ b/src/routes/safe/component/Safe/index.jsx @@ -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, + tokens: Map, userAddress: string, } @@ -42,7 +42,7 @@ const listStyle = { width: '100%', } -const getEthBalanceFrom = (balances: Map) => { +const getEthBalanceFrom = (balances: Map) => { const ethBalance = balances.get('ETH') if (!ethBalance) { return 0 @@ -93,13 +93,13 @@ class GnoSafe extends React.PureComponent { this.setState({ component: }) } - onMoveTokens = (ercToken: Balance) => { + onMoveTokens = (ercToken: Token) => { const { safe } = this.props this.setState({ component: , @@ -107,15 +107,15 @@ class GnoSafe extends React.PureComponent { } 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 ( - + [ 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 { 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 { render() { const { done } = this.state - const { balance } = this.props + const { token } = this.props const steps = getSteps() const finishedButton = - const symbol = balance.get('symbol') + const symbol = token.get('symbol') return ( @@ -98,7 +98,7 @@ class SendToken extends React.Component { steps={steps} onReset={this.onReset} > - + { SendTokenForm } diff --git a/src/routes/safe/container/actions.js b/src/routes/safe/container/actions.js index ff9a91b4..7924354b 100644 --- a/src/routes/safe/container/actions.js +++ b/src/routes/safe/container/actions.js @@ -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, } diff --git a/src/routes/safe/container/index.jsx b/src/routes/safe/container/index.jsx index 2b1c9d0d..7a746e7c 100644 --- a/src/routes/safe/container/index.jsx +++ b/src/routes/safe/container/index.jsx @@ -16,12 +16,12 @@ const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 15000 class SafeView extends React.PureComponent { 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 { 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 { render() { const { - safe, provider, balances, granted, userAddress, + safe, provider, tokens, granted, userAddress, } = this.props return ( { granted - ? + ? : } diff --git a/src/routes/safe/container/selector.js b/src/routes/safe/container/selector.js index 45410f57..fce3795f 100644 --- a/src/routes/safe/container/selector.js +++ b/src/routes/safe/container/selector.js @@ -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, + tokens: Map, userAddress: string, } @@ -40,7 +41,7 @@ export const grantedSelector: Selector = crea export default createStructuredSelector({ safe: safeSelector, provider: providerNameSelector, - balances: balanceSelector, + tokens: tokensSelector, granted: grantedSelector, userAddress: userAccountSelector, }) diff --git a/src/routes/safe/store/actions/addBalances.js b/src/routes/safe/store/actions/addBalances.js deleted file mode 100644 index eb473d1e..00000000 --- a/src/routes/safe/store/actions/addBalances.js +++ /dev/null @@ -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, -} - -const addBalances = createAction( - ADD_BALANCES, - (safeAddress: string, balances: Map): BalanceProps => ({ - safeAddress, - balances, - }), -) - -export default addBalances diff --git a/src/routes/safe/store/actions/fetchSafes.js b/src/routes/safe/store/actions/fetchSafes.js index 196c1b40..45633a22 100644 --- a/src/routes/safe/store/actions/fetchSafes.js +++ b/src/routes/safe/store/actions/fetchSafes.js @@ -11,15 +11,23 @@ const buildSafesFrom = async (loadedSafes: Object): Promise> = 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) => { const storedSafes = load(SAFES_KEY) + const safes = storedSafes ? await buildSafesFrom(storedSafes) : Map() return dispatch(updateSafes(safes)) diff --git a/src/routes/safe/store/reducer/balances.js b/src/routes/safe/store/reducer/balances.js deleted file mode 100644 index 2bad95d0..00000000 --- a/src/routes/safe/store/reducer/balances.js +++ /dev/null @@ -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> - -export default handleActions({ - [ADD_BALANCES]: (state: State, action: ActionType): State => - state.update(action.payload.safeAddress, (prevSafe: Map) => { - if (!prevSafe) { - return action.payload.balances - } - - return prevSafe.equals(action.payload.balances) ? prevSafe : action.payload.balances - }), -}, Map()) diff --git a/src/routes/safe/store/selectors/index.js b/src/routes/safe/store/selectors/index.js index b1a9703f..7bbdd8b2 100644 --- a/src/routes/safe/store/selectors/index.js +++ b/src/routes/safe/store/selectors/index.js @@ -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> = createSelector( transactionsSelector, safePropAddressSelector, @@ -82,18 +78,6 @@ export const safeSelector: Selector }, ) -export const balanceSelector: Selector> = createSelector( - balancesSelector, - safeParamAddressSelector, - (balances: Map>, address: string) => { - if (!address) { - return Map() - } - - return balances.get(address) || Map() - }, -) - export default createStructuredSelector({ safe: safeSelector, }) diff --git a/src/routes/safe/store/test/confirmations.selector.js b/src/routes/safe/store/test/confirmations.selector.js index 3f1f924d..e1023985 100644 --- a/src/routes/safe/store/test/confirmations.selector.js +++ b/src/routes/safe/store/test/confirmations.selector.js @@ -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(), } diff --git a/src/routes/safe/store/test/granted.selector.js b/src/routes/safe/store/test/granted.selector.js index ff52fe15..cf51cd68 100644 --- a/src/routes/safe/store/test/granted.selector.js +++ b/src/routes/safe/store/test/granted.selector.js @@ -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, } diff --git a/src/routes/safe/store/test/safe.selector.js b/src/routes/safe/store/test/safe.selector.js index 22c027ae..f8f8b931 100644 --- a/src/routes/safe/store/test/safe.selector.js +++ b/src/routes/safe/store/test/safe.selector.js @@ -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, } diff --git a/src/routes/safe/store/test/transactions.selector.js b/src/routes/safe/store/test/transactions.selector.js index 5897ffd8..e45063f2 100644 --- a/src/routes/safe/store/test/transactions.selector.js +++ b/src/routes/safe/store/test/transactions.selector.js @@ -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]) }), } diff --git a/src/routes/safeList/store/test/safes.selector.js b/src/routes/safeList/store/test/safes.selector.js index b5946457..a5f20230 100644 --- a/src/routes/safeList/store/test/safes.selector.js +++ b/src/routes/safeList/store/test/safes.selector.js @@ -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, } diff --git a/src/routes/tokens/component/Layout.jsx b/src/routes/tokens/component/Layout.jsx new file mode 100644 index 00000000..90b461be --- /dev/null +++ b/src/routes/tokens/component/Layout.jsx @@ -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 { + state = { + component: undefined, + } + /* + onAddToken = () => { + const { addresses } = this.props + this.setState({ component: }) + } + + onRemoveToken = () => { + this.setState({ component: }) + } + */ + 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 ( + + + + {tokens.map((token: Token) => ())} + + + + + + {name} + + + + + { component || Safe Icon } + + + + + ) + } +} + +export default TokenLayout diff --git a/src/routes/tokens/component/Token/index.jsx b/src/routes/tokens/component/Token/index.jsx new file mode 100644 index 00000000..452355ce --- /dev/null +++ b/src/routes/tokens/component/Token/index.jsx @@ -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 { + state = { + checked: true, + } + + // onRemoveClick = () => this.props.onRemoveToken(this.props.token) + + handleChange = (e: SyntheticInputEvent) => { + 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 ( + + + + {name} + + {symbol} + + + + + {symbol} + + + {/* + + + + */} + + + + + ) + } +} + +export default withStyles(styles, { withTheme: true })(TokenComponent) diff --git a/src/routes/tokens/container/actions.js b/src/routes/tokens/container/actions.js new file mode 100644 index 00000000..5184fdcc --- /dev/null +++ b/src/routes/tokens/container/actions.js @@ -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, +} diff --git a/src/routes/tokens/container/index.jsx b/src/routes/tokens/container/index.jsx new file mode 100644 index 00000000..73754080 --- /dev/null +++ b/src/routes/tokens/container/index.jsx @@ -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 { + render() { + const { + tokens, addresses, safe, disableToken, enableToken, + } = this.props + + return ( + + + + ) + } +} + +export default connect(selector, actions)(TokensView) diff --git a/src/routes/tokens/container/selector.js b/src/routes/tokens/container/selector.js new file mode 100644 index 00000000..aac3a947 --- /dev/null +++ b/src/routes/tokens/container/selector.js @@ -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, + addresses: List, + safe: Safe, +} + +export default createStructuredSelector({ + safe: safeSelector, + tokens: tokenListSelector, + addresses: tokenAddressesSelector, +}) diff --git a/src/routes/tokens/store/actions/addTokens.js b/src/routes/tokens/store/actions/addTokens.js new file mode 100644 index 00000000..28705a89 --- /dev/null +++ b/src/routes/tokens/store/actions/addTokens.js @@ -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, +} + +const addTokens = createAction( + ADD_TOKENS, + (safeAddress: string, tokens: Map): TokenProps => ({ + safeAddress, + tokens, + }), +) + +export default addTokens diff --git a/src/routes/tokens/store/actions/disableToken.js b/src/routes/tokens/store/actions/disableToken.js new file mode 100644 index 00000000..d8b31edc --- /dev/null +++ b/src/routes/tokens/store/actions/disableToken.js @@ -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 diff --git a/src/routes/tokens/store/actions/enableToken.js b/src/routes/tokens/store/actions/enableToken.js new file mode 100644 index 00000000..e09f9d24 --- /dev/null +++ b/src/routes/tokens/store/actions/enableToken.js @@ -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 diff --git a/src/routes/safe/store/actions/fetchBalances.js b/src/routes/tokens/store/actions/fetchTokens.js similarity index 81% rename from src/routes/safe/store/actions/fetchBalances.js rename to src/routes/tokens/store/actions/fetchTokens.js index 764b8eb3..efea6f9d 100644 --- a/src/routes/safe/store/actions/fetchBalances.js +++ b/src/routes/tokens/store/actions/fetchTokens.js @@ -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) => { +export const fetchTokens = (safeAddress: string) => async (dispatch: ReduxDispatch) => { 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 = Map().withMutations((map) => { + const balances: Map = 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...") diff --git a/src/routes/safe/store/model/balance.js b/src/routes/tokens/store/model/token.js similarity index 59% rename from src/routes/safe/store/model/balance.js rename to src/routes/tokens/store/model/token.js index c8adf28a..713a68e3 100644 --- a/src/routes/safe/store/model/balance.js +++ b/src/routes/tokens/store/model/token.js @@ -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 = Record({ +export const makeToken: RecordFactory = Record({ address: '', name: '', symbol: '', decimals: 0, logoUrl: '', funds: '0', + status: true, + removable: false, }) -export type Balance = RecordOf +export type Token = RecordOf diff --git a/src/routes/tokens/store/reducer/tokens.js b/src/routes/tokens/store/reducer/tokens.js new file mode 100644 index 00000000..61db9c16 --- /dev/null +++ b/src/routes/tokens/store/reducer/tokens.js @@ -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> + +export default handleActions({ + [ADD_TOKENS]: (state: State, action: ActionType): State => + state.update(action.payload.safeAddress, (prevSafe: Map) => { + if (!prevSafe) { + return action.payload.tokens + } + + return prevSafe.equals(action.payload.tokens) ? prevSafe : action.payload.tokens + }), + [DISABLE_TOKEN]: (state: State, action: ActionType): State => + state.setIn([action.payload.safeAddress, action.payload.symbol, 'status'], false), + [ENABLE_TOKEN]: (state: State, action: ActionType): State => + state.setIn([action.payload.safeAddress, action.payload.symbol, 'status'], true), +}, Map()) diff --git a/src/routes/tokens/store/selectors/index.js b/src/routes/tokens/store/selectors/index.js new file mode 100644 index 00000000..e0baffa4 --- /dev/null +++ b/src/routes/tokens/store/selectors/index.js @@ -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> = createSelector( + balancesSelector, + safeParamAddressSelector, + (balances: Map>, address: string) => { + if (!address) { + return Map() + } + + return balances.get(address) || Map() + }, +) + +export const tokenListSelector = createSelector( + tokensSelector, + (balances: Map) => balances.toList(), +) + +export const tokenAddressesSelector = createSelector( + tokenListSelector, + (balances: List) => { + const addresses = List().withMutations(list => + balances.map(token => list.push(token.address))) + + return addresses + }, +) diff --git a/src/store/index.js b/src/store/index.js index c968b44d..48924c8a 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -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 = combineReducers({ routing: routerReducer, [PROVIDER_REDUCER_ID]: provider, [SAFE_REDUCER_ID]: safe, - [BALANCE_REDUCER_ID]: balances, + [TOKEN_REDUCER_ID]: tokens, [TRANSACTIONS_REDUCER_ID]: transactions, }) diff --git a/src/test/safe.dom.tokens.test.js b/src/test/safe.dom.tokens.test.js index 5c6ce2e2..668ac43f 100644 --- a/src/test/safe.dom.tokens.test.js +++ b/src/test/safe.dom.tokens.test.js @@ -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' diff --git a/src/test/safe.redux.balance.test.js b/src/test/safe.redux.balance.test.js index 07fe16e8..352e639c 100644 --- a/src/test/safe.redux.balance.test.js +++ b/src/test/safe.redux.balance.test.js @@ -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> | typeof undefined = store.getState()[BALANCE_REDUCER_ID] - if (!balances) throw new Error() + const tokens: Map> | typeof undefined = store.getState()[TOKEN_REDUCER_ID] + if (!tokens) throw new Error() - const safeBalances: Map | typeof undefined = balances.get(address) + const safeBalances: Map | 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> | typeof undefined = store.getState()[BALANCE_REDUCER_ID] - if (!balances) throw new Error() + const tokens: Map> | typeof undefined = store.getState()[TOKEN_REDUCER_ID] + if (!tokens) throw new Error() - const safeBalances: Map | typeof undefined = balances.get(address) + const safeBalances: Map | 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') diff --git a/src/test/utils/transactions/moveTokens.helper.js b/src/test/utils/transactions/moveTokens.helper.js index e783a536..41bfa12e 100644 --- a/src/test/utils/transactions/moveTokens.helper.js +++ b/src/test/utils/transactions/moveTokens.helper.js @@ -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, @@ -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 = Map().set('TKN', makeBalance({ + const fetchBalancesMock = jest.spyOn(fetchTokensAction, 'fetchTokens') + const funds = await fetchTokensAction.calculateBalanceOf(tokenAddress, address, 18) + const balances: Map = 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() } From 3afefea0a73e84018b5ecd4a0f033f3280f9faa9 Mon Sep 17 00:00:00 2001 From: apanizo Date: Tue, 10 Jul 2018 16:48:20 +0200 Subject: [PATCH 04/15] WA-232 Displaying safe's token in settings route --- src/routes/index.js | 9 ++++ .../safe/component/Safe/BalanceInfo.jsx | 12 ++++- src/routes/safe/component/Safe/index.jsx | 5 ++- src/routes/safe/component/SendToken/index.jsx | 3 +- src/routes/tokens/component/Layout.jsx | 4 +- src/routes/tokens/component/Token/index.jsx | 44 +++++++------------ src/routes/tokens/container/actions.js | 2 + src/routes/tokens/container/index.jsx | 14 +++++- src/routes/tokens/container/selector.js | 3 +- src/utils/tokens.js | 2 + 10 files changed, 63 insertions(+), 35 deletions(-) create mode 100644 src/utils/tokens.js diff --git a/src/routes/index.js b/src/routes/index.js index 4bc05bb0..109198db 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -11,6 +11,11 @@ const Safe = Loadable({ loading: Loader, }) +const Settings = Loadable({ + loader: () => import('./tokens/container'), + loading: Loader, +}) + const SafeList = Loadable({ loader: () => import('./safeList/container'), loading: Loader, @@ -22,6 +27,9 @@ const Open = Loadable({ }) const SAFE_ADDRESS = `${SAFELIST_ADDRESS}/:${SAFE_PARAM_ADDRESS}` +const SAFE_SETTINGS = `${SAFE_ADDRESS}/settings` + +export const settingsUrlFrom = (safeAddress: string) => `${SAFELIST_ADDRESS}/${safeAddress}/settings` const Routes = () => ( @@ -30,6 +38,7 @@ const Routes = () => ( + ) diff --git a/src/routes/safe/component/Safe/BalanceInfo.jsx b/src/routes/safe/component/Safe/BalanceInfo.jsx index 7f4c04f4..98110ed1 100644 --- a/src/routes/safe/component/Safe/BalanceInfo.jsx +++ b/src/routes/safe/component/Safe/BalanceInfo.jsx @@ -1,7 +1,9 @@ // @flow import * as React from 'react' import classNames from 'classnames' +import Link from '~/components/layout/Link' import AccountBalance from '@material-ui/icons/AccountBalance' +import Settings from '@material-ui/icons/Settings' import Avatar from '@material-ui/core/Avatar' import Collapse from '@material-ui/core/Collapse' import IconButton from '@material-ui/core/IconButton' @@ -18,8 +20,10 @@ import Button from '~/components/layout/Button' import openHoc, { type Open } from '~/components/hoc/OpenHoc' import { type WithStyles } from '~/theme/mui' import { type Token } from '~/routes/tokens/store/model/token' +import { settingsUrlFrom } from '~/routes' type Props = Open & WithStyles & { + safeAddress: string, tokens: Map, onMoveFunds: (token: Token) => void, } @@ -33,9 +37,10 @@ const styles = { export const MOVE_FUNDS_BUTTON_TEXT = 'Move' const BalanceComponent = openHoc(({ - open, toggle, tokens, classes, onMoveFunds, + open, toggle, tokens, classes, onMoveFunds, safeAddress, }: Props) => { const hasBalances = tokens.count() > 0 + const settingsUrl = settingsUrlFrom(safeAddress) return ( @@ -44,6 +49,11 @@ const BalanceComponent = openHoc(({ + + + + + {open ? diff --git a/src/routes/safe/component/Safe/index.jsx b/src/routes/safe/component/Safe/index.jsx index bc800860..666cceb7 100644 --- a/src/routes/safe/component/Safe/index.jsx +++ b/src/routes/safe/component/Safe/index.jsx @@ -110,12 +110,13 @@ class GnoSafe extends React.PureComponent { const { safe, tokens, userAddress } = this.props const { component } = this.state const ethBalance = getEthBalanceFrom(tokens) + const address = safe.get('address') return ( - + { onRemoveOwner={this.onRemoveOwner} /> -
+
diff --git a/src/routes/safe/component/SendToken/index.jsx b/src/routes/safe/component/SendToken/index.jsx index 84d2f999..0258f977 100644 --- a/src/routes/safe/component/SendToken/index.jsx +++ b/src/routes/safe/component/SendToken/index.jsx @@ -10,6 +10,7 @@ import { type Token } from '~/routes/tokens/store/model/token' import { createTransaction } from '~/wallets/createTransactions' import { EMPTY_DATA } from '~/wallets/ethTransactions' import { toNative } from '~/wallets/tokens' +import { isEther } from '~/utils/tokens' import actions, { type Actions } from './actions' import selector, { type SelectorProps } from './selector' import SendTokenForm, { TKN_DESTINATION_PARAM, TKN_VALUE_PARAM } from './SendTokenForm' @@ -31,8 +32,6 @@ type State = { export const SEE_TXS_BUTTON_TEXT = 'VISIT TXS' -const isEther = (symbol: string) => symbol === 'ETH' - const getTransferData = async (tokenAddress: string, to: string, amount: BigNumber) => { const StandardToken = await getStandardTokenContract() const myToken = await StandardToken.at(tokenAddress) diff --git a/src/routes/tokens/component/Layout.jsx b/src/routes/tokens/component/Layout.jsx index 90b461be..69c04d48 100644 --- a/src/routes/tokens/component/Layout.jsx +++ b/src/routes/tokens/component/Layout.jsx @@ -1,5 +1,5 @@ // @flow -import * as MuiList from '@material-ui/core/List' +import MuiList from '@material-ui/core/List' import * as React from 'react' import Block from '~/components/layout/Block' import Col from '~/components/layout/Col' @@ -24,6 +24,7 @@ type State = { const listStyle = { width: '100%', + paddingBottom: 0, } class TokenLayout extends React.PureComponent { @@ -64,6 +65,7 @@ class TokenLayout extends React.PureComponent { {tokens.map((token: Token) => ( ({ +const styles = () => ({ card: { display: 'flex', }, @@ -36,18 +36,9 @@ const styles = theme => ({ 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, + width: 150, + margin: 10, + backgroundSize: '50%', }, }) @@ -60,14 +51,15 @@ class TokenComponent extends React.Component { handleChange = (e: SyntheticInputEvent) => { const { checked } = e.target - const callback = checked ? this.props.onDisableToken : this.props.onDisableToken - this.setState(() => ({ checked: e.target.checked }), () => callback(this.props.token)) + const callback = checked ? this.props.onEnableToken : this.props.onDisableToken + this.setState(() => ({ checked }), () => callback(this.props.token)) } render() { const { classes, token } = this.props const name = token.get('name') const symbol = token.get('symbol') + const disabled = isEther(symbol) return ( @@ -75,25 +67,23 @@ class TokenComponent extends React.Component { {name} + {symbol} + + {/* - - {symbol} - - - {/* - */} - + */} { + componentDidUpdate() { + const { safeAddress } = this.props + + if (this.props.tokens.count() === 0) { + this.props.fetchTokens(safeAddress) + } + } + render() { const { tokens, addresses, safe, disableToken, enableToken, diff --git a/src/routes/tokens/container/selector.js b/src/routes/tokens/container/selector.js index aac3a947..de805afc 100644 --- a/src/routes/tokens/container/selector.js +++ b/src/routes/tokens/container/selector.js @@ -3,7 +3,7 @@ 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 { safeSelector, safeParamAddressSelector } from '~/routes/safe/store/selectors' import { type Token } from '~/routes/tokens/store/model/token' export type SelectorProps = { @@ -14,6 +14,7 @@ export type SelectorProps = { export default createStructuredSelector({ safe: safeSelector, + safeAddress: safeParamAddressSelector, tokens: tokenListSelector, addresses: tokenAddressesSelector, }) diff --git a/src/utils/tokens.js b/src/utils/tokens.js new file mode 100644 index 00000000..16d5c169 --- /dev/null +++ b/src/utils/tokens.js @@ -0,0 +1,2 @@ +// @flow +export const isEther = (symbol: string) => symbol === 'ETH' From ece4e4a5bffa584b520da5a6a46722cd6e2e127e Mon Sep 17 00:00:00 2001 From: apanizo Date: Tue, 10 Jul 2018 17:17:04 +0200 Subject: [PATCH 05/15] WA-232 Enabling navigation from settings to safe --- src/routes/safe/component/Safe/BalanceInfo.jsx | 2 +- src/routes/tokens/component/Layout.jsx | 9 ++++++++- src/routes/tokens/container/index.jsx | 4 ++-- src/routes/tokens/container/selector.js | 1 + 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/routes/safe/component/Safe/BalanceInfo.jsx b/src/routes/safe/component/Safe/BalanceInfo.jsx index 98110ed1..9989148d 100644 --- a/src/routes/safe/component/Safe/BalanceInfo.jsx +++ b/src/routes/safe/component/Safe/BalanceInfo.jsx @@ -50,7 +50,7 @@ const BalanceComponent = openHoc(({ - + diff --git a/src/routes/tokens/component/Layout.jsx b/src/routes/tokens/component/Layout.jsx index 69c04d48..36016f13 100644 --- a/src/routes/tokens/component/Layout.jsx +++ b/src/routes/tokens/component/Layout.jsx @@ -3,13 +3,17 @@ import MuiList from '@material-ui/core/List' import * as React from 'react' import Block from '~/components/layout/Block' import Col from '~/components/layout/Col' +import AccountBalanceWallet from '@material-ui/icons/AccountBalanceWallet' +import Link from '~/components/layout/Link' import Bold from '~/components/layout/Bold' import Img from '~/components/layout/Img' +import IconButton from '@material-ui/core/IconButton' 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 { SAFELIST_ADDRESS } from '~/routes/routes' import TokenComponent from './Token' // import AddToken from '~/routes/tokens/component/AddToken' // import RemoveToken from '~/routes/tokens/component/RemoveToken' @@ -56,7 +60,7 @@ class TokenLayout extends React.PureComponent { } render() { - const { safe, tokens } = this.props + const { safe, safeAddress, tokens } = this.props const { component } = this.state const name = safe ? safe.get('name') : '' @@ -75,6 +79,9 @@ class TokenLayout extends React.PureComponent { + + + {name} diff --git a/src/routes/tokens/container/index.jsx b/src/routes/tokens/container/index.jsx index bb4ba4a3..8a83a93e 100644 --- a/src/routes/tokens/container/index.jsx +++ b/src/routes/tokens/container/index.jsx @@ -8,7 +8,6 @@ import selector, { type SelectorProps } from './selector' import actions, { type Actions } from './actions' type Props = Actions & SelectorProps & { - safeAddress: string, fetchTokens: typeof fetchTokens, } @@ -23,7 +22,7 @@ class TokensView extends React.PureComponent { render() { const { - tokens, addresses, safe, disableToken, enableToken, + tokens, addresses, safe, safeAddress, disableToken, enableToken, } = this.props return ( @@ -32,6 +31,7 @@ class TokensView extends React.PureComponent { tokens={tokens} addresses={addresses} safe={safe} + safeAddress={safeAddress} disableToken={disableToken} enableToken={enableToken} /> diff --git a/src/routes/tokens/container/selector.js b/src/routes/tokens/container/selector.js index de805afc..dd490988 100644 --- a/src/routes/tokens/container/selector.js +++ b/src/routes/tokens/container/selector.js @@ -10,6 +10,7 @@ export type SelectorProps = { tokens: List, addresses: List, safe: Safe, + safeAddress: string, } export default createStructuredSelector({ From 0f4d5cb33a4c0a752362edb166aebc60d0d7b5c8 Mon Sep 17 00:00:00 2001 From: apanizo Date: Wed, 11 Jul 2018 12:59:50 +0200 Subject: [PATCH 06/15] WA-232 Created localStorage tokens utility class --- .../index.js} | 1 + src/utils/localStorage/tokens.js | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) rename src/utils/{localStorage.js => localStorage/index.js} (97%) create mode 100644 src/utils/localStorage/tokens.js diff --git a/src/utils/localStorage.js b/src/utils/localStorage/index.js similarity index 97% rename from src/utils/localStorage.js rename to src/utils/localStorage/index.js index 474e3dbb..f908a03a 100644 --- a/src/utils/localStorage.js +++ b/src/utils/localStorage/index.js @@ -5,6 +5,7 @@ import { type Owner } from '~/routes/safe/store/model/owner' export const SAFES_KEY = 'SAFES' export const TX_KEY = 'TX' export const OWNERS_KEY = 'OWNERS' +export const TOKENS_KEY = 'TOKENS' export const load = (key: string) => { try { diff --git a/src/utils/localStorage/tokens.js b/src/utils/localStorage/tokens.js new file mode 100644 index 00000000..a29fd3fb --- /dev/null +++ b/src/utils/localStorage/tokens.js @@ -0,0 +1,28 @@ +// @flow +import { List } from 'immutable' +import { load, TOKENS_KEY } from '~/utils/localStorage' + +const getTokensKey = (safeAddress: string) => `${TOKENS_KEY}-${safeAddress}` + +export const setTokens = (safeAddress: string, tokens: List) => { + try { + const serializedState = JSON.stringify(tokens) + const key = getTokensKey(safeAddress) + localStorage.setItem(key, serializedState) + } catch (err) { + // eslint-disable-next-line + console.log('Error storing tokens in localstorage') + } +} + +export const getTokens = (safeAddress: string): List => { + const key = getTokensKey(safeAddress) + const data = load(key) + + return data ? List(data) : List() +} + +export const storedTokensBefore = (safeAddress: string) => { + const key = getTokensKey(safeAddress) + return localStorage.getItem(key) === null +} From be9bfe0df9519ebb61cf442d367abdbdb548acc5 Mon Sep 17 00:00:00 2001 From: apanizo Date: Wed, 11 Jul 2018 13:01:08 +0200 Subject: [PATCH 07/15] WA-232 Using custom fetch and active tokens extractor --- src/utils/fetch.js | 19 +++++++++++++++++ src/utils/tokens.js | 37 ++++++++++++++++++++++++++++++++++ src/wallets/ethTransactions.js | 19 ++++------------- 3 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 src/utils/fetch.js diff --git a/src/utils/fetch.js b/src/utils/fetch.js new file mode 100644 index 00000000..2777fdd4 --- /dev/null +++ b/src/utils/fetch.js @@ -0,0 +1,19 @@ +// @flow + +export const enhancedFetch = async (url: string, errMsg: string) => { + const header = new Headers({ + 'Access-Control-Allow-Origin': '*', + }) + + const sentData = { + mode: 'cors', + header, + } + + const response = await fetch(url, sentData) + if (!response.ok) { + throw new Error(errMsg) + } + + return response.json() +} diff --git a/src/utils/tokens.js b/src/utils/tokens.js index 16d5c169..92930af0 100644 --- a/src/utils/tokens.js +++ b/src/utils/tokens.js @@ -1,2 +1,39 @@ // @flow +import { List } from 'immutable' +import logo from '~/assets/icons/icon_etherTokens.svg' +import { getBalanceInEtherOf } from '~/wallets/getWeb3' +import { makeToken, type Token } from '~/routes/tokens/store/model/token' + export const isEther = (symbol: string) => symbol === 'ETH' + +export const getSafeEthToken = async (safeAddress: string) => { + const balance = await getBalanceInEtherOf(safeAddress) + + const ethBalance = makeToken({ + address: '0', + name: 'Ether', + symbol: 'ETH', + decimals: 18, + logoUrl: logo, + funds: balance, + }) + + return ethBalance +} + +export const calculateActiveErc20TokensFrom = (tokens: List) => { + const addresses = List().withMutations(list => + tokens.forEach((token: Token) => { + if (isEther(token.get('symbol'))) { + return + } + + if (!token.get('status')) { + return + } + + list.push(token.address) + })) + + return addresses +} diff --git a/src/wallets/ethTransactions.js b/src/wallets/ethTransactions.js index 9805f648..430dbdb5 100644 --- a/src/wallets/ethTransactions.js +++ b/src/wallets/ethTransactions.js @@ -2,6 +2,7 @@ import { BigNumber } from 'bignumber.js' import { getWeb3 } from '~/wallets/getWeb3' import { promisify } from '~/utils/promisify' +import { enhancedFetch } from '~/utils/fetch' // const MAINNET_NETWORK = 1 export const EMPTY_DATA = '0x' @@ -40,21 +41,9 @@ export const calculateGasPrice = async () => { return '20000000000' } - const header = new Headers({ - 'Access-Control-Allow-Origin': '*', - }) - - const sentData = { - mode: 'cors', - header, - } - - const response = await fetch('https://ethgasstation.info/json/ethgasAPI.json', sentData) - if (!response.ok) { - throw new Error('Error querying gast station') - } - - const json = await response.json() + const url = 'https://ethgasstation.info/json/ethgasAPI.json' + const errMsg = 'Error querying gas station' + const json = await enhancedFetch(url, errMsg) return new BigNumber(json.average).multipliedBy(1e8).toString() } From 84057d03eec64eb4c30b8cbc71b9369aa3ba85ab Mon Sep 17 00:00:00 2001 From: apanizo Date: Wed, 11 Jul 2018 13:01:58 +0200 Subject: [PATCH 08/15] WA-232 Storing in localStorage active tokens --- src/routes/safe/component/Layout.jsx | 4 +- src/routes/safe/component/Layout.stories.js | 8 +- src/routes/safe/container/index.jsx | 8 +- src/routes/safe/container/selector.js | 6 +- src/routes/tokens/component/Token/index.jsx | 2 +- .../tokens/store/actions/disableToken.js | 1 + .../tokens/store/actions/enableToken.js | 1 + .../tokens/store/actions/fetchTokens.js | 79 ++++++++----------- src/routes/tokens/store/reducer/tokens.js | 44 ++++++++--- src/routes/tokens/store/selectors/index.js | 17 +++- 10 files changed, 97 insertions(+), 73 deletions(-) diff --git a/src/routes/safe/component/Layout.jsx b/src/routes/safe/component/Layout.jsx index 91254cad..231731cf 100644 --- a/src/routes/safe/component/Layout.jsx +++ b/src/routes/safe/component/Layout.jsx @@ -7,11 +7,11 @@ import GnoSafe from './Safe' type Props = SelectorProps const Layout = ({ - safe, tokens, provider, userAddress, + safe, activeTokens, provider, userAddress, }: Props) => ( { safe - ? + ? : } diff --git a/src/routes/safe/component/Layout.stories.js b/src/routes/safe/component/Layout.stories.js index b73000bb..86e8f8a5 100644 --- a/src/routes/safe/component/Layout.stories.js +++ b/src/routes/safe/component/Layout.stories.js @@ -30,7 +30,7 @@ storiesOf('Routes /safe:address', module) userAddress="foo" safe={undefined} provider="METAMASK" - tokens={Map()} + activeTokens={Map()} fetchBalance={() => {}} /> )) @@ -39,7 +39,7 @@ storiesOf('Routes /safe:address', module) userAddress="foo" safe={undefined} provider="" - tokens={Map()} + activeTokens={Map()} fetchBalance={() => {}} /> )) @@ -51,7 +51,7 @@ storiesOf('Routes /safe:address', module) userAddress="foo" safe={safe} provider="METAMASK" - tokens={Map().set('ETH', ethBalance)} + activeTokens={Map().set('ETH', ethBalance)} fetchBalance={() => {}} /> ) @@ -64,7 +64,7 @@ storiesOf('Routes /safe:address', module) userAddress="foo" safe={safe} provider="METAMASK" - tokens={Map().set('ETH', ethBalance)} + activeTokens={Map().set('ETH', ethBalance)} fetchBalance={() => {}} /> ) diff --git a/src/routes/safe/container/index.jsx b/src/routes/safe/container/index.jsx index 7a746e7c..b980ec2a 100644 --- a/src/routes/safe/container/index.jsx +++ b/src/routes/safe/container/index.jsx @@ -16,7 +16,9 @@ const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 15000 class SafeView extends React.PureComponent { componentDidMount() { this.intervalId = setInterval(() => { - const { safe, fetchTokens, fetchSafe } = this.props + const { + safe, fetchTokens, fetchSafe, + } = this.props if (!safe) { return } @@ -45,13 +47,13 @@ class SafeView extends React.PureComponent { render() { const { - safe, provider, tokens, granted, userAddress, + safe, provider, activeTokens, granted, userAddress, } = this.props return ( { granted - ? + ? : } diff --git a/src/routes/safe/container/selector.js b/src/routes/safe/container/selector.js index fce3795f..94b76a07 100644 --- a/src/routes/safe/container/selector.js +++ b/src/routes/safe/container/selector.js @@ -7,13 +7,13 @@ 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 { tokensSelector } from '~/routes/tokens/store/selectors' +import { activeTokensSelector } from '~/routes/tokens/store/selectors' import { type Token } from '~/routes/tokens/store/model/token' export type SelectorProps = { safe: SafeSelectorProps, provider: string, - tokens: Map, + activeTokens: Map, userAddress: string, } @@ -41,7 +41,7 @@ export const grantedSelector: Selector = crea export default createStructuredSelector({ safe: safeSelector, provider: providerNameSelector, - tokens: tokensSelector, + activeTokens: activeTokensSelector, granted: grantedSelector, userAddress: userAccountSelector, }) diff --git a/src/routes/tokens/component/Token/index.jsx b/src/routes/tokens/component/Token/index.jsx index e0ca79f7..5df9da20 100644 --- a/src/routes/tokens/component/Token/index.jsx +++ b/src/routes/tokens/component/Token/index.jsx @@ -44,7 +44,7 @@ const styles = () => ({ class TokenComponent extends React.Component { state = { - checked: true, + checked: this.props.token.get('status'), } // onRemoveClick = () => this.props.onRemoveToken(this.props.token) diff --git a/src/routes/tokens/store/actions/disableToken.js b/src/routes/tokens/store/actions/disableToken.js index d8b31edc..eae0db78 100644 --- a/src/routes/tokens/store/actions/disableToken.js +++ b/src/routes/tokens/store/actions/disableToken.js @@ -9,6 +9,7 @@ const disableToken = createAction( (safeAddress: string, token: Token) => ({ safeAddress, symbol: token.get('symbol'), + address: token.get('address'), }), ) diff --git a/src/routes/tokens/store/actions/enableToken.js b/src/routes/tokens/store/actions/enableToken.js index e09f9d24..9056e4dc 100644 --- a/src/routes/tokens/store/actions/enableToken.js +++ b/src/routes/tokens/store/actions/enableToken.js @@ -9,6 +9,7 @@ const enableToken = createAction( (safeAddress: string, token: Token) => ({ safeAddress, symbol: token.get('symbol'), + address: token.get('address'), }), ) diff --git a/src/routes/tokens/store/actions/fetchTokens.js b/src/routes/tokens/store/actions/fetchTokens.js index efea6f9d..349c045e 100644 --- a/src/routes/tokens/store/actions/fetchTokens.js +++ b/src/routes/tokens/store/actions/fetchTokens.js @@ -1,16 +1,17 @@ // @flow -import { Map } from 'immutable' +import { List, Map } from 'immutable' import contract from 'truffle-contract' 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 { getWeb3 } from '~/wallets/getWeb3' import { type GlobalState } from '~/store/index' 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 { getTokens } from '~/utils/localStorage/tokens' +import { getSafeEthToken } from '~/utils/tokens' +import { enhancedFetch } from '~/utils/fetch' import addTokens from './addTokens' - const createStandardTokenContract = async () => { const web3 = getWeb3() const erc20Token = await contract(StandardToken) @@ -29,49 +30,33 @@ export const calculateBalanceOf = async (tokenAddress: string, address: string, .catch(() => '0') } -export const fetchTokens = (safeAddress: string) => async (dispatch: ReduxDispatch) => { - const balance = await getBalanceInEtherOf(safeAddress) - const ethBalance = makeToken({ - address: '0', - name: 'Ether', - symbol: 'ETH', - decimals: 18, - logoUrl: logo, - funds: balance, - }) +export const fetchTokens = (safeAddress: string) => + async (dispatch: ReduxDispatch) => { + const tokens: List = getTokens(safeAddress) + const ethBalance = await getSafeEthToken(safeAddress) - const header = new Headers({ - 'Access-Control-Allow-Origin': '*', - }) + const url = 'https://gist.githubusercontent.com/rmeissner/98911fcf74b0ea9731e2dae2441c97a4/raw/' + const errMsg = 'Error querying safe balances' + const json = await enhancedFetch(url, errMsg) - const sentData = { - mode: 'cors', - header, + try { + const balancesRecords = await Promise.all(json.map(async (item: TokenProps) => { + const funds = await calculateBalanceOf(item.address, safeAddress, item.decimals) + const status = tokens.includes(item.address) + return makeToken({ ...item, status, funds }) + })) + + const balances: Map = Map().withMutations((map) => { + balancesRecords.forEach(record => map.set(record.get('symbol'), record)) + + map.set('ETH', ethBalance) + }) + + return dispatch(addTokens(safeAddress, balances)) + } catch (err) { + // eslint-disable-next-line + console.log("Error fetching token balances... " + err) + + return Promise.resolve() + } } - - const response = await fetch('https://gist.githubusercontent.com/rmeissner/98911fcf74b0ea9731e2dae2441c97a4/raw/', sentData) - if (!response.ok) { - throw new Error('Error querying safe balances') - } - - const json = await response.json() - - try { - const balancesRecords = await Promise.all(json.map(async (item: TokenProps) => { - const funds = await calculateBalanceOf(item.address, safeAddress, item.decimals) - return makeToken({ ...item, funds }) - })) - - const balances: Map = Map().withMutations((map) => { - balancesRecords.forEach(record => map.set(record.get('symbol'), record)) - map.set('ETH', ethBalance) - }) - - return dispatch(addTokens(safeAddress, balances)) - } catch (err) { - // eslint-disable-next-line - console.log("Error fetching token balances...") - - return Promise.resolve() - } -} diff --git a/src/routes/tokens/store/reducer/tokens.js b/src/routes/tokens/store/reducer/tokens.js index 61db9c16..d66c02da 100644 --- a/src/routes/tokens/store/reducer/tokens.js +++ b/src/routes/tokens/store/reducer/tokens.js @@ -1,26 +1,50 @@ // @flow -import { Map } from 'immutable' +import { List, 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' +import { setTokens, getTokens } from '~/utils/localStorage/tokens' +import { ensureOnce } from '~/utils/singleton' +import { calculateActiveErc20TokensFrom } from '~/utils/tokens' export const TOKEN_REDUCER_ID = 'tokens' export type State = Map> +const setTokensOnce = ensureOnce(setTokens) + export default handleActions({ - [ADD_TOKENS]: (state: State, action: ActionType): State => - state.update(action.payload.safeAddress, (prevSafe: Map) => { + [ADD_TOKENS]: (state: State, action: ActionType): State => { + const { safeAddress, tokens } = action.payload + + const activeAddresses: List = calculateActiveErc20TokensFrom(tokens.toList()) + setTokensOnce(safeAddress, activeAddresses) + + return state.update(safeAddress, (prevSafe: Map) => { if (!prevSafe) { - return action.payload.tokens + return tokens } - return prevSafe.equals(action.payload.tokens) ? prevSafe : action.payload.tokens - }), - [DISABLE_TOKEN]: (state: State, action: ActionType): State => - state.setIn([action.payload.safeAddress, action.payload.symbol, 'status'], false), - [ENABLE_TOKEN]: (state: State, action: ActionType): State => - state.setIn([action.payload.safeAddress, action.payload.symbol, 'status'], true), + return prevSafe.equals(tokens) ? prevSafe : tokens + }) + }, + [DISABLE_TOKEN]: (state: State, action: ActionType): State => { + const { address, safeAddress, symbol } = action.payload + + const activeTokens = getTokens(safeAddress) + const index = activeTokens.indexOf(address) + setTokens(safeAddress, activeTokens.delete(index)) + + return state.setIn([safeAddress, symbol, 'status'], false) + }, + [ENABLE_TOKEN]: (state: State, action: ActionType): State => { + const { address, safeAddress, symbol } = action.payload + + const activeTokens = getTokens(safeAddress) + setTokens(safeAddress, activeTokens.push(address)) + + return state.setIn([safeAddress, symbol, 'status'], true) + }, }, Map()) diff --git a/src/routes/tokens/store/selectors/index.js b/src/routes/tokens/store/selectors/index.js index e0baffa4..4925e95d 100644 --- a/src/routes/tokens/store/selectors/index.js +++ b/src/routes/tokens/store/selectors/index.js @@ -5,24 +5,30 @@ import { safeParamAddressSelector, type RouterProps } from '~/routes/safe/store/ import { type GlobalState } from '~/store' import { TOKEN_REDUCER_ID } from '~/routes/tokens/store/reducer/tokens' import { type Token } from '~/routes/tokens/store/model/token' +import { calculateActiveErc20TokensFrom } from '~/utils/tokens' const balancesSelector = (state: GlobalState) => state[TOKEN_REDUCER_ID] export const tokensSelector: Selector> = createSelector( balancesSelector, safeParamAddressSelector, - (balances: Map>, address: string) => { + (tokens: Map>, address: string) => { if (!address) { return Map() } - return balances.get(address) || Map() + return tokens.get(address) || Map() }, ) export const tokenListSelector = createSelector( tokensSelector, - (balances: Map) => balances.toList(), + (tokens: Map) => tokens.toList(), +) + +export const activeTokensSelector = createSelector( + tokenListSelector, + (tokens: List) => tokens.filter((token: Token) => token.get('status')), ) export const tokenAddressesSelector = createSelector( @@ -34,3 +40,8 @@ export const tokenAddressesSelector = createSelector( return addresses }, ) + +export const activeTokenAddressesSelector = createSelector( + tokenListSelector, + (balances: List) => calculateActiveErc20TokensFrom(balances), +) From a5b03007a9290d1481ac4559d3674429312b221e Mon Sep 17 00:00:00 2001 From: apanizo Date: Wed, 11 Jul 2018 13:46:33 +0200 Subject: [PATCH 09/15] WA-232 Fix Enabling Withdraw button when there is ETH funds --- src/routes/safe/component/Safe/index.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/routes/safe/component/Safe/index.jsx b/src/routes/safe/component/Safe/index.jsx index 666cceb7..232cfee3 100644 --- a/src/routes/safe/component/Safe/index.jsx +++ b/src/routes/safe/component/Safe/index.jsx @@ -42,13 +42,13 @@ const listStyle = { width: '100%', } -const getEthBalanceFrom = (balances: Map) => { - const ethBalance = balances.get('ETH') - if (!ethBalance) { +const getEthBalanceFrom = (tokens: List) => { + const ethToken = tokens.filter(token => token.get('symbol') === 'ETH') + if (ethToken.count() === 0) { return 0 } - return Number(ethBalance.get('funds')) + return Number(ethToken.get(0).get('funds')) } class GnoSafe extends React.PureComponent { From 7de871ba815310a8049e073783e2ee73732cd68a Mon Sep 17 00:00:00 2001 From: apanizo Date: Thu, 12 Jul 2018 09:48:58 +0200 Subject: [PATCH 10/15] WA-232 Only fetching balance of active tokens --- src/routes/tokens/store/actions/fetchTokens.js | 3 ++- src/routes/tokens/store/selectors/index.js | 6 ------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/routes/tokens/store/actions/fetchTokens.js b/src/routes/tokens/store/actions/fetchTokens.js index 349c045e..64a03d05 100644 --- a/src/routes/tokens/store/actions/fetchTokens.js +++ b/src/routes/tokens/store/actions/fetchTokens.js @@ -41,8 +41,9 @@ export const fetchTokens = (safeAddress: string) => try { const balancesRecords = await Promise.all(json.map(async (item: TokenProps) => { - const funds = await calculateBalanceOf(item.address, safeAddress, item.decimals) const status = tokens.includes(item.address) + const funds = status ? await calculateBalanceOf(item.address, safeAddress, item.decimals) : '0' + return makeToken({ ...item, status, funds }) })) diff --git a/src/routes/tokens/store/selectors/index.js b/src/routes/tokens/store/selectors/index.js index 4925e95d..2339dcb5 100644 --- a/src/routes/tokens/store/selectors/index.js +++ b/src/routes/tokens/store/selectors/index.js @@ -5,7 +5,6 @@ import { safeParamAddressSelector, type RouterProps } from '~/routes/safe/store/ import { type GlobalState } from '~/store' import { TOKEN_REDUCER_ID } from '~/routes/tokens/store/reducer/tokens' import { type Token } from '~/routes/tokens/store/model/token' -import { calculateActiveErc20TokensFrom } from '~/utils/tokens' const balancesSelector = (state: GlobalState) => state[TOKEN_REDUCER_ID] @@ -40,8 +39,3 @@ export const tokenAddressesSelector = createSelector( return addresses }, ) - -export const activeTokenAddressesSelector = createSelector( - tokenListSelector, - (balances: List) => calculateActiveErc20TokensFrom(balances), -) From 3fa7a10a16950fff3ae818baf16a434534df2a5f Mon Sep 17 00:00:00 2001 From: apanizo Date: Thu, 12 Jul 2018 18:31:31 +0200 Subject: [PATCH 11/15] WA-232 Improvement: Only fetch balance of activated tokens. Included Tests --- src/routes/index.js | 4 +- src/routes/routes.js | 1 + src/routes/tokens/component/Token/index.jsx | 3 +- .../tokens/store/actions/fetchTokens.js | 10 +- src/test/builder/safe.dom.utils.js | 20 ++- src/test/safe.dom.tokens.test.js | 6 +- src/test/tokens.dom.enabling.test.js | 134 ++++++++++++++++++ src/test/utils/tokenMovements.js | 7 +- 8 files changed, 168 insertions(+), 17 deletions(-) create mode 100644 src/test/tokens.dom.enabling.test.js diff --git a/src/routes/index.js b/src/routes/index.js index 109198db..42b6f74c 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -4,7 +4,7 @@ import Loadable from 'react-loadable' import { Switch, Redirect, Route } from 'react-router-dom' import Loader from '~/components/Loader' import Welcome from './welcome/container' -import { SAFELIST_ADDRESS, OPEN_ADDRESS, SAFE_PARAM_ADDRESS, WELCOME_ADDRESS } from './routes' +import { SAFELIST_ADDRESS, OPEN_ADDRESS, SAFE_PARAM_ADDRESS, WELCOME_ADDRESS, SETTINS_ADDRESS } from './routes' const Safe = Loadable({ loader: () => import('./safe/container'), @@ -27,7 +27,7 @@ const Open = Loadable({ }) const SAFE_ADDRESS = `${SAFELIST_ADDRESS}/:${SAFE_PARAM_ADDRESS}` -const SAFE_SETTINGS = `${SAFE_ADDRESS}/settings` +const SAFE_SETTINGS = `${SAFE_ADDRESS}${SETTINS_ADDRESS}` export const settingsUrlFrom = (safeAddress: string) => `${SAFELIST_ADDRESS}/${safeAddress}/settings` diff --git a/src/routes/routes.js b/src/routes/routes.js index ed521627..4ab227a3 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -3,3 +3,4 @@ export const SAFE_PARAM_ADDRESS = 'address' export const SAFELIST_ADDRESS = '/safes' export const OPEN_ADDRESS = '/open' export const WELCOME_ADDRESS = '/welcome' +export const SETTINS_ADDRESS = '/settings' diff --git a/src/routes/tokens/component/Token/index.jsx b/src/routes/tokens/component/Token/index.jsx index 5df9da20..b40a789d 100644 --- a/src/routes/tokens/component/Token/index.jsx +++ b/src/routes/tokens/component/Token/index.jsx @@ -51,6 +51,7 @@ class TokenComponent extends React.Component { handleChange = (e: SyntheticInputEvent) => { const { checked } = e.target + const callback = checked ? this.props.onEnableToken : this.props.onDisableToken this.setState(() => ({ checked }), () => callback(this.props.token)) } @@ -69,7 +70,7 @@ class TokenComponent extends React.Component { diff --git a/src/routes/tokens/store/actions/fetchTokens.js b/src/routes/tokens/store/actions/fetchTokens.js index 64a03d05..3ddc9b09 100644 --- a/src/routes/tokens/store/actions/fetchTokens.js +++ b/src/routes/tokens/store/actions/fetchTokens.js @@ -30,14 +30,18 @@ export const calculateBalanceOf = async (tokenAddress: string, address: string, .catch(() => '0') } +export const fetchTokensData = async () => { + const url = 'https://gist.githubusercontent.com/rmeissner/98911fcf74b0ea9731e2dae2441c97a4/raw/' + const errMsg = 'Error querying safe balances' + return enhancedFetch(url, errMsg) +} + export const fetchTokens = (safeAddress: string) => async (dispatch: ReduxDispatch) => { const tokens: List = getTokens(safeAddress) const ethBalance = await getSafeEthToken(safeAddress) - const url = 'https://gist.githubusercontent.com/rmeissner/98911fcf74b0ea9731e2dae2441c97a4/raw/' - const errMsg = 'Error querying safe balances' - const json = await enhancedFetch(url, errMsg) + const json = await exports.fetchTokensData() try { const balancesRecords = await Promise.all(json.map(async (item: TokenProps) => { diff --git a/src/test/builder/safe.dom.utils.js b/src/test/builder/safe.dom.utils.js index c11d7571..3b50c0d6 100644 --- a/src/test/builder/safe.dom.utils.js +++ b/src/test/builder/safe.dom.utils.js @@ -8,7 +8,7 @@ import { sleep } from '~/utils/timer' import { Provider } from 'react-redux' import { ConnectedRouter } from 'react-router-redux' import AppRoutes from '~/routes' -import { SAFELIST_ADDRESS } from '~/routes/routes' +import { SAFELIST_ADDRESS, SETTINS_ADDRESS } from '~/routes/routes' import { history, type GlobalState } from '~/store' import { EMPTY_DATA } from '~/wallets/ethTransactions' @@ -93,15 +93,25 @@ export const refreshTransactions = async (store: Store) => { await sleep(1500) } -export const travelToSafe = (store: Store, address: string): React$Component<{}> => { - history.push(`${SAFELIST_ADDRESS}/${address}`) - const SafeDom = TestUtils.renderIntoDocument(( +const createDom = (store: Store): React$Component<{}> => ( + TestUtils.renderIntoDocument(( )) +) - return SafeDom +export const travelToSafe = (store: Store, address: string): React$Component<{}> => { + history.push(`${SAFELIST_ADDRESS}/${address}`) + + return createDom(store) +} + +export const travelToTokens = (store: Store, address: string): React$Component<{}> => { + const url = `${SAFELIST_ADDRESS}/${address}${SETTINS_ADDRESS}` + history.push(url) + + return createDom(store) } diff --git a/src/test/safe.dom.tokens.test.js b/src/test/safe.dom.tokens.test.js index 668ac43f..858524ea 100644 --- a/src/test/safe.dom.tokens.test.js +++ b/src/test/safe.dom.tokens.test.js @@ -4,7 +4,7 @@ import TestUtils from 'react-dom/test-utils' 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' +import { addTknTo, getFirstTokenContract } from '~/test/utils/tokenMovements' import { EXPAND_BALANCE_INDEX, travelToSafe } from '~/test/builder/safe.dom.utils' import { promisify } from '~/utils/promisify' import { getWeb3 } from '~/wallets/getWeb3' @@ -47,14 +47,14 @@ describe('DOM > Feature > SAFE ERC20 TOKENS', () => { const receiverFunds = await fetchBalancesAction.calculateBalanceOf(tokenAddress, receiver, 18) expect(Number(receiverFunds)).toBe(20) - const token = await getTokenContract(getWeb3(), accounts[0]) + const token = await getFirstTokenContract(getWeb3(), accounts[0]) const nativeSafeFunds = await token.balanceOf(safeAddress) expect(Number(nativeSafeFunds.valueOf())).toEqual(80 * (10 ** 18)) }) it('disables send token button when balance is 0', async () => { // GIVEN - const token = await getTokenContract(getWeb3(), accounts[0]) + const token = await getFirstTokenContract(getWeb3(), accounts[0]) await dispatchTknBalance(store, token.address, safeAddress) // WHEN diff --git a/src/test/tokens.dom.enabling.test.js b/src/test/tokens.dom.enabling.test.js new file mode 100644 index 00000000..afef8a8c --- /dev/null +++ b/src/test/tokens.dom.enabling.test.js @@ -0,0 +1,134 @@ +// @flow +import * as TestUtils from 'react-dom/test-utils' +import { getWeb3 } from '~/wallets/getWeb3' +import { type Match } from 'react-router-dom' +import { promisify } from '~/utils/promisify' +import TokenComponent from '~/routes/tokens/component/Token' +import Checkbox from '@material-ui/core/Checkbox' +import { getFirstTokenContract, getSecondTokenContract, addTknTo } from '~/test/utils/tokenMovements' +import { aNewStore } from '~/store' +import { aMinedSafe } from '~/test/builder/safe.redux.builder' +import { travelToTokens } from '~/test/builder/safe.dom.utils' +import { sleep } from '~/utils/timer' +import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps' +import { tokenListSelector, activeTokensSelector } from '~/routes/tokens/store/selectors' +import { type Token } from '~/routes/tokens/store/model/token' + +const fetchTokensModule = require('../routes/tokens/store/actions/fetchTokens') + +// $FlowFixMe +fetchTokensModule.fetchTokensData = jest.fn() + +describe('DOM > Feature > Enable and disable default tokens', () => { + let web3 + let accounts + let firstErc20Token + let secondErc20Token + + beforeAll(async () => { + web3 = getWeb3() + accounts = await promisify(cb => web3.eth.getAccounts(cb)) + firstErc20Token = await getFirstTokenContract(web3, accounts[0]) + secondErc20Token = await getSecondTokenContract(web3, accounts[0]) + // $FlowFixMe + fetchTokensModule.fetchTokensData.mockImplementation(() => Promise.resolve([ + { + address: firstErc20Token.address, + name: 'First Token Example', + symbol: 'FTE', + decimals: 18, + logoUrl: 'https://upload.wikimedia.org/wikipedia/commons/c/c0/Earth_simple_icon.png', + }, + { + address: secondErc20Token.address, + name: 'Second Token Example', + symbol: 'STE', + decimals: 18, + logoUrl: 'https://upload.wikimedia.org/wikipedia/commons/c/c0/Earth_simple_icon.png', + }, + ])) + }) + + it('retrieves only ether as active token', async () => { + // GIVEN + const store = aNewStore() + const safeAddress = await aMinedSafe(store) + await store.dispatch(fetchTokensModule.fetchTokens(safeAddress)) + + // WHEN + const TokensDom = await travelToTokens(store, safeAddress) + await sleep(400) + + // THEN + const tokens = TestUtils.scryRenderedComponentsWithType(TokensDom, TokenComponent) + expect(tokens.length).toBe(3) + + const firstToken = tokens[0] + expect(firstToken.props.token.get('symbol')).toBe('FTE') + expect(firstToken.props.token.get('status')).toBe(false) + + const secontToken = tokens[1] + expect(secontToken.props.token.get('symbol')).toBe('STE') + expect(secontToken.props.token.get('status')).toBe(false) + + const etherToken = tokens[2] + expect(etherToken.props.token.get('symbol')).toBe('ETH') + expect(etherToken.props.token.get('status')).toBe(true) + + const ethCheckbox = TestUtils.findRenderedComponentWithType(etherToken, Checkbox) + if (!ethCheckbox) throw new Error() + expect(ethCheckbox.props.disabled).toBe(true) + }) + + const testToken = (token: Token | typeof undefined, symbol: string, status: boolean, funds?: string) => { + if (!token) throw new Error() + expect(token.get('symbol')).toBe(symbol) + expect(token.get('status')).toBe(status) + if (funds) { + expect(token.get('funds')).toBe(funds) + } + } + + it('fetch balances of only enabled tokens', async () => { + // GIVEN + const store = aNewStore() + const safeAddress = await aMinedSafe(store) + await addTknTo(safeAddress, 50, firstErc20Token) + await addTknTo(safeAddress, 50, secondErc20Token) + await store.dispatch(fetchTokensModule.fetchTokens(safeAddress)) + const TokensDom = await travelToTokens(store, safeAddress) + await sleep(400) + + // WHEN + const inputs = TestUtils.scryRenderedDOMComponentsWithTag(TokensDom, 'input') + + const ethTokenInput = inputs[2] + expect(ethTokenInput.hasAttribute('disabled')).toBe(true) + const firstTokenInput = inputs[0] + expect(firstTokenInput.hasAttribute('disabled')).toBe(false) + TestUtils.Simulate.change(firstTokenInput, { target: { checked: 'true' } }) + + // THEN + const match: Match = buildMathPropsFrom(safeAddress) + const tokenList = tokenListSelector(store.getState(), { match }) + + testToken(tokenList.get(0), 'FTE', true) + testToken(tokenList.get(1), 'STE', false) + testToken(tokenList.get(2), 'ETH', true) + + const activeTokenList = activeTokensSelector(store.getState(), { match }) + expect(activeTokenList.count()).toBe(2) + + testToken(activeTokenList.get(0), 'FTE', true) + testToken(activeTokenList.get(1), 'ETH', true) + + await store.dispatch(fetchTokensModule.fetchTokens(safeAddress)) + + const fundedTokenList = tokenListSelector(store.getState(), { match }) + expect(fundedTokenList.count()).toBe(3) + + testToken(fundedTokenList.get(0), 'FTE', true, '50') + testToken(fundedTokenList.get(1), 'STE', false, '0') + testToken(fundedTokenList.get(2), 'ETH', true, '0') + }) +}) diff --git a/src/test/utils/tokenMovements.js b/src/test/utils/tokenMovements.js index 4353f65d..1b8ff359 100644 --- a/src/test/utils/tokenMovements.js +++ b/src/test/utils/tokenMovements.js @@ -39,13 +39,14 @@ const createTokenContract = async (web3: any, executor: string) => { return token.new({ from: executor, gas: '5000000' }) } -export const getTokenContract = ensureOnce(createTokenContract) +export const getFirstTokenContract = ensureOnce(createTokenContract) +export const getSecondTokenContract = ensureOnce(createTokenContract) -export const addTknTo = async (safe: string, value: number) => { +export const addTknTo = async (safe: string, value: number, tokenContract?: any) => { const web3 = getWeb3() const accounts = await promisify(cb => getWeb3().eth.getAccounts(cb)) - const myToken = await getTokenContract(web3, accounts[0]) + const myToken = tokenContract || await getFirstTokenContract(web3, accounts[0]) const nativeValue = await toNative(value, 18) await myToken.transfer(safe, nativeValue.valueOf(), { from: accounts[0], gas: '5000000' }) From 9f99d007ca33ab5f4ce678cf11139fd9c1f10bf1 Mon Sep 17 00:00:00 2001 From: apanizo Date: Fri, 13 Jul 2018 09:20:34 +0200 Subject: [PATCH 12/15] WA-232 Test localStorage's tokens --- src/test/builder/tokens.dom.utils.js | 29 +++++++++++++ src/test/tokens.dom.enabling.test.js | 62 +++++++++++++--------------- 2 files changed, 57 insertions(+), 34 deletions(-) create mode 100644 src/test/builder/tokens.dom.utils.js diff --git a/src/test/builder/tokens.dom.utils.js b/src/test/builder/tokens.dom.utils.js new file mode 100644 index 00000000..3c46152e --- /dev/null +++ b/src/test/builder/tokens.dom.utils.js @@ -0,0 +1,29 @@ +// @flow +import * as TestUtils from 'react-dom/test-utils' +import { travelToTokens } from '~/test/builder/safe.dom.utils' +import { sleep } from '~/utils/timer' +import { type Token } from '~/routes/tokens/store/model/token' + +export const enableFirstToken = async (store: Store, safeAddress: string) => { + const TokensDom = await travelToTokens(store, safeAddress) + await sleep(400) + + // WHEN + const inputs = TestUtils.scryRenderedDOMComponentsWithTag(TokensDom, 'input') + + const ethTokenInput = inputs[2] + expect(ethTokenInput.hasAttribute('disabled')).toBe(true) + const firstTokenInput = inputs[0] + expect(firstTokenInput.hasAttribute('disabled')).toBe(false) + TestUtils.Simulate.change(firstTokenInput, { target: { checked: 'true' } }) +} + +export const testToken = (token: Token | typeof undefined, symbol: string, status: boolean, funds?: string) => { + if (!token) throw new Error() + expect(token.get('symbol')).toBe(symbol) + expect(token.get('status')).toBe(status) + if (funds) { + expect(token.get('funds')).toBe(funds) + } +} + diff --git a/src/test/tokens.dom.enabling.test.js b/src/test/tokens.dom.enabling.test.js index afef8a8c..44938bf1 100644 --- a/src/test/tokens.dom.enabling.test.js +++ b/src/test/tokens.dom.enabling.test.js @@ -1,5 +1,6 @@ // @flow import * as TestUtils from 'react-dom/test-utils' +import { List } from 'immutable' import { getWeb3 } from '~/wallets/getWeb3' import { type Match } from 'react-router-dom' import { promisify } from '~/utils/promisify' @@ -12,7 +13,8 @@ import { travelToTokens } from '~/test/builder/safe.dom.utils' import { sleep } from '~/utils/timer' import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps' import { tokenListSelector, activeTokensSelector } from '~/routes/tokens/store/selectors' -import { type Token } from '~/routes/tokens/store/model/token' +import { getTokens } from '~/utils/localStorage/tokens' +import { enableFirstToken, testToken } from '~/test/builder/tokens.dom.utils' const fetchTokensModule = require('../routes/tokens/store/actions/fetchTokens') @@ -49,7 +51,7 @@ describe('DOM > Feature > Enable and disable default tokens', () => { ])) }) - it('retrieves only ether as active token', async () => { + it('retrieves only ether as active token in first moment', async () => { // GIVEN const store = aNewStore() const safeAddress = await aMinedSafe(store) @@ -63,32 +65,15 @@ describe('DOM > Feature > Enable and disable default tokens', () => { const tokens = TestUtils.scryRenderedComponentsWithType(TokensDom, TokenComponent) expect(tokens.length).toBe(3) - const firstToken = tokens[0] - expect(firstToken.props.token.get('symbol')).toBe('FTE') - expect(firstToken.props.token.get('status')).toBe(false) + testToken(tokens[0].props.token, 'FTE', false) + testToken(tokens[1].props.token, 'STE', false) + testToken(tokens[2].props.token, 'ETH', true) - const secontToken = tokens[1] - expect(secontToken.props.token.get('symbol')).toBe('STE') - expect(secontToken.props.token.get('status')).toBe(false) - - const etherToken = tokens[2] - expect(etherToken.props.token.get('symbol')).toBe('ETH') - expect(etherToken.props.token.get('status')).toBe(true) - - const ethCheckbox = TestUtils.findRenderedComponentWithType(etherToken, Checkbox) + const ethCheckbox = TestUtils.findRenderedComponentWithType(tokens[2], Checkbox) if (!ethCheckbox) throw new Error() expect(ethCheckbox.props.disabled).toBe(true) }) - const testToken = (token: Token | typeof undefined, symbol: string, status: boolean, funds?: string) => { - if (!token) throw new Error() - expect(token.get('symbol')).toBe(symbol) - expect(token.get('status')).toBe(status) - if (funds) { - expect(token.get('funds')).toBe(funds) - } - } - it('fetch balances of only enabled tokens', async () => { // GIVEN const store = aNewStore() @@ -96,17 +81,7 @@ describe('DOM > Feature > Enable and disable default tokens', () => { await addTknTo(safeAddress, 50, firstErc20Token) await addTknTo(safeAddress, 50, secondErc20Token) await store.dispatch(fetchTokensModule.fetchTokens(safeAddress)) - const TokensDom = await travelToTokens(store, safeAddress) - await sleep(400) - - // WHEN - const inputs = TestUtils.scryRenderedDOMComponentsWithTag(TokensDom, 'input') - - const ethTokenInput = inputs[2] - expect(ethTokenInput.hasAttribute('disabled')).toBe(true) - const firstTokenInput = inputs[0] - expect(firstTokenInput.hasAttribute('disabled')).toBe(false) - TestUtils.Simulate.change(firstTokenInput, { target: { checked: 'true' } }) + await enableFirstToken(store, safeAddress) // THEN const match: Match = buildMathPropsFrom(safeAddress) @@ -131,4 +106,23 @@ describe('DOM > Feature > Enable and disable default tokens', () => { testToken(fundedTokenList.get(1), 'STE', false, '0') testToken(fundedTokenList.get(2), 'ETH', true, '0') }) + + it('localStorage always returns a list', async () => { + const store = aNewStore() + const safeAddress = await aMinedSafe(store) + let tokens: List = getTokens(safeAddress) + expect(tokens).toEqual(List([])) + + await store.dispatch(fetchTokensModule.fetchTokens(safeAddress)) + tokens = getTokens(safeAddress) + expect(tokens.count()).toBe(0) + + await enableFirstToken(store, safeAddress) + tokens = getTokens(safeAddress) + expect(tokens.count()).toBe(1) + + await store.dispatch(fetchTokensModule.fetchTokens(safeAddress)) + tokens = getTokens(safeAddress) + expect(tokens.count()).toBe(1) + }) }) From c7a2a2ae5261267e4bc7537e8de67dadf20dffa5 Mon Sep 17 00:00:00 2001 From: apanizo Date: Fri, 13 Jul 2018 09:28:04 +0200 Subject: [PATCH 13/15] WA-232 Disabling cog while tokens get loaded --- src/routes/safe/component/Safe/BalanceInfo.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/safe/component/Safe/BalanceInfo.jsx b/src/routes/safe/component/Safe/BalanceInfo.jsx index 9989148d..10399168 100644 --- a/src/routes/safe/component/Safe/BalanceInfo.jsx +++ b/src/routes/safe/component/Safe/BalanceInfo.jsx @@ -50,7 +50,7 @@ const BalanceComponent = openHoc(({ - + From bcece2e72e577aa8aeaa52650fd9f85218e18b98 Mon Sep 17 00:00:00 2001 From: apanizo Date: Fri, 13 Jul 2018 12:35:38 +0200 Subject: [PATCH 14/15] WA-232 Refactor jest.fn().mockImplementation for fetching ERC20 tokens in tests --- src/test/tokens.dom.enabling.test.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/test/tokens.dom.enabling.test.js b/src/test/tokens.dom.enabling.test.js index 44938bf1..0565f5c4 100644 --- a/src/test/tokens.dom.enabling.test.js +++ b/src/test/tokens.dom.enabling.test.js @@ -15,11 +15,8 @@ import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps' import { tokenListSelector, activeTokensSelector } from '~/routes/tokens/store/selectors' import { getTokens } from '~/utils/localStorage/tokens' import { enableFirstToken, testToken } from '~/test/builder/tokens.dom.utils' - -const fetchTokensModule = require('../routes/tokens/store/actions/fetchTokens') - -// $FlowFixMe -fetchTokensModule.fetchTokensData = jest.fn() +import * as fetchTokensModule from '~/routes/tokens/store/actions/fetchTokens' +import * as enhancedFetchModule from '~/utils/fetch' describe('DOM > Feature > Enable and disable default tokens', () => { let web3 @@ -33,7 +30,8 @@ describe('DOM > Feature > Enable and disable default tokens', () => { firstErc20Token = await getFirstTokenContract(web3, accounts[0]) secondErc20Token = await getSecondTokenContract(web3, accounts[0]) // $FlowFixMe - fetchTokensModule.fetchTokensData.mockImplementation(() => Promise.resolve([ + enhancedFetchModule.enhancedFetch = jest.fn() + enhancedFetchModule.enhancedFetch.mockImplementation(() => Promise.resolve([ { address: firstErc20Token.address, name: 'First Token Example', From 30baac190d204c6e2892ef6705d95f185744aea4 Mon Sep 17 00:00:00 2001 From: apanizo Date: Fri, 13 Jul 2018 13:20:11 +0200 Subject: [PATCH 15/15] WA-232 Refactor withdraw test --- src/routes/safe/test/Safe.withdraw.test.js | 114 --------------------- src/test/safe.redux.withdraw.test.js | 84 +++++++++++++++ 2 files changed, 84 insertions(+), 114 deletions(-) delete mode 100644 src/routes/safe/test/Safe.withdraw.test.js create mode 100644 src/test/safe.redux.withdraw.test.js diff --git a/src/routes/safe/test/Safe.withdraw.test.js b/src/routes/safe/test/Safe.withdraw.test.js deleted file mode 100644 index 5699ec58..00000000 --- a/src/routes/safe/test/Safe.withdraw.test.js +++ /dev/null @@ -1,114 +0,0 @@ -// @flow -import * as React from 'react' -import TestUtils from 'react-dom/test-utils' -import { Provider } from 'react-redux' -import { ConnectedRouter } from 'react-router-redux' -import { type Match } from 'react-router-dom' -import Button from '~/components/layout/Button' -import { aNewStore, history } from '~/store' -import { addEtherTo } from '~/test/utils/tokenMovements' -import { aDeployedSafe, executeWithdrawOn } from '~/routes/safe/store/test/builder/deployedSafe.builder' -import { SAFELIST_ADDRESS } from '~/routes/routes' -import SafeView from '~/routes/safe/component/Safe' -import AppRoutes from '~/routes' -import { WITHDRAW_BUTTON_TEXT } from '~/routes/safe/component/Safe/DailyLimit' -import WithdrawComponent, { SEE_TXS_BUTTON_TEXT } from '~/routes/safe/component/Withdraw' -import { getBalanceInEtherOf } from '~/wallets/getWeb3' -import { sleep } from '~/utils/timer' -import { getDailyLimitFrom } from '~/routes/safe/component/Withdraw/withdraw' -import { type DailyLimitProps } from '~/routes/safe/store/model/dailyLimit' -import { WITHDRAW_INDEX } from '~/test/builder/safe.dom.utils' -import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps' -import { safeSelector } from '~/routes/safe/store/selectors/index' -import { filterMoveButtonsFrom } from '~/test/builder/safe.dom.builder' - -describe('React DOM TESTS > Withdraw funds from safe', () => { - let SafeDom - let store - let address - beforeEach(async () => { - // create store - store = aNewStore() - // deploy safe updating store - address = await aDeployedSafe(store) - // navigate to SAFE route - history.push(`${SAFELIST_ADDRESS}/${address}`) - SafeDom = TestUtils.renderIntoDocument(( - - - - - - )) - }) - - it('should withdraw funds under dailyLimit without needing confirmations', async () => { - // add funds to safe - await addEtherTo(address, '0.1') - await sleep(3000) - const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView) - - // $FlowFixMe - const expandedButtons = TestUtils.scryRenderedDOMComponentsWithTag(Safe, 'button') - const buttons = filterMoveButtonsFrom(expandedButtons) - const addWithdrawButton = buttons[WITHDRAW_INDEX] - expect(addWithdrawButton.getElementsByTagName('span')[0].innerHTML).toEqual(WITHDRAW_BUTTON_TEXT) - TestUtils.Simulate.click(addWithdrawButton) - await sleep(4000) - const Withdraw = TestUtils.findRenderedComponentWithType(SafeDom, WithdrawComponent) - - // $FlowFixMe - const inputs = TestUtils.scryRenderedDOMComponentsWithTag(Withdraw, 'input') - const amountInEth = inputs[0] - const toAddress = inputs[1] - TestUtils.Simulate.change(amountInEth, { target: { value: '0.01' } }) - TestUtils.Simulate.change(toAddress, { target: { value: store.getState().providers.account } }) - - // $FlowFixMe - const form = TestUtils.findRenderedDOMComponentWithTag(Withdraw, 'form') - - TestUtils.Simulate.submit(form) // fill the form - TestUtils.Simulate.submit(form) // confirming data - await sleep(6000) - - const safeBalance = await getBalanceInEtherOf(address) - expect(safeBalance).toBe('0.09') - - // $FlowFixMe - const withdrawButtons = TestUtils.scryRenderedComponentsWithType(Withdraw, Button) - const visitTxsButton = withdrawButtons[0] - expect(visitTxsButton.props.children).toEqual(SEE_TXS_BUTTON_TEXT) - }) - - it('spentToday dailyLimitModule property is updated correctly', async () => { - // add funds to safe - await addEtherTo(address, '0.1') - - const match: Match = buildMathPropsFrom(address) - const safe = safeSelector(store.getState(), { match }) - if (!safe) throw new Error() - await executeWithdrawOn(safe, 0.01) - await executeWithdrawOn(safe, 0.01) - - const ethAddress = 0 - const dailyLimit: DailyLimitProps = await getDailyLimitFrom(address, ethAddress) - - // THEN - expect(dailyLimit.value).toBe(0.5) - expect(dailyLimit.spentToday).toBe(0.02) - }) - - it('Withdraw button disabled when balance is 0', async () => { - const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView) - // $FlowFixMe - const buttons = TestUtils.scryRenderedDOMComponentsWithTag(Safe, 'button') - const addWithdrawButton = buttons[WITHDRAW_INDEX] - expect(addWithdrawButton.getElementsByTagName('span')[0].innerHTML).toEqual(WITHDRAW_BUTTON_TEXT) - expect(addWithdrawButton.hasAttribute('disabled')).toBe(true) - - await addEtherTo(address, '0.1') - await sleep(1800) - - expect(addWithdrawButton.hasAttribute('disabled')).toBe(false) - }) -}) diff --git a/src/test/safe.redux.withdraw.test.js b/src/test/safe.redux.withdraw.test.js new file mode 100644 index 00000000..af8a1a80 --- /dev/null +++ b/src/test/safe.redux.withdraw.test.js @@ -0,0 +1,84 @@ +// @flow +import * as React from 'react' +import TestUtils from 'react-dom/test-utils' +import { Provider } from 'react-redux' +import { ConnectedRouter } from 'react-router-redux' +import { aNewStore, history } from '~/store' +import { addEtherTo } from '~/test/utils/tokenMovements' +import { executeWithdrawOn } from '~/routes/safe/store/test/builder/deployedSafe.builder' +import { SAFELIST_ADDRESS } from '~/routes/routes' +import SafeView from '~/routes/safe/component/Safe' +import AppRoutes from '~/routes' +import { WITHDRAW_BUTTON_TEXT } from '~/routes/safe/component/Safe/DailyLimit' +import { getBalanceInEtherOf } from '~/wallets/getWeb3' +import { sleep } from '~/utils/timer' +import { getDailyLimitFrom } from '~/routes/safe/component/Withdraw/withdraw' +import { type DailyLimitProps } from '~/routes/safe/store/model/dailyLimit' +import { WITHDRAW_INDEX } from '~/test/builder/safe.dom.utils' +import { aMinedSafe } from '~/test/builder/safe.redux.builder' +import { getSafeFrom } from '~/test/utils/safeHelper' +import { filterMoveButtonsFrom } from '~/test/builder/safe.dom.builder' +import { fetchTokens } from '~/routes/tokens/store/actions/fetchTokens' + +describe('React DOM TESTS > Withdraw funds from safe', () => { + let store + let safeAddress: string + beforeEach(async () => { + store = aNewStore() + safeAddress = await aMinedSafe(store) + }) + + it('should withdraw funds under dailyLimit without needing confirmations', async () => { + // add funds to safe + await addEtherTo(safeAddress, '0.1') + + const safe = getSafeFrom(store.getState(), safeAddress) + await executeWithdrawOn(safe, 0.01) + + const safeBalance = await getBalanceInEtherOf(safeAddress) + expect(safeBalance).toBe('0.09') + }) + + it('spentToday dailyLimitModule property is updated correctly', async () => { + // add funds to safe + await addEtherTo(safeAddress, '0.1') + + const safe = getSafeFrom(store.getState(), safeAddress) + await executeWithdrawOn(safe, 0.01) + await executeWithdrawOn(safe, 0.01) + + const ethAddress = 0 + const dailyLimit: DailyLimitProps = await getDailyLimitFrom(safeAddress, ethAddress) + + // THEN + expect(dailyLimit.value).toBe(0.5) + expect(dailyLimit.spentToday).toBe(0.02) + }) + + it('Withdraw button disabled when balance is 0', async () => { + // navigate to SAFE route + history.push(`${SAFELIST_ADDRESS}/${safeAddress}`) + const SafeDom = TestUtils.renderIntoDocument(( + + + + + + )) + + await sleep(300) + const Safe = TestUtils.findRenderedComponentWithType(SafeDom, SafeView) + // $FlowFixMe + const buttons = TestUtils.scryRenderedDOMComponentsWithTag(Safe, 'button') + const filteredButtons = filterMoveButtonsFrom(buttons) + const addWithdrawButton = filteredButtons[WITHDRAW_INDEX] + expect(addWithdrawButton.getElementsByTagName('span')[0].innerHTML).toEqual(WITHDRAW_BUTTON_TEXT) + expect(addWithdrawButton.hasAttribute('disabled')).toBe(true) + + await addEtherTo(safeAddress, '0.1') + await store.dispatch(fetchTokens(safeAddress)) + await sleep(150) + + expect(addWithdrawButton.hasAttribute('disabled')).toBe(false) + }) +})