From 84057d03eec64eb4c30b8cbc71b9369aa3ba85ab Mon Sep 17 00:00:00 2001 From: apanizo Date: Wed, 11 Jul 2018 13:01:58 +0200 Subject: [PATCH] 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), +)