From 21b7a59f201f9b00ae317169f95c4674eda58efa Mon Sep 17 00:00:00 2001 From: Fernando Date: Thu, 5 Dec 2019 06:18:07 -0300 Subject: [PATCH] Feature #224: Activate tokens automatically (#300) * Replace 'Manage Tokens' with 'Manage List' * prevent 301 redirects * Add `BLACKLISTED_TOKENS` key to persist through immortal * Add store/action to extract _activate tokens by its balance_ - keeps already activated tokens - discards blacklisted tokens - adds tokens whose vales are bigger than zero and are not blacklisted * Add `blacklistedTokens` list to safe's store * Display activeTokensByBalance in 'Balances' screen * Enable token's blacklisting functionality in Tokens List * Retrieve balance from API * Rename action to `activateTokensByBalance` * Fix linting errors - line too long - required return * Do not persist a separate list into `BLACKLISTED_TOKENS` --- src/logic/tokens/api/fetchTokenBalanceList.js | 16 ++++++ src/logic/tokens/api/fetchTokenList.js | 2 +- .../store/actions/activateTokensByBalance.js | 49 +++++++++++++++++ src/logic/tokens/store/actions/removeToken.js | 2 +- .../components/Balances/Tokens/actions.js | 3 ++ .../safe/components/Balances/Tokens/index.jsx | 7 ++- .../Tokens/screens/TokenList/index.jsx | 53 ++++++++++++------- src/routes/safe/components/Balances/index.jsx | 21 +++++++- src/routes/safe/components/Layout.jsx | 6 +++ src/routes/safe/container/actions.js | 3 ++ src/routes/safe/container/index.jsx | 6 +++ src/routes/safe/container/selector.js | 3 ++ .../safe/store/actions/fetchTokenBalances.js | 3 +- .../store/actions/updateBlacklistedTokens.js | 13 +++++ .../safe/store/middleware/safeStorage.js | 10 ++-- src/routes/safe/store/models/safe.js | 2 + src/routes/safe/store/reducer/safe.js | 2 + src/routes/safe/store/selectors/index.js | 31 +++++++++++ 18 files changed, 202 insertions(+), 30 deletions(-) create mode 100644 src/logic/tokens/api/fetchTokenBalanceList.js create mode 100644 src/logic/tokens/store/actions/activateTokensByBalance.js create mode 100644 src/routes/safe/store/actions/updateBlacklistedTokens.js diff --git a/src/logic/tokens/api/fetchTokenBalanceList.js b/src/logic/tokens/api/fetchTokenBalanceList.js new file mode 100644 index 00000000..cc047a91 --- /dev/null +++ b/src/logic/tokens/api/fetchTokenBalanceList.js @@ -0,0 +1,16 @@ +// @flow +import axios from 'axios' +import { getTxServiceHost } from '~/config/index' + +const fetchTokenBalanceList = (safeAddress: string) => { + const apiUrl = getTxServiceHost() + const url = `${apiUrl}safes/${safeAddress}/balances/` + + return axios.get(url, { + params: { + limit: 300, + }, + }) +} + +export default fetchTokenBalanceList diff --git a/src/logic/tokens/api/fetchTokenList.js b/src/logic/tokens/api/fetchTokenList.js index 82cbb7ba..167e09a7 100644 --- a/src/logic/tokens/api/fetchTokenList.js +++ b/src/logic/tokens/api/fetchTokenList.js @@ -4,7 +4,7 @@ import { getRelayUrl } from '~/config/index' const fetchTokenList = () => { const apiUrl = getRelayUrl() - const url = `${apiUrl}/tokens` + const url = `${apiUrl}tokens/` return axios.get(url, { params: { diff --git a/src/logic/tokens/store/actions/activateTokensByBalance.js b/src/logic/tokens/store/actions/activateTokensByBalance.js new file mode 100644 index 00000000..a946535f --- /dev/null +++ b/src/logic/tokens/store/actions/activateTokensByBalance.js @@ -0,0 +1,49 @@ +// @flow +import type { Dispatch as ReduxDispatch } from 'redux' +import { Set } from 'immutable' +import { type GetState, type GlobalState } from '~/store' +import updateActiveTokens from '~/routes/safe/store/actions/updateActiveTokens' +import { + safeActiveTokensSelectorBySafe, + safeBlacklistedTokensSelectorBySafe, + safesMapSelector, +} from '~/routes/safe/store/selectors' +import fetchTokenBalanceList from '~/logic/tokens/api/fetchTokenBalanceList' +import updateSafe from '~/routes/safe/store/actions/updateSafe' + +const activateTokensByBalance = (safeAddress: string) => async ( + dispatch: ReduxDispatch, + getState: GetState, +) => { + try { + const result = await fetchTokenBalanceList(safeAddress) + const safes = safesMapSelector(getState()) + const alreadyActiveTokens = safeActiveTokensSelectorBySafe(safeAddress, safes) + const blacklistedTokens = safeBlacklistedTokensSelectorBySafe(safeAddress, safes) + + // addresses: potentially active tokens by balance + // balances: tokens' balance returned by the backend + const { addresses, balances } = result.data.reduce((acc, { tokenAddress, balance }) => ({ + addresses: [...acc.addresses, tokenAddress], + balances: [...acc.balances, balance], + }), { addresses: [], balances: [] }) + + // update balance list for the safe + dispatch(updateSafe({ address: safeAddress, balances: Set(balances) })) + + // active tokens by balance, excluding those already blacklisted and the `null` address + const activeByBalance = addresses.filter((address) => address !== null && !blacklistedTokens.includes(address)) + + // need to persist those already active tokens, despite its balances + const activeTokens = alreadyActiveTokens.toSet().union(activeByBalance) + + // update list of active tokens + dispatch(updateActiveTokens(safeAddress, activeTokens)) + } catch (err) { + console.error('Error fetching token list', err) + } + + return null +} + +export default activateTokensByBalance diff --git a/src/logic/tokens/store/actions/removeToken.js b/src/logic/tokens/store/actions/removeToken.js index 0dedb20a..91e1bc38 100644 --- a/src/logic/tokens/store/actions/removeToken.js +++ b/src/logic/tokens/store/actions/removeToken.js @@ -2,7 +2,7 @@ import { createAction } from 'redux-actions' import type { Dispatch as ReduxDispatch } from 'redux' import { type Token } from '~/logic/tokens/store/model/token' -import { removeTokenFromStorage, removeFromActiveTokens } from '~/logic/tokens/utils/tokensStorage' +import { removeFromActiveTokens, removeTokenFromStorage } from '~/logic/tokens/utils/tokensStorage' import { type GlobalState } from '~/store/index' export const REMOVE_TOKEN = 'REMOVE_TOKEN' diff --git a/src/routes/safe/components/Balances/Tokens/actions.js b/src/routes/safe/components/Balances/Tokens/actions.js index 06292b57..3f0dbc99 100644 --- a/src/routes/safe/components/Balances/Tokens/actions.js +++ b/src/routes/safe/components/Balances/Tokens/actions.js @@ -2,11 +2,13 @@ import fetchTokens from '~/logic/tokens/store/actions/fetchTokens' import { addToken } from '~/logic/tokens/store/actions/addToken' import updateActiveTokens from '~/routes/safe/store/actions/updateActiveTokens' +import updateBlacklistedTokens from '~/routes/safe/store/actions/updateBlacklistedTokens' import activateTokenForAllSafes from '~/routes/safe/store/actions/activateTokenForAllSafes' export type Actions = { fetchTokens: Function, updateActiveTokens: Function, + updateBlacklistedTokens: typeof updateBlacklistedTokens, addToken: Function, activateTokenForAllSafes: Function, } @@ -15,5 +17,6 @@ export default { fetchTokens, addToken, updateActiveTokens, + updateBlacklistedTokens, activateTokenForAllSafes, } diff --git a/src/routes/safe/components/Balances/Tokens/index.jsx b/src/routes/safe/components/Balances/Tokens/index.jsx index 18483d07..a23f0811 100644 --- a/src/routes/safe/components/Balances/Tokens/index.jsx +++ b/src/routes/safe/components/Balances/Tokens/index.jsx @@ -22,6 +22,7 @@ type Props = Actions & { tokens: List, safeAddress: string, activeTokens: List, + blacklistedTokens: List, } type ActiveScreen = 'tokenList' | 'addCustomToken' @@ -32,8 +33,10 @@ const Tokens = (props: Props) => { classes, tokens, activeTokens, + blacklistedTokens, fetchTokens, updateActiveTokens, + updateBlacklistedTokens, safeAddress, addToken, activateTokenForAllSafes, @@ -43,7 +46,7 @@ const Tokens = (props: Props) => { <> - Manage Tokens + Manage List @@ -54,8 +57,10 @@ const Tokens = (props: Props) => { diff --git a/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.jsx b/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.jsx index 8b13d3f6..8afba236 100644 --- a/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.jsx +++ b/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.jsx @@ -25,14 +25,17 @@ type Props = { tokens: List, safeAddress: string, activeTokens: List, - fetchTokens: Function, + blacklistedTokens: List, updateActiveTokens: Function, + updateBlacklistedTokens: Function, setActiveScreen: Function, } type State = { filter: string, activeTokensAddresses: Set, + initialActiveTokensAddresses: Set, + blacklistedTokensAddresses: Set, } const filterBy = (filter: string, tokens: List): List => tokens.filter( @@ -52,13 +55,10 @@ class Tokens extends React.Component { state = { filter: '', activeTokensAddresses: Set([]), + initialActiveTokensAddresses: Set([]), + blacklistedTokensAddresses: Set([]), activeTokensCalculated: false, - } - - componentDidMount() { - const { fetchTokens } = this.props - - fetchTokens() + blacklistedTokensCalculated: false, } static getDerivedStateFromProps(nextProps, prevState) { @@ -70,17 +70,29 @@ class Tokens extends React.Component { return { activeTokensAddresses: Set(activeTokens.map(({ address }) => address)), + initialActiveTokensAddresses: Set(activeTokens.map(({ address }) => address)), activeTokensCalculated: true, } } + + if (!prevState.blacklistedTokensCalculated) { + const { blacklistedTokens } = nextProps + + return { + blacklistedTokensAddresses: blacklistedTokens, + blacklistedTokensCalculated: true, + } + } + return null } componentWillUnmount() { - const { activeTokensAddresses } = this.state - const { updateActiveTokens, safeAddress } = this.props + const { activeTokensAddresses, blacklistedTokensAddresses } = this.state + const { updateActiveTokens, updateBlacklistedTokens, safeAddress } = this.props updateActiveTokens(safeAddress, activeTokensAddresses) + updateBlacklistedTokens(safeAddress, blacklistedTokensAddresses) } onCancelSearch = () => { @@ -92,17 +104,20 @@ class Tokens extends React.Component { } onSwitch = (token: Token) => () => { - const { activeTokensAddresses } = this.state + this.setState((prevState) => { + const activeTokensAddresses = prevState.activeTokensAddresses.has(token.address) + ? prevState.activeTokensAddresses.remove(token.address) + : prevState.activeTokensAddresses.add(token.address) - if (activeTokensAddresses.has(token.address)) { - this.setState({ - activeTokensAddresses: activeTokensAddresses.remove(token.address), - }) - } else { - this.setState({ - activeTokensAddresses: activeTokensAddresses.add(token.address), - }) - } + let { blacklistedTokensAddresses } = prevState + if (activeTokensAddresses.has(token.address)) { + blacklistedTokensAddresses = prevState.blacklistedTokensAddresses.remove(token.address) + } else if (prevState.initialActiveTokensAddresses.has(token.address)) { + blacklistedTokensAddresses = prevState.blacklistedTokensAddresses.add(token.address) + } + + return ({ ...prevState, activeTokensAddresses, blacklistedTokensAddresses }) + }) } createItemData = (tokens, activeTokensAddresses) => ({ diff --git a/src/routes/safe/components/Balances/index.jsx b/src/routes/safe/components/Balances/index.jsx index 63c0ab4c..85ea6d3d 100644 --- a/src/routes/safe/components/Balances/index.jsx +++ b/src/routes/safe/components/Balances/index.jsx @@ -38,6 +38,9 @@ type Props = { granted: boolean, tokens: List, activeTokens: List, + blacklistedTokens: List, + activateTokensByBalance: Function, + fetchTokens: Function, safeAddress: string, safeName: string, ethBalance: string, @@ -57,6 +60,7 @@ class Balances extends React.Component { }, showReceive: false, } + props.fetchTokens() } onShow = (action: Action) => () => { @@ -85,6 +89,17 @@ class Balances extends React.Component { }) } + handleChange = (e: SyntheticInputEvent) => { + const { checked } = e.target + + this.setState(() => ({ hideZero: checked })) + } + + componentDidMount(): void { + const { activateTokensByBalance, safeAddress } = this.props + activateTokensByBalance(safeAddress) + } + render() { const { showToken, showReceive, sendFunds, @@ -95,6 +110,7 @@ class Balances extends React.Component { tokens, safeAddress, activeTokens, + blacklistedTokens, safeName, ethBalance, createTransaction, @@ -110,10 +126,10 @@ class Balances extends React.Component { - Manage Tokens + Manage List { onClose={this.onHide('Token')} safeAddress={safeAddress} activeTokens={activeTokens} + blacklistedTokens={blacklistedTokens} /> diff --git a/src/routes/safe/components/Layout.jsx b/src/routes/safe/components/Layout.jsx index 9063c619..bb653fdc 100644 --- a/src/routes/safe/components/Layout.jsx +++ b/src/routes/safe/components/Layout.jsx @@ -60,9 +60,12 @@ const Layout = (props: Props) => { granted, tokens, activeTokens, + blacklistedTokens, createTransaction, processTransaction, fetchTransactions, + activateTokensByBalance, + fetchTokens, updateSafe, transactions, userAddress, @@ -156,8 +159,11 @@ const Layout = (props: Props) => { ethBalance={ethBalance} tokens={tokens} activeTokens={activeTokens} + blacklistedTokens={blacklistedTokens} granted={granted} safeAddress={address} + activateTokensByBalance={activateTokensByBalance} + fetchTokens={fetchTokens} safeName={name} createTransaction={createTransaction} /> diff --git a/src/routes/safe/container/actions.js b/src/routes/safe/container/actions.js index 03c49de9..19145afb 100644 --- a/src/routes/safe/container/actions.js +++ b/src/routes/safe/container/actions.js @@ -7,6 +7,7 @@ import processTransaction from '~/routes/safe/store/actions/processTransaction' import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions' import updateSafe from '~/routes/safe/store/actions/updateSafe' import fetchTokens from '~/logic/tokens/store/actions/fetchTokens' +import activateTokensByBalance from '~/logic/tokens/store/actions/activateTokensByBalance' export type Actions = { fetchSafe: typeof fetchSafe, @@ -17,6 +18,7 @@ export type Actions = { fetchTokens: typeof fetchTokens, processTransaction: typeof processTransaction, fetchEtherBalance: typeof fetchEtherBalance, + activateTokensByBalance: typeof activateTokensByBalance } export default { @@ -26,6 +28,7 @@ export default { processTransaction, fetchTokens, fetchTransactions, + activateTokensByBalance, updateSafe, fetchEtherBalance, checkAndUpdateSafeOwners, diff --git a/src/routes/safe/container/index.jsx b/src/routes/safe/container/index.jsx index 7104ad3c..c194da21 100644 --- a/src/routes/safe/container/index.jsx +++ b/src/routes/safe/container/index.jsx @@ -102,6 +102,7 @@ class SafeView extends React.Component { safe, provider, activeTokens, + blacklistedTokens, granted, userAddress, network, @@ -109,6 +110,8 @@ class SafeView extends React.Component { createTransaction, processTransaction, fetchTransactions, + activateTokensByBalance, + fetchTokens, updateSafe, transactions, } = this.props @@ -117,6 +120,7 @@ class SafeView extends React.Component { { createTransaction={createTransaction} processTransaction={processTransaction} fetchTransactions={fetchTransactions} + activateTokensByBalance={activateTokensByBalance} + fetchTokens={fetchTokens} updateSafe={updateSafe} transactions={transactions} sendFunds={sendFunds} diff --git a/src/routes/safe/container/selector.js b/src/routes/safe/container/selector.js index d8254506..7fadf156 100644 --- a/src/routes/safe/container/selector.js +++ b/src/routes/safe/container/selector.js @@ -5,6 +5,7 @@ import { safeSelector, safeActiveTokensSelector, safeBalancesSelector, + safeBlacklistedTokensSelector, type RouterProps, type SafeSelectorProps, } from '~/routes/safe/store/selectors' @@ -25,6 +26,7 @@ export type SelectorProps = { provider: string, tokens: List, activeTokens: List, + blacklistedTokens: List, userAddress: string, network: string, safeUrl: string, @@ -135,6 +137,7 @@ export default createStructuredSelector({ provider: providerNameSelector, tokens: orderedTokenListSelector, activeTokens: extendedSafeTokensSelector, + blacklistedTokens: safeBlacklistedTokensSelector, granted: grantedSelector, userAddress: userAccountSelector, network: networkSelector, diff --git a/src/routes/safe/store/actions/fetchTokenBalances.js b/src/routes/safe/store/actions/fetchTokenBalances.js index ab69d87b..bf008cde 100644 --- a/src/routes/safe/store/actions/fetchTokenBalances.js +++ b/src/routes/safe/store/actions/fetchTokenBalances.js @@ -19,7 +19,7 @@ export const calculateBalanceOf = async (tokenAddress: string, safeAddress: stri const token = await erc20Token.at(tokenAddress) balance = await token.balanceOf(safeAddress) } catch (err) { - console.error('Failed to fetch token balances: ', err) + console.error('Failed to fetch token balances: ', tokenAddress, err) } return new BigNumber(balance).div(10 ** decimals).toString() @@ -50,7 +50,6 @@ const fetchTokenBalances = (safeAddress: string, tokens: List) => async ( dispatch(updateSafe({ address: safeAddress, balances })) } catch (err) { - // eslint-disable-next-line console.error('Error when fetching token balances:', err) } } diff --git a/src/routes/safe/store/actions/updateBlacklistedTokens.js b/src/routes/safe/store/actions/updateBlacklistedTokens.js new file mode 100644 index 00000000..2602cfe6 --- /dev/null +++ b/src/routes/safe/store/actions/updateBlacklistedTokens.js @@ -0,0 +1,13 @@ +// @flow +import { Set } from 'immutable' +import type { Dispatch as ReduxDispatch } from 'redux' +import { type GlobalState } from '~/store' +import updateSafe from './updateSafe' + +const updateBlacklistedTokens = (safeAddress: string, blacklistedTokens: Set) => async ( + dispatch: ReduxDispatch, +) => { + dispatch(updateSafe({ address: safeAddress, blacklistedTokens })) +} + +export default updateBlacklistedTokens diff --git a/src/routes/safe/store/middleware/safeStorage.js b/src/routes/safe/store/middleware/safeStorage.js index a58ed541..1bb7e4b0 100644 --- a/src/routes/safe/store/middleware/safeStorage.js +++ b/src/routes/safe/store/middleware/safeStorage.js @@ -1,5 +1,5 @@ // @flow -import type { Store, AnyAction } from 'redux' +import type { AnyAction, Store } from 'redux' import { List } from 'immutable' import { ADD_SAFE } from '~/routes/safe/store/actions/addSafe' import { UPDATE_SAFE } from '~/routes/safe/store/actions/updateSafe' @@ -10,10 +10,12 @@ import { REPLACE_SAFE_OWNER } from '~/routes/safe/store/actions/replaceSafeOwner import { EDIT_SAFE_OWNER } from '~/routes/safe/store/actions/editSafeOwner' import { type GlobalState } from '~/store/' import { - saveSafes, setOwners, removeOwners, saveDefaultSafe, + removeOwners, + saveDefaultSafe, + saveSafes, + setOwners, } from '~/logic/safe/utils' -import { safesMapSelector, getActiveTokensAddressesForAllSafes } from '~/routes/safe/store/selectors' - +import { getActiveTokensAddressesForAllSafes, safesMapSelector } from '~/routes/safe/store/selectors' import { tokensSelector } from '~/logic/tokens/store/selectors' import type { Token } from '~/logic/tokens/store/model/token' import { makeOwner } from '~/routes/safe/store/models/owner' diff --git a/src/routes/safe/store/models/safe.js b/src/routes/safe/store/models/safe.js index 2ef03995..4b1209d4 100644 --- a/src/routes/safe/store/models/safe.js +++ b/src/routes/safe/store/models/safe.js @@ -12,6 +12,7 @@ export type SafeProps = { owners: List, balances?: Map, activeTokens: Set, + blacklistedTokens: Set, ethBalance?: string, } @@ -22,6 +23,7 @@ const SafeRecord: RecordFactory = Record({ ethBalance: 0, owners: List([]), activeTokens: new Set(), + blacklistedTokens: new Set(), balances: Map({}), }) diff --git a/src/routes/safe/store/reducer/safe.js b/src/routes/safe/store/reducer/safe.js index bdb67ad8..df7fed82 100644 --- a/src/routes/safe/store/reducer/safe.js +++ b/src/routes/safe/store/reducer/safe.js @@ -22,6 +22,7 @@ export const buildSafe = (storedSafe: SafeProps) => { const addresses = storedSafe.owners.map((owner: OwnerProps) => owner.address) const owners = buildOwnersFrom(Array.from(names), Array.from(addresses)) const activeTokens = Set(storedSafe.activeTokens) + const blacklistedTokens = Set(storedSafe.blacklistedTokens) const balances = Map(storedSafe.balances) const safe: SafeProps = { @@ -29,6 +30,7 @@ export const buildSafe = (storedSafe: SafeProps) => { owners, balances, activeTokens, + blacklistedTokens, } return safe diff --git a/src/routes/safe/store/selectors/index.js b/src/routes/safe/store/selectors/index.js index 46574d4b..2155744c 100644 --- a/src/routes/safe/store/selectors/index.js +++ b/src/routes/safe/store/selectors/index.js @@ -106,6 +106,21 @@ export const safeActiveTokensSelector: Selector> = createSelector( + safeSelector, + (safe: Safe) => { + if (!safe) { + return List() + } + + return safe.blacklistedTokens + }, +) + +export const safeActiveTokensSelectorBySafe = (safeAddress: string, safes: Map): List => safes.get(safeAddress).get('activeTokens') + +export const safeBlacklistedTokensSelectorBySafe = (safeAddress: string, safes: Map): List => safes.get(safeAddress).get('blacklistedTokens') + export const safeBalancesSelector: Selector> = createSelector( safeSelector, (safe: Safe) => { @@ -132,7 +147,23 @@ export const getActiveTokensAddressesForAllSafes: Selector> = createSelector( + safesListSelector, + (safes: List) => { + const addresses = Set().withMutations((set) => { + safes.forEach((safe: Safe) => { + safe.blacklistedTokens.forEach((tokenAddress) => { + set.add(tokenAddress) + }) + }) + }) + + return addresses + }, +) + export default createStructuredSelector({ safe: safeSelector, tokens: safeActiveTokensSelector, + blacklistedTokens: safeBlacklistedTokensSelector, })