(Feature) Remove spam tokens / Deactivate tokens refresh fix (#1331)

* Fix load current session

* Fixs useMemo usage in filteredData

* Type fetchTokens

* Type useFetchTokens

* Type setCurrencyBalances

* Fixs ADD_SAFE reducer for existing safe, uses mergeDeep instead of merge, now the active tokens for the safe are not overwritten

* Fix save selected currency

* Adds excludeSpamTokens param in fetchTokenCurrenciesBalances

* Adds onlyTrustedTokens param in fetchTokenCurrenciesBalances

* Merge with development

* Remove onlyTrustedTokens param

* Fix unnecesary changes

* Replace Dispatch with ThunkDispatch

* Fix import consistency

* Type containsMethodByHash

* Fix blacklisted addresses calculation

* Adds types on updateActiveTokens
Adds types on updateBlacklistedTokens

* Refactor Tokens to TokenList, makes it functional component
also fix blacklisted addresses calculation

* Refactor Tokens to TokenList, makes it functional component
also fix blacklisted addresses calculation

* Refactor AddCustomToken, add types
Removes actions from Tokens

* Fix warning on useEffect

Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm>
This commit is contained in:
Agustin Pane 2020-09-15 08:12:30 -03:00 committed by GitHub
parent 8efafc1aaa
commit 33018172df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 210 additions and 247 deletions

View File

@ -11,9 +11,12 @@ export type BalanceEndpoint = {
usdConversion: string
}
const fetchTokenCurrenciesBalances = (safeAddress: string): Promise<AxiosResponse<BalanceEndpoint[]>> => {
const fetchTokenCurrenciesBalances = (
safeAddress: string,
excludeSpamTokens = true,
): Promise<AxiosResponse<BalanceEndpoint[]>> => {
const apiUrl = getTxServiceHost()
const url = `${apiUrl}safes/${safeAddress}/balances/usd/`
const url = `${apiUrl}safes/${safeAddress}/balances/usd/?exclude_spam=${excludeSpamTokens}`
return axios.get(url, {
params: {

View File

@ -1,9 +1,12 @@
import { createAction } from 'redux-actions'
import { BalanceCurrencyList } from 'src/logic/currencyValues/store/model/currencyValues'
export const SET_CURRENCY_BALANCES = 'SET_CURRENCY_BALANCES'
// eslint-disable-next-line max-len
export const setCurrencyBalances = createAction(SET_CURRENCY_BALANCES, (safeAddress, currencyBalances) => ({
export const setCurrencyBalances = createAction(
SET_CURRENCY_BALANCES,
(safeAddress: string, currencyBalances: BalanceCurrencyList) => ({
safeAddress,
currencyBalances,
}))
}),
)

View File

@ -1,4 +1,6 @@
import updateSafe from './updateSafe'
import { Set } from 'immutable'
import { Dispatch } from 'redux'
// the selector uses ownProps argument/router props to get the address of the safe
// so in order to use it I had to recreate the same structure
@ -10,7 +12,7 @@ import updateSafe from './updateSafe'
// },
// })
const updateActiveTokens = (safeAddress, activeTokens) => async (dispatch) => {
const updateActiveTokens = (safeAddress: string, activeTokens: Set<string>) => (dispatch: Dispatch): void => {
dispatch(updateSafe({ address: safeAddress, activeTokens }))
}

View File

@ -1,6 +1,8 @@
import updateSafe from './updateSafe'
import { Dispatch } from 'redux'
import { Set } from 'immutable'
const updateBlacklistedTokens = (safeAddress, blacklistedTokens) => async (dispatch) => {
const updateBlacklistedTokens = (safeAddress: string, blacklistedTokens: Set<string>) => (dispatch: Dispatch): void => {
dispatch(updateSafe({ address: safeAddress, blacklistedTokens }))
}

View File

@ -77,7 +77,7 @@ export default handleActions(
// with initial props and it would overwrite existing ones
if (state.hasIn(['safes', safe.address])) {
return state.updateIn(['safes', safe.address], (prevSafe) => prevSafe.merge(safe))
return state.updateIn(['safes', safe.address], (prevSafe) => prevSafe.mergeDeep(safe))
}
return state.setIn(['safes', safe.address], makeSafe(safe))

View File

@ -36,7 +36,7 @@ const cancellationTransactionsSelector = (state: AppReduxState) => state[CANCELL
const incomingTransactionsSelector = (state: AppReduxState) => state[INCOMING_TRANSACTIONS_REDUCER_ID]
export const safeParamAddressFromStateSelector = (state: AppReduxState): string | undefined => {
export const safeParamAddressFromStateSelector = (state: AppReduxState): string => {
const match = matchPath<{ safeAddress: string }>(state.router.location.pathname, {
path: `${SAFELIST_ADDRESS}/:safeAddress`,
})
@ -45,7 +45,7 @@ export const safeParamAddressFromStateSelector = (state: AppReduxState): string
return checksumAddress(match.params.safeAddress)
}
return undefined
return ''
}
export const safeParamAddressSelector = (

View File

@ -12,8 +12,10 @@ import { fetchTokenList } from 'src/logic/tokens/api'
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
import { tokensSelector } from 'src/logic/tokens/store/selectors'
import { getWeb3 } from 'src/logic/wallets/getWeb3'
import { store } from 'src/store'
import { AppReduxState, store } from 'src/store'
import { ensureOnce } from 'src/utils/singleton'
import { ThunkDispatch } from 'redux-thunk'
import { AnyAction } from 'redux'
const createStandardTokenContract = async () => {
const web3 = getWeb3()
@ -43,7 +45,7 @@ export const getStandardTokenContract = ensureOnce(createStandardTokenContract)
export const getERC721TokenContract = ensureOnce(createERC721TokenContract)
export const containsMethodByHash = async (contractAddress, methodHash) => {
export const containsMethodByHash = async (contractAddress: string, methodHash: string): Promise<boolean> => {
const web3 = getWeb3()
const byteCode = await web3.eth.getCode(contractAddress)
@ -87,7 +89,10 @@ export const getTokenInfos = async (tokenAddress: string): Promise<Token | undef
return token
}
export const fetchTokens = () => async (dispatch, getState) => {
export const fetchTokens = () => async (
dispatch: ThunkDispatch<AppReduxState, undefined, AnyAction>,
getState: () => AppReduxState,
): Promise<void> => {
try {
const currentSavedTokens = tokensSelector(getState())

View File

@ -9,8 +9,6 @@ import { Skeleton } from '@material-ui/lab'
import InfoIcon from 'src/assets/icons/info.svg'
import { useStyles } from './styles'
import Img from 'src/components/layout/Img'
import Table from 'src/components/Table'
import { cellWidth } from 'src/components/Table/TableHead'
@ -33,25 +31,16 @@ import {
} from 'src/routes/safe/components/Balances/dataFetcher'
import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/container/selector'
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
import { makeStyles } from '@material-ui/core/styles'
import { styles } from './styles'
const useStyles = makeStyles(styles)
type Props = {
showReceiveFunds: () => void
showSendFunds: (tokenAddress: string) => void
}
export type BalanceDataRow = List<{
asset: {
name: string
address: string
logoUri: string
}
assetOrder: string
balance: string
balanceOrder: number
fixed: boolean
value: string
}>
type CurrencyTooltipProps = {
valueWithCurrency: string
balanceWithSymbol: string
@ -84,16 +73,16 @@ const Coins = (props: Props): React.ReactElement => {
const activeTokens = useSelector(extendedSafeTokensSelector)
const currencyValues = useSelector(safeFiatBalancesListSelector)
const granted = useSelector(grantedSelector)
const [filteredData, setFilteredData] = React.useState<List<BalanceData>>(List())
const { trackEvent } = useAnalytics()
useEffect(() => {
trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Coins' })
}, [trackEvent])
useMemo(() => {
setFilteredData(getBalanceData(activeTokens, selectedCurrency, currencyValues, currencyRate))
}, [activeTokens, selectedCurrency, currencyValues, currencyRate])
const filteredData: List<BalanceData> = useMemo(
() => getBalanceData(activeTokens, selectedCurrency, currencyValues, currencyRate),
[activeTokens, selectedCurrency, currencyValues, currencyRate],
)
return (
<TableContainer>

View File

@ -1,7 +1,7 @@
import { makeStyles } from '@material-ui/core/styles'
import { sm, xs } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const useStyles = makeStyles({
export const styles = createStyles({
iconSmall: {
fontSize: 16,
},

View File

@ -1,13 +0,0 @@
import { addToken } from 'src/logic/tokens/store/actions/addToken'
import fetchTokens from 'src/logic/tokens/store/actions/fetchTokens'
import activateTokenForAllSafes from 'src/logic/safe/store/actions/activateTokenForAllSafes'
import updateActiveTokens from 'src/logic/safe/store/actions/updateActiveTokens'
import updateBlacklistedTokens from 'src/logic/safe/store/actions/updateBlacklistedTokens'
export default {
fetchTokens,
addToken,
updateActiveTokens,
updateBlacklistedTokens,
activateTokenForAllSafes,
}

View File

@ -1,11 +1,10 @@
import IconButton from '@material-ui/core/IconButton'
import { withStyles } from '@material-ui/core/styles'
import { makeStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close'
import React, { useState } from 'react'
import { connect, useSelector } from 'react-redux'
import { useSelector } from 'react-redux'
import actions from './actions'
import { styles } from './style'
import Hairline from 'src/components/layout/Hairline'
@ -16,27 +15,27 @@ import { orderedTokenListSelector } from 'src/logic/tokens/store/selectors'
import AddCustomAssetComponent from 'src/routes/safe/components/Balances/Tokens/screens/AddCustomAsset'
import AddCustomToken from 'src/routes/safe/components/Balances/Tokens/screens/AddCustomToken'
import AssetsList from 'src/routes/safe/components/Balances/Tokens/screens/AssetsList'
import TokenList from 'src/routes/safe/components/Balances/Tokens/screens/TokenList'
import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector'
import { safeBlacklistedTokensSelector } from 'src/logic/safe/store/selectors'
import { TokenList } from 'src/routes/safe/components/Balances/Tokens/screens/TokenList'
export const MANAGE_TOKENS_MODAL_CLOSE_BUTTON_TEST_ID = 'manage-tokens-close-modal-btn'
const Tokens = (props) => {
const {
activateTokenForAllSafes,
addToken,
classes,
fetchTokens,
modalScreen,
onClose,
safeAddress,
updateActiveTokens,
updateBlacklistedTokens,
} = props
const useStyles = makeStyles(styles)
type Props = {
safeAddress: string
modalScreen: string
onClose: () => void
}
const Tokens = (props: Props): React.ReactElement => {
const { modalScreen, onClose, safeAddress } = props
const tokens = useSelector(orderedTokenListSelector)
const activeTokens = useSelector(extendedSafeTokensSelector)
const blacklistedTokens = useSelector(safeBlacklistedTokensSelector)
const classes = useStyles()
const [activeScreen, setActiveScreen] = useState(modalScreen)
return (
@ -54,26 +53,20 @@ const Tokens = (props) => {
<TokenList
activeTokens={activeTokens}
blacklistedTokens={blacklistedTokens}
fetchTokens={fetchTokens}
safeAddress={safeAddress}
setActiveScreen={setActiveScreen}
tokens={tokens}
updateActiveTokens={updateActiveTokens}
updateBlacklistedTokens={updateBlacklistedTokens}
/>
)}
{activeScreen === 'assetsList' && <AssetsList setActiveScreen={setActiveScreen} />}
{activeScreen === 'addCustomToken' && (
<AddCustomToken
activateTokenForAllSafes={activateTokenForAllSafes}
activeTokens={activeTokens}
addToken={addToken}
onClose={onClose}
parentList={'tokenList'}
safeAddress={safeAddress}
setActiveScreen={setActiveScreen}
tokens={tokens}
updateActiveTokens={updateActiveTokens}
/>
)}
{activeScreen === 'addCustomAsset' && (
@ -83,6 +76,4 @@ const Tokens = (props) => {
)
}
const TokenComponent = withStyles(styles as any)(Tokens)
export default connect(undefined, actions)(TokenComponent)
export default Tokens

View File

@ -1,4 +1,4 @@
import { withStyles } from '@material-ui/core/styles'
import { makeStyles } from '@material-ui/core/styles'
import React, { useState } from 'react'
import { FormSpy } from 'react-final-form'
@ -22,6 +22,12 @@ import Row from 'src/components/layout/Row'
import TokenPlaceholder from 'src/routes/safe/components/Balances/assets/token_placeholder.svg'
import { checksumAddress } from 'src/utils/checksumAddress'
import { Checkbox } from '@gnosis.pm/safe-react-components'
import { useDispatch } from 'react-redux'
import { addToken } from 'src/logic/tokens/store/actions/addToken'
import updateActiveTokens from 'src/logic/safe/store/actions/updateActiveTokens'
import activateTokenForAllSafes from 'src/logic/safe/store/actions/activateTokenForAllSafes'
import { Token } from 'src/logic/tokens/store/model/token'
import { List, Set } from 'immutable'
export const ADD_CUSTOM_TOKEN_ADDRESS_INPUT_TEST_ID = 'add-custom-token-address-input'
export const ADD_CUSTOM_TOKEN_SYMBOLS_INPUT_TEST_ID = 'add-custom-token-symbols-input'
@ -35,20 +41,22 @@ const INITIAL_FORM_STATE = {
logoUri: '',
}
const AddCustomToken = (props) => {
const {
activateTokenForAllSafes,
activeTokens,
addToken,
classes,
onClose,
parentList,
safeAddress,
setActiveScreen,
tokens,
updateActiveTokens,
} = props
const useStyles = makeStyles(styles)
type Props = {
activeTokens: List<Token>
onClose: () => void
parentList: string
safeAddress: string
setActiveScreen: (screen: string) => void
tokens: List<Token>
}
const AddCustomToken = (props: Props): React.ReactElement => {
const { activeTokens, onClose, parentList, safeAddress, setActiveScreen, tokens } = props
const [formValues, setFormValues] = useState(INITIAL_FORM_STATE)
const classes = useStyles()
const dispatch = useDispatch()
const handleSubmit = (values) => {
const address = checksumAddress(values.address)
@ -59,12 +67,12 @@ const AddCustomToken = (props) => {
name: values.symbol,
}
addToken(token)
dispatch(addToken(token))
if (values.showForAllSafes) {
activateTokenForAllSafes(token.address)
dispatch(activateTokenForAllSafes(token.address))
} else {
const activeTokensAddresses = activeTokens.map(({ address }) => address)
updateActiveTokens(safeAddress, activeTokensAddresses.push(token.address))
const activeTokensAddresses = Set(activeTokens.map(({ address }) => address))
dispatch(updateActiveTokens(safeAddress, activeTokensAddresses.add(token.address)))
}
onClose()
@ -203,6 +211,4 @@ const AddCustomToken = (props) => {
)
}
const AddCustomTokenComponent = withStyles(styles as any)(AddCustomToken)
export default AddCustomTokenComponent
export default AddCustomToken

View File

@ -1,6 +1,7 @@
import { lg, md } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const styles = () => ({
export const styles = createStyles({
title: {
padding: `${lg} 0 20px`,
fontSize: md,

View File

@ -1,9 +1,9 @@
import CircularProgress from '@material-ui/core/CircularProgress'
import MuiList from '@material-ui/core/List'
import { withStyles } from '@material-ui/core/styles'
import { makeStyles } from '@material-ui/core/styles'
import Search from '@material-ui/icons/Search'
import cn from 'classnames'
import { Set } from 'immutable'
import { List, Set } from 'immutable'
import SearchBar from 'material-ui-search-bar'
import * as React from 'react'
import { FixedSizeList } from 'react-window'
@ -17,10 +17,15 @@ import Button from 'src/components/layout/Button'
import Divider from 'src/components/layout/Divider'
import Hairline from 'src/components/layout/Hairline'
import Row from 'src/components/layout/Row'
import { useEffect, useState } from 'react'
import { Token } from 'src/logic/tokens/store/model/token'
import { useDispatch } from 'react-redux'
import updateBlacklistedTokens from 'src/logic/safe/store/actions/updateBlacklistedTokens'
import updateActiveTokens from 'src/logic/safe/store/actions/updateActiveTokens'
export const ADD_CUSTOM_TOKEN_BUTTON_TEST_ID = 'add-custom-token-btn'
const filterBy = (filter, tokens) =>
const filterBy = (filter: string, tokens: List<Token>): List<Token> =>
tokens.filter(
(token) =>
!filter ||
@ -28,107 +33,77 @@ const filterBy = (filter, tokens) =>
token.name.toLowerCase().includes(filter.toLowerCase()),
)
// OPTIMIZATION IDEA (Thanks Andre)
// Calculate active tokens on component mount, store it in component state
// After user closes modal, dispatch an action so we don't have 100500 actions
// And selectors don't recalculate
const useStyles = makeStyles(styles)
class Tokens extends React.Component<any> {
renderCount = 0
state = {
filter: '',
activeTokensAddresses: Set([]),
initialActiveTokensAddresses: Set([]),
blacklistedTokensAddresses: Set([]),
activeTokensCalculated: false,
blacklistedTokensCalculated: false,
type Props = {
setActiveScreen: (newScreen: string) => void
tokens: List<Token>
activeTokens: List<Token>
blacklistedTokens: Set<string>
safeAddress: string
}
static getDerivedStateFromProps(nextProps, prevState) {
// I moved this logic here because if placed in ComponentDidMount
// the user would see Switches switch and this method fires before the component mounts
export const TokenList = (props: Props): React.ReactElement => {
const classes = useStyles()
const { setActiveScreen, tokens, activeTokens, blacklistedTokens, safeAddress } = props
const [activeTokensAddresses, setActiveTokensAddresses] = useState(Set(activeTokens.map(({ address }) => address)))
const [blacklistedTokensAddresses, setBlacklistedTokensAddresses] = useState<Set<string>>(blacklistedTokens)
const [filter, setFilter] = useState('')
const dispatch = useDispatch()
if (!prevState.activeTokensCalculated) {
const { activeTokens } = nextProps
return {
activeTokensAddresses: Set(activeTokens.map(({ address }) => address)),
initialActiveTokensAddresses: Set(activeTokens.map(({ address }) => address)),
activeTokensCalculated: true,
}
useEffect(() => {
return () => {
dispatch(updateActiveTokens(safeAddress, activeTokensAddresses))
dispatch(updateBlacklistedTokens(safeAddress, blacklistedTokensAddresses))
}
}, [dispatch, safeAddress, activeTokensAddresses, blacklistedTokensAddresses])
if (!prevState.blacklistedTokensCalculated) {
const { blacklistedTokens } = nextProps
return {
blacklistedTokensAddresses: blacklistedTokens,
blacklistedTokensCalculated: true,
}
}
return null
}
componentWillUnmount() {
const { activeTokensAddresses, blacklistedTokensAddresses } = this.state
const { safeAddress, updateActiveTokens, updateBlacklistedTokens } = this.props
updateActiveTokens(safeAddress, activeTokensAddresses)
updateBlacklistedTokens(safeAddress, blacklistedTokensAddresses)
}
onCancelSearch = () => {
this.setState(() => ({ filter: '' }))
}
onChangeSearchBar = (value) => {
this.setState(() => ({ filter: value }))
}
onSwitch = (token) => () => {
this.setState((prevState: any) => {
const activeTokensAddresses = prevState.activeTokensAddresses.has(token.address)
? prevState.activeTokensAddresses.remove(token.address)
: prevState.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) => ({
tokens,
activeTokensAddresses,
onSwitch: this.onSwitch,
})
getItemKey = (index, { tokens }) => {
const token = tokens.get(index)
return token.address
}
render() {
const { classes, setActiveScreen, tokens } = this.props
const { activeTokensAddresses, filter } = this.state
const searchClasses = {
input: classes.searchInput,
root: classes.searchRoot,
iconButton: classes.searchIcon,
searchContainer: classes.searchContainer,
}
const onCancelSearch = () => {
setFilter('')
this.setState(() => ({ filter: '' }))
}
const onChangeSearchBar = (value: string) => {
setFilter(value)
}
const onSwitch = (token: Token) => () => {
if (activeTokensAddresses.has(token.address)) {
const newTokens = activeTokensAddresses.remove(token.address)
setActiveTokensAddresses(newTokens)
setBlacklistedTokensAddresses(blacklistedTokensAddresses.add(token.address))
} else {
setActiveTokensAddresses(activeTokensAddresses.add(token.address))
setBlacklistedTokensAddresses(blacklistedTokensAddresses.remove(token.address))
}
}
const createItemData = (
tokens: List<Token>,
activeTokensAddresses: Set<string>,
): { tokens: List<Token>; activeTokensAddresses: Set<string>; onSwitch: (token: Token) => void } => {
return {
tokens,
activeTokensAddresses,
onSwitch: onSwitch,
}
}
const switchToAddCustomTokenScreen = () => setActiveScreen('addCustomToken')
const getItemKey = (index: number, { tokens }): string => {
return tokens.get(index).address
}
const filteredTokens = filterBy(filter, tokens)
const itemData = this.createItemData(filteredTokens, activeTokensAddresses)
const itemData = createItemData(filteredTokens, activeTokensAddresses)
return (
<>
@ -137,8 +112,8 @@ class Tokens extends React.Component<any> {
<Search className={classes.search} />
<SearchBar
classes={searchClasses}
onCancelSearch={this.onCancelSearch}
onChange={this.onChangeSearchBar}
onCancelSearch={onCancelSearch}
onChange={onChangeSearchBar}
placeholder="Search by name or symbol"
searchIcon={<div />}
value={filter}
@ -171,7 +146,7 @@ class Tokens extends React.Component<any> {
height={413}
itemCount={filteredTokens.size}
itemData={itemData}
itemKey={this.getItemKey}
itemKey={getItemKey}
itemSize={51}
overscanCount={process.env.NODE_ENV === 'test' ? 100 : 10}
width={500}
@ -183,8 +158,3 @@ class Tokens extends React.Component<any> {
</>
)
}
}
const TokenComponent = withStyles(styles as any)(Tokens)
export default TokenComponent

View File

@ -1,6 +1,7 @@
import { border, md, mediumFontSize, secondaryText, sm, xs } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const styles = () => ({
export const styles = createStyles({
root: {
minHeight: '52px',
},

View File

@ -1,6 +1,7 @@
import { lg, md } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const styles = () => ({
export const styles = createStyles({
heading: {
padding: `${md} ${lg}`,
justifyContent: 'space-between',

View File

@ -42,7 +42,9 @@ const getTokenPriceInCurrency = (
export interface BalanceData {
asset: { name: string; logoUri: string; address: string; symbol: string }
assetOrder: string
balance: string
balanceOrder: number
fixed: boolean
value: string
}

View File

@ -38,7 +38,7 @@ import safe, { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe'
import transactions, { TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/reducer/transactions'
import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/OpenSea'
import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe'
import allTransactions, { TRANSACTIONS, TransactionsState } from '../logic/safe/store/reducer/allTransactions'
import allTransactions, { TRANSACTIONS, TransactionsState } from 'src/logic/safe/store/reducer/allTransactions'
export const history = createHashHistory()